electrum

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

commit ba2c737a2103a0cdc56ec631f4821ecebf956ca8
parent 2c0489c8094b2ba9409ae0e36bfe674ce327d883
Author: ThomasV <thomasv@electrum.org>
Date:   Mon, 23 Nov 2015 19:38:48 +0100

finish separation between plugins and GUIs

Diffstat:
Mgui/qt/main_window.py | 2+-
Mlib/plugins.py | 39+++++++++++++--------------------------
Mplugins/__init__.py | 101-------------------------------------------------------------------------------
Dplugins/audio_modem.py | 139-------------------------------------------------------------------------------
Aplugins/audio_modem/__init__.py | 7+++++++
Aplugins/audio_modem/qt.py | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/btchipwallet.py | 588-------------------------------------------------------------------------------
Dplugins/cosigner_pool.py | 203-------------------------------------------------------------------------------
Aplugins/cosigner_pool/__init__.py | 9+++++++++
Aplugins/cosigner_pool/qt.py | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/email_requests.py | 211-------------------------------------------------------------------------------
Aplugins/email_requests/__init__.py | 5+++++
Aplugins/email_requests/qt.py | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/exchange_rate.py | 537-------------------------------------------------------------------------------
Aplugins/exchange_rate/__init__.py | 5+++++
Aplugins/exchange_rate/exchange_rate.py | 365+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplugins/exchange_rate/kivy.py | 3+++
Aplugins/exchange_rate/qt.py | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/greenaddress_instant.py | 97-------------------------------------------------------------------------------
Aplugins/greenaddress_instant/__init__.py | 5+++++
Aplugins/greenaddress_instant/qt.py | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/keepkey.py | 691-------------------------------------------------------------------------------
Aplugins/keepkey/__init__.py | 8++++++++
Aplugins/keepkey/keepkey.py | 597+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplugins/keepkey/qt.py | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/labels.py | 209-------------------------------------------------------------------------------
Aplugins/labels/__init__.py | 9+++++++++
Aplugins/labels/kivy.py | 3+++
Aplugins/labels/labels.py | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplugins/labels/qt.py | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplugins/ledger/__init__.py | 8++++++++
Aplugins/ledger/ledger.py | 522+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplugins/ledger/qt.py | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/plot.py | 118-------------------------------------------------------------------------------
Aplugins/plot/__init__.py | 6++++++
Aplugins/plot/qt.py | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/trezor.py | 673-------------------------------------------------------------------------------
Aplugins/trezor/__init__.py | 9+++++++++
Aplugins/trezor/qt.py | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplugins/trezor/trezor.py | 486+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/trustedcoin.py | 681-------------------------------------------------------------------------------
Aplugins/trustedcoin/__init__.py | 11+++++++++++
Aplugins/trustedcoin/qt.py | 225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aplugins/trustedcoin/trustedcoin.py | 457+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dplugins/virtualkeyboard.py | 60------------------------------------------------------------
Aplugins/virtualkeyboard/__init__.py | 5+++++
Aplugins/virtualkeyboard/qt.py | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msetup.py | 18+++++++++++++++++-
48 files changed, 4367 insertions(+), 4336 deletions(-)

diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -2867,7 +2867,7 @@ class ElectrumWindow(QMainWindow, PrintError): run_hook('init_qt', self.gui_object) for i, descr in enumerate(plugins.descriptions): - name = descr['name'] + name = descr['__name__'] p = plugins.get(name) if descr.get('registers_wallet_type'): continue diff --git a/lib/plugins.py b/lib/plugins.py @@ -33,26 +33,25 @@ class Plugins(PrintError): if is_local: find = imp.find_module('plugins') plugins = imp.load_module('electrum_plugins', *find) - self.pathname = find[1] else: plugins = __import__('electrum_plugins') - self.pathname = None - + self.pkgpath = os.path.dirname(plugins.__file__) self.plugins = {} self.network = None self.gui_name = gui_name - self.descriptions = plugins.descriptions - for item in self.descriptions: - name = item['name'] - if gui_name not in item.get('available_for', []): + self.descriptions = [] + for loader, name, ispkg in pkgutil.iter_modules([self.pkgpath]): + m = loader.find_module(name).load_module(name) + d = m.__dict__ + if gui_name not in d.get('available_for', []): continue - x = item.get('registers_wallet_type') + self.descriptions.append(d) + x = d.get('registers_wallet_type') if x: self.register_wallet_type(config, name, x) - if config.get('use_' + name): + if not d.get('requires_wallet_type') and config.get('use_' + name): self.load_plugin(config, name) - def get(self, name): return self.plugins.get(name) @@ -60,22 +59,10 @@ class Plugins(PrintError): return len(self.plugins) def load_plugin(self, config, name): - full_name = 'electrum_plugins.' + name + full_name = 'electrum_plugins.' + name + '.' + self.gui_name try: - if self.pathname: # local - path = os.path.join(self.pathname, name + '.py') - p = imp.load_source(full_name, path) - else: - p = __import__(full_name, fromlist=['electrum_plugins']) - - if self.gui_name == 'qt': - klass = p.QtPlugin - elif self.gui_name == 'cmdline': - klass = p.CmdlinePlugin - else: - return - - plugin = klass(self, config, name) + p = pkgutil.find_loader(full_name).load_module(full_name) + plugin = p.Plugin(self, config, name) if self.network: self.network.add_jobs(plugin.thread_jobs()) self.plugins[name] = plugin @@ -103,7 +90,7 @@ class Plugins(PrintError): def is_available(self, name, w): for d in self.descriptions: - if d.get('name') == name: + if d.get('__name__') == name: break else: return False diff --git a/plugins/__init__.py b/plugins/__init__.py @@ -16,105 +16,4 @@ # 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 electrum -from electrum.i18n import _ -descriptions = [ - { - 'name': 'audio_modem', - 'fullname': _('Audio MODEM'), - 'description': _('Provides support for air-gapped transaction signing.'), - 'requires': [('amodem', 'http://github.com/romanz/amodem/')], - 'available_for': ['qt'], - }, - { - 'name': 'btchipwallet', - 'fullname': _('Ledger Wallet'), - 'description': _('Provides support for Ledger hardware wallet'), - 'requires': [('btchip', 'github.com/ledgerhq/btchip-python')], - 'requires_wallet_type': ['btchip'], - 'registers_wallet_type': ('hardware', 'btchip', _("Ledger wallet")), - 'available_for': ['qt', 'cmdline'], - }, - { - 'name': 'cosigner_pool', - 'fullname': _('Cosigner Pool'), - 'description': ' '.join([ - _("This plugin facilitates the use of multi-signatures wallets."), - _("It sends and receives partially signed transactions from/to your cosigner wallet."), - _("Transactions are encrypted and stored on a remote server.") - ]), - 'requires_wallet_type': ['2of2', '2of3'], - 'available_for': ['qt'], - }, - { - 'name': 'email_requests', - 'fullname': 'Email', - 'description': _("Send and receive payment request with an email account"), - 'available_for': ['qt'], - }, - { - 'name': 'exchange_rate', - 'fullname': _("Exchange rates"), - 'description': _("Exchange rates and currency conversion tools."), - 'available_for': ['qt','kivy'], - }, - { - 'name': 'greenaddress_instant', - 'fullname': 'GreenAddress instant', - 'description': _("Allows validating if your transactions have instant confirmations by GreenAddress"), - 'available_for': ['qt'], - }, - { - 'name':'keepkey', - 'fullname': 'KeepKey', - 'description': _('Provides support for KeepKey hardware wallet'), - 'requires': [('keepkeylib','github.com/keepkey/python-keepkey')], - 'requires_wallet_type': ['keepkey'], - 'registers_wallet_type': ('hardware', 'keepkey', _("KeepKey wallet")), - 'available_for': ['qt', 'cmdline'], - }, - { - 'name': 'labels', - 'fullname': _('LabelSync'), - 'description': '\n'.join([ - _("Synchronize your labels across multiple Electrum installs by using a remote database to save your data. Labels, transactions ids and addresses are encrypted before they are sent to the remote server."), - _("The label sync's server software is open-source as well and can be found on github.com/maran/electrum-sync-server") - ]), - 'available_for': ['qt','kivy'] - }, - { - 'name': 'plot', - 'fullname': 'Plot History', - 'description': _("Ability to plot transaction history in graphical mode."), - 'requires': [('matplotlib', 'matplotlib')], - 'available_for': ['qt'], - }, - { - 'name':'trezor', - 'fullname': 'Trezor Wallet', - 'description': _('Provides support for Trezor hardware wallet'), - 'requires': [('trezorlib','github.com/trezor/python-trezor')], - 'requires_wallet_type': ['trezor'], - 'registers_wallet_type': ('hardware', 'trezor', _("Trezor wallet")), - 'available_for': ['qt', 'cmdline'], - }, - { - 'name': 'trustedcoin', - 'fullname': _('Two Factor Authentication'), - 'description': ''.join([ - _("This plugin adds two-factor authentication to your wallet."), '<br/>', - _("For more information, visit"), - " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>" - ]), - 'requires_wallet_type': ['2fa'], - 'registers_wallet_type': ('twofactor', '2fa', _("Wallet with two-factor authentication")), - 'available_for': ['qt', 'cmdline'], - }, - { - 'name': 'virtualkeyboard', - 'fullname': 'Virtual Keyboard', - 'description': '%s\n%s' % (_("Add an optional virtual keyboard to the password dialog."), _("Warning: do not use this if it makes you pick a weaker password.")), - 'available_for': ['qt'], - } -] diff --git a/plugins/audio_modem.py b/plugins/audio_modem.py @@ -1,139 +0,0 @@ -from electrum.plugins import BasePlugin, hook -from electrum_gui.qt.util import WaitingDialog, EnterButton -from electrum.util import print_msg, print_error -from electrum.i18n import _ - -from PyQt4.QtGui import * -from PyQt4.QtCore import * - -import traceback -import zlib -import json -from io import BytesIO -import sys -import platform - -try: - import amodem.audio - import amodem.main - import amodem.config - print_error('Audio MODEM is available.') - amodem.log.addHandler(amodem.logging.StreamHandler(sys.stderr)) - amodem.log.setLevel(amodem.logging.INFO) -except ImportError: - amodem = None - print_error('Audio MODEM is not found.') - - -class QtPlugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - if self.is_available(): - self.modem_config = amodem.config.slowest() - self.library_name = { - 'Linux': 'libportaudio.so' - }[platform.system()] - - def is_available(self): - return amodem is not None - - def requires_settings(self): - return True - - def settings_widget(self, window): - return EnterButton(_('Settings'), self.settings_dialog) - - def settings_dialog(self): - d = QDialog() - d.setWindowTitle("Settings") - - layout = QGridLayout(d) - layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0) - - bitrates = list(sorted(amodem.config.bitrates.keys())) - - def _index_changed(index): - bitrate = bitrates[index] - self.modem_config = amodem.config.bitrates[bitrate] - - combo = QComboBox() - combo.addItems(map(str, bitrates)) - combo.currentIndexChanged.connect(_index_changed) - layout.addWidget(combo, 0, 1) - - ok_button = QPushButton(_("OK")) - ok_button.clicked.connect(d.accept) - layout.addWidget(ok_button, 1, 1) - - return bool(d.exec_()) - - @hook - def transaction_dialog(self, dialog): - b = QPushButton() - b.setIcon(QIcon(":icons/speaker.png")) - - def handler(): - blob = json.dumps(dialog.tx.as_dict()) - self.sender = self._send(parent=dialog, blob=blob) - self.sender.start() - b.clicked.connect(handler) - dialog.sharing_buttons.insert(-1, b) - - @hook - def scan_text_edit(self, parent): - def handler(): - self.receiver = self._recv(parent=parent) - self.receiver.start() - parent.addButton(':icons/microphone.png', handler, _("Read from microphone")) - - @hook - def show_text_edit(self, parent): - def handler(): - blob = str(parent.toPlainText()) - self.sender = self._send(parent=parent, blob=blob) - self.sender.start() - parent.addButton(':icons/speaker.png', handler, _("Send to speaker")) - - def _audio_interface(self): - interface = amodem.audio.Interface(config=self.modem_config) - return interface.load(self.library_name) - - def _send(self, parent, blob): - def sender_thread(): - try: - with self._audio_interface() as interface: - src = BytesIO(blob) - dst = interface.player() - amodem.main.send(config=self.modem_config, src=src, dst=dst) - except Exception: - traceback.print_exc() - - print_msg('Sending:', repr(blob)) - blob = zlib.compress(blob) - - kbps = self.modem_config.modem_bps / 1e3 - msg = 'Sending to Audio MODEM ({0:.1f} kbps)...'.format(kbps) - return WaitingDialog(parent=parent, message=msg, run_task=sender_thread) - - def _recv(self, parent): - def receiver_thread(): - try: - with self._audio_interface() as interface: - src = interface.recorder() - dst = BytesIO() - amodem.main.recv(config=self.modem_config, src=src, dst=dst) - return dst.getvalue() - except Exception: - traceback.print_exc() - - def on_success(blob): - if blob: - blob = zlib.decompress(blob) - print_msg('Received:', repr(blob)) - parent.setText(blob) - - kbps = self.modem_config.modem_bps / 1e3 - msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps) - return WaitingDialog(parent=parent, message=msg, - run_task=receiver_thread, on_success=on_success) diff --git a/plugins/audio_modem/__init__.py b/plugins/audio_modem/__init__.py @@ -0,0 +1,7 @@ +from electrum.i18n import _ + +fullname = _('Audio MODEM') +description = _('Provides support for air-gapped transaction signing.') +requires = [('amodem', 'http://github.com/romanz/amodem/')] +available_for = ['qt'] + diff --git a/plugins/audio_modem/qt.py b/plugins/audio_modem/qt.py @@ -0,0 +1,139 @@ +from electrum.plugins import BasePlugin, hook +from electrum_gui.qt.util import WaitingDialog, EnterButton +from electrum.util import print_msg, print_error +from electrum.i18n import _ + +from PyQt4.QtGui import * +from PyQt4.QtCore import * + +import traceback +import zlib +import json +from io import BytesIO +import sys +import platform + +try: + import amodem.audio + import amodem.main + import amodem.config + print_error('Audio MODEM is available.') + amodem.log.addHandler(amodem.logging.StreamHandler(sys.stderr)) + amodem.log.setLevel(amodem.logging.INFO) +except ImportError: + amodem = None + print_error('Audio MODEM is not found.') + + +class Plugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + if self.is_available(): + self.modem_config = amodem.config.slowest() + self.library_name = { + 'Linux': 'libportaudio.so' + }[platform.system()] + + def is_available(self): + return amodem is not None + + def requires_settings(self): + return True + + def settings_widget(self, window): + return EnterButton(_('Settings'), self.settings_dialog) + + def settings_dialog(self): + d = QDialog() + d.setWindowTitle("Settings") + + layout = QGridLayout(d) + layout.addWidget(QLabel(_('Bit rate [kbps]: ')), 0, 0) + + bitrates = list(sorted(amodem.config.bitrates.keys())) + + def _index_changed(index): + bitrate = bitrates[index] + self.modem_config = amodem.config.bitrates[bitrate] + + combo = QComboBox() + combo.addItems(map(str, bitrates)) + combo.currentIndexChanged.connect(_index_changed) + layout.addWidget(combo, 0, 1) + + ok_button = QPushButton(_("OK")) + ok_button.clicked.connect(d.accept) + layout.addWidget(ok_button, 1, 1) + + return bool(d.exec_()) + + @hook + def transaction_dialog(self, dialog): + b = QPushButton() + b.setIcon(QIcon(":icons/speaker.png")) + + def handler(): + blob = json.dumps(dialog.tx.as_dict()) + self.sender = self._send(parent=dialog, blob=blob) + self.sender.start() + b.clicked.connect(handler) + dialog.sharing_buttons.insert(-1, b) + + @hook + def scan_text_edit(self, parent): + def handler(): + self.receiver = self._recv(parent=parent) + self.receiver.start() + parent.addButton(':icons/microphone.png', handler, _("Read from microphone")) + + @hook + def show_text_edit(self, parent): + def handler(): + blob = str(parent.toPlainText()) + self.sender = self._send(parent=parent, blob=blob) + self.sender.start() + parent.addButton(':icons/speaker.png', handler, _("Send to speaker")) + + def _audio_interface(self): + interface = amodem.audio.Interface(config=self.modem_config) + return interface.load(self.library_name) + + def _send(self, parent, blob): + def sender_thread(): + try: + with self._audio_interface() as interface: + src = BytesIO(blob) + dst = interface.player() + amodem.main.send(config=self.modem_config, src=src, dst=dst) + except Exception: + traceback.print_exc() + + print_msg('Sending:', repr(blob)) + blob = zlib.compress(blob) + + kbps = self.modem_config.modem_bps / 1e3 + msg = 'Sending to Audio MODEM ({0:.1f} kbps)...'.format(kbps) + return WaitingDialog(parent=parent, message=msg, run_task=sender_thread) + + def _recv(self, parent): + def receiver_thread(): + try: + with self._audio_interface() as interface: + src = interface.recorder() + dst = BytesIO() + amodem.main.recv(config=self.modem_config, src=src, dst=dst) + return dst.getvalue() + except Exception: + traceback.print_exc() + + def on_success(blob): + if blob: + blob = zlib.decompress(blob) + print_msg('Received:', repr(blob)) + parent.setText(blob) + + kbps = self.modem_config.modem_bps / 1e3 + msg = 'Receiving from Audio MODEM ({0:.1f} kbps)...'.format(kbps) + return WaitingDialog(parent=parent, message=msg, + run_task=receiver_thread, on_success=on_success) diff --git a/plugins/btchipwallet.py b/plugins/btchipwallet.py @@ -1,588 +0,0 @@ -from binascii import unhexlify -from binascii import hexlify -from struct import pack,unpack -from sys import stderr -from time import sleep - -import electrum -from electrum.account import BIP32_Account -from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160 -from electrum.i18n import _ -from electrum.plugins import BasePlugin, hook -from electrum.transaction import deserialize -from electrum.wallet import BIP32_HD_Wallet, BIP32_Wallet - -from electrum.util import format_satoshis_plain, print_error, print_msg -import hashlib -import threading - -try: - from btchip.btchipComm import getDongle, DongleWait - from btchip.btchip import btchip - from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script - from btchip.bitcoinTransaction import bitcoinTransaction - from btchip.btchipPersoWizard import StartBTChipPersoDialog - from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware - from btchip.btchipException import BTChipException - BTCHIP = True - BTCHIP_DEBUG = False -except ImportError: - BTCHIP = False - - -class BTChipWallet(BIP32_HD_Wallet): - wallet_type = 'btchip' - root_derivation = "m/44'/0'" - - def __init__(self, storage): - BIP32_HD_Wallet.__init__(self, storage) - self.transport = None - self.client = None - self.mpk = None - self.device_checked = False - self.signing = False - self.force_watching_only = False - - def give_error(self, message, clear_client = False): - print_error(message) - if not self.signing: - QMessageBox.warning(QDialog(), _('Warning'), _(message), _('OK')) - else: - self.signing = False - if clear_client and self.client is not None: - self.client.bad = True - self.device_checked = False - raise Exception(message) - - def get_action(self): - if not self.accounts: - return 'create_accounts' - - def can_sign_xpubkey(self, x_pubkey): - xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) - return xpub in self.master_public_keys.values() - - def can_create_accounts(self): - return False - - def synchronize(self): - # synchronize existing accounts - BIP32_Wallet.synchronize(self) - # no further accounts for the moment - - def can_change_password(self): - return False - - def is_watching_only(self): - return self.force_watching_only - - def get_client(self, noPin=False): - if not BTCHIP: - self.give_error('please install github.com/btchip/btchip-python') - - aborted = False - if not self.client or self.client.bad: - try: - d = getDongle(BTCHIP_DEBUG) - self.client = btchip(d) - self.client.handler = self.plugin.handler - firmware = self.client.getFirmwareVersion()['version'].split(".") - if not checkFirmware(firmware): - d.close() - try: - updateFirmware() - except Exception, e: - aborted = True - raise e - d = getDongle(BTCHIP_DEBUG) - self.client = btchip(d) - try: - self.client.getOperationMode() - except BTChipException, e: - if (e.sw == 0x6985): - d.close() - dialog = StartBTChipPersoDialog() - dialog.exec_() - # Then fetch the reference again as it was invalidated - d = getDongle(BTCHIP_DEBUG) - self.client = btchip(d) - else: - raise e - if not noPin: - # Immediately prompts for the PIN - remaining_attempts = self.client.getVerifyPinRemainingAttempts() - if remaining_attempts <> 1: - msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts) - else: - msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." - confirmed, p, pin = self.password_dialog(msg) - if not confirmed: - aborted = True - raise Exception('Aborted by user - please unplug the dongle and plug it again before retrying') - pin = pin.encode() - self.client.verifyPin(pin) - - except BTChipException, e: - try: - self.client.dongle.close() - except: - pass - self.client = None - if (e.sw == 0x6faa): - raise Exception("Dongle is temporarily locked - please unplug it and replug it again") - if ((e.sw & 0xFFF0) == 0x63c0): - raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") - raise e - except Exception, e: - try: - self.client.dongle.close() - except: - pass - self.client = None - if not aborted: - raise Exception("Could not connect to your Ledger wallet. Please verify access permissions, PIN, or unplug the dongle and plug it again") - else: - raise e - self.client.bad = False - self.device_checked = False - self.proper_device = False - return self.client - - def address_id(self, address): - account_id, (change, address_index) = self.get_address_index(address) - return "44'/0'/%s'/%d/%d" % (account_id, change, address_index) - - def create_main_account(self, password): - self.create_account('Main account', None) #name, empty password - - def derive_xkeys(self, root, derivation, password): - derivation = derivation.replace(self.root_name,"44'/0'/") - xpub = self.get_public_key(derivation) - return xpub, None - - def get_private_key(self, address, password): - return [] - - def get_public_key(self, bip32_path): - # S-L-O-W - we don't handle the fingerprint directly, so compute it manually from the previous node - # This only happens once so it's bearable - self.get_client() # prompt for the PIN before displaying the dialog if necessary - self.plugin.handler.show_message("Computing master public key") - try: - splitPath = bip32_path.split('/') - fingerprint = 0 - if len(splitPath) > 1: - prevPath = "/".join(splitPath[0:len(splitPath) - 1]) - nodeData = self.get_client().getWalletPublicKey(prevPath) - publicKey = compress_public_key(nodeData['publicKey']) - h = hashlib.new('ripemd160') - h.update(hashlib.sha256(publicKey).digest()) - fingerprint = unpack(">I", h.digest()[0:4])[0] - nodeData = self.get_client().getWalletPublicKey(bip32_path) - publicKey = compress_public_key(nodeData['publicKey']) - depth = len(splitPath) - lastChild = splitPath[len(splitPath) - 1].split('\'') - if len(lastChild) == 1: - childnum = int(lastChild[0]) - else: - childnum = 0x80000000 | int(lastChild[0]) - xpub = "0488B21E".decode('hex') + chr(depth) + self.i4b(fingerprint) + self.i4b(childnum) + str(nodeData['chainCode']) + str(publicKey) - except Exception, e: - self.give_error(e, True) - finally: - self.plugin.handler.stop() - - return EncodeBase58Check(xpub) - - def get_master_public_key(self): - try: - if not self.mpk: - self.mpk = self.get_public_key("44'/0'") - return self.mpk - except Exception, e: - self.give_error(e, True) - - def i4b(self, x): - return pack('>I', x) - - def add_keypairs(self, tx, keypairs, password): - #do nothing - no priv keys available - pass - - def decrypt_message(self, pubkey, message, password): - self.give_error("Not supported") - - def sign_message(self, address, message, password): - if self.has_seed(): - return BIP32_HD_Wallet.sign_message(self, address, message, password) - use2FA = False - self.signing = True - self.get_client() # prompt for the PIN before displaying the dialog if necessary - if not self.check_proper_device(): - self.give_error('Wrong device or password') - address_path = self.address_id(address) - self.plugin.handler.show_message("Signing message ...") - try: - info = self.get_client().signMessagePrepare(address_path, message) - pin = "" - if info['confirmationNeeded']: - # TODO : handle different confirmation types. For the time being only supports keyboard 2FA - use2FA = True - confirmed, p, pin = self.password_dialog() - if not confirmed: - raise Exception('Aborted by user') - pin = pin.encode() - self.client.bad = True - self.device_checked = False - self.get_client(True) - signature = self.get_client().signMessageSign(pin) - except BTChipException, e: - if e.sw == 0x6a80: - self.give_error("Unfortunately, this message cannot be signed by the Ledger wallet. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.") - else: - self.give_error(e, True) - except Exception, e: - self.give_error(e, True) - finally: - self.plugin.handler.stop() - self.client.bad = use2FA - self.signing = False - - # Parse the ASN.1 signature - - rLength = signature[3] - r = signature[4 : 4 + rLength] - sLength = signature[4 + rLength + 1] - s = signature[4 + rLength + 2:] - if rLength == 33: - r = r[1:] - if sLength == 33: - s = s[1:] - r = str(r) - s = str(s) - - # And convert it - return chr(27 + 4 + (signature[0] & 0x01)) + r + s - - def sign_transaction(self, tx, password): - if self.has_seed(): - return BIP32_HD_Wallet.sign_transaction(self, tx, password) - if tx.is_complete(): - return - #if tx.error: - # raise BaseException(tx.error) - self.signing = True - inputs = [] - inputsPaths = [] - pubKeys = [] - trustedInputs = [] - redeemScripts = [] - signatures = [] - preparedTrustedInputs = [] - changePath = "" - changeAmount = None - output = None - outputAmount = None - use2FA = False - pin = "" - rawTx = tx.serialize() - # Fetch inputs of the transaction to sign - for txinput in tx.inputs: - if ('is_coinbase' in txinput and txinput['is_coinbase']): - self.give_error("Coinbase not supported") # should never happen - inputs.append([ self.transactions[txinput['prevout_hash']].raw, - txinput['prevout_n'] ]) - address = txinput['address'] - inputsPaths.append(self.address_id(address)) - pubKeys.append(self.get_public_keys(address)) - - # Recognize outputs - only one output and one change is authorized - if len(tx.outputs) > 2: # should never happen - self.give_error("Transaction with more than 2 outputs not supported") - for type, address, amount in tx.outputs: - assert type == 'address' - if self.is_change(address): - changePath = self.address_id(address) - changeAmount = amount - else: - if output <> None: # should never happen - self.give_error("Multiple outputs with no change not supported") - output = address - outputAmount = amount - - self.get_client() # prompt for the PIN before displaying the dialog if necessary - if not self.check_proper_device(): - self.give_error('Wrong device or password') - - self.plugin.handler.show_message("Signing Transaction ...") - try: - # Get trusted inputs from the original transactions - for utxo in inputs: - txtmp = bitcoinTransaction(bytearray(utxo[0].decode('hex'))) - trustedInputs.append(self.get_client().getTrustedInput(txtmp, utxo[1])) - # TODO : Support P2SH later - redeemScripts.append(txtmp.outputs[utxo[1]].script) - # Sign all inputs - firstTransaction = True - inputIndex = 0 - while inputIndex < len(inputs): - self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, - trustedInputs, redeemScripts[inputIndex]) - outputData = self.get_client().finalizeInput(output, format_satoshis_plain(outputAmount), - format_satoshis_plain(self.get_tx_fee(tx)), changePath, bytearray(rawTx.decode('hex'))) - if firstTransaction: - transactionOutput = outputData['outputData'] - if outputData['confirmationNeeded']: - # TODO : handle different confirmation types. For the time being only supports keyboard 2FA - self.plugin.handler.stop() - if 'keycardData' in outputData: - pin2 = "" - for keycardIndex in range(len(outputData['keycardData'])): - msg = "Do not enter your device PIN here !\r\n\r\n" + \ - "Your Ledger Wallet wants to talk to you and tell you a unique second factor code.\r\n" + \ - "For this to work, please match the character between stars of the output address using your security card\r\n\r\n" + \ - "Output address : " - for index in range(len(output)): - if index == outputData['keycardData'][keycardIndex]: - msg = msg + "*" + output[index] + "*" - else: - msg = msg + output[index] - msg = msg + "\r\n" - confirmed, p, pin = self.password_dialog(msg) - if not confirmed: - raise Exception('Aborted by user') - try: - pin2 = pin2 + chr(int(pin[0], 16)) - except: - raise Exception('Invalid PIN character') - pin = pin2 - else: - use2FA = True - confirmed, p, pin = self.password_dialog() - if not confirmed: - raise Exception('Aborted by user') - pin = pin.encode() - self.client.bad = True - self.device_checked = False - self.get_client(True) - self.plugin.handler.show_message("Signing ...") - else: - # Sign input with the provided PIN - inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], - pin) - inputSignature[0] = 0x30 # force for 1.4.9+ - signatures.append(inputSignature) - inputIndex = inputIndex + 1 - firstTransaction = False - except Exception, e: - self.give_error(e, True) - finally: - self.plugin.handler.stop() - - # Reformat transaction - inputIndex = 0 - while inputIndex < len(inputs): - # TODO : Support P2SH later - inputScript = get_regular_input_script(signatures[inputIndex], pubKeys[inputIndex][0].decode('hex')) - preparedTrustedInputs.append([ trustedInputs[inputIndex]['value'], inputScript ]) - inputIndex = inputIndex + 1 - updatedTransaction = format_transaction(transactionOutput, preparedTrustedInputs) - updatedTransaction = hexlify(updatedTransaction) - tx.update(updatedTransaction) - self.client.bad = use2FA - self.signing = False - - def check_proper_device(self): - pubKey = DecodeBase58Check(self.master_public_keys["x/0'"])[45:] - if not self.device_checked: - self.plugin.handler.show_message("Checking device") - try: - nodeData = self.get_client().getWalletPublicKey("44'/0'/0'") - except Exception, e: - self.give_error(e, True) - finally: - self.plugin.handler.stop() - pubKeyDevice = compress_public_key(nodeData['publicKey']) - self.device_checked = True - if pubKey != pubKeyDevice: - self.proper_device = False - else: - self.proper_device = True - - return self.proper_device - - def password_dialog(self, msg=None): - if not msg: - msg = _("Do not enter your device PIN here !\r\n\r\n" \ - "Your Ledger Wallet wants to talk to you and tell you a unique second factor code.\r\n" \ - "For this to work, please open a text editor (on a different computer / device if you believe this computer is compromised) and put your cursor into it, unplug your Ledger Wallet and plug it back in.\r\n" \ - "It should show itself to your computer as a keyboard and output the second factor along with a summary of the transaction it is signing into the text-editor.\r\n\r\n" \ - "Check that summary and then enter the second factor code here.\r\n" \ - "Before clicking OK, re-plug the device once more (unplug it and plug it again if you read the second factor code on the same computer)") - response = self.plugin.handler.prompt_auth(msg) - if response is None: - return False, None, None - return True, response, response - - -class Plugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self._is_available = self._init() - self.wallet = None - self.handler = None - - def constructor(self, s): - return BTChipWallet(s) - - def _init(self): - return BTCHIP - - def is_available(self): - if not self._is_available: - return False - if not self.wallet: - return False - if self.wallet.storage.get('wallet_type') != 'btchip': - return False - return True - - def set_enabled(self, enabled): - self.wallet.storage.put('use_' + self.name, enabled) - - def is_enabled(self): - if not self.is_available(): - return False - if self.wallet.has_seed(): - return False - return True - - def btchip_is_connected(self): - try: - self.wallet.get_client().getFirmwareVersion() - except: - return False - return True - - @hook - def close_wallet(self): - self.wallet = None - - @hook - def installwizard_load_wallet(self, wallet, window): - if type(wallet) != BTChipWallet: - return - self.load_wallet(wallet, window) - - @hook - def installwizard_restore(self, wizard, storage): - if storage.get('wallet_type') != 'btchip': - return - wallet = BTChipWallet(storage) - try: - wallet.create_main_account(None) - except BaseException as e: - QMessageBox.information(None, _('Error'), str(e), _('OK')) - return - return wallet - - @hook - def sign_tx(self, window, tx): - tx.error = None - try: - self.wallet.sign_transaction(tx, None) - except Exception as e: - tx.error = str(e) - -from PyQt4.Qt import QApplication, QMessageBox, QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, QThread, SIGNAL -import PyQt4.QtCore as QtCore -from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog - -class QtPlugin(Plugin): - - @hook - def load_wallet(self, wallet, window): - self.wallet = wallet - self.wallet.plugin = self - if self.handler is None: - self.handler = BTChipQTHandler(window) - if self.btchip_is_connected(): - if not self.wallet.check_proper_device(): - QMessageBox.information(window, _('Error'), _("This wallet does not match your Ledger device"), _('OK')) - self.wallet.force_watching_only = True - else: - QMessageBox.information(window, _('Error'), _("Ledger device not detected.\nContinuing in watching-only mode."), _('OK')) - self.wallet.force_watching_only = True - - -class BTChipQTHandler: - - def __init__(self, win): - self.win = win - self.win.connect(win, SIGNAL('btchip_done'), self.dialog_stop) - self.win.connect(win, SIGNAL('btchip_message_dialog'), self.message_dialog) - self.win.connect(win, SIGNAL('btchip_auth_dialog'), self.auth_dialog) - self.done = threading.Event() - - def stop(self): - self.win.emit(SIGNAL('btchip_done')) - - def show_message(self, msg): - self.message = msg - self.win.emit(SIGNAL('btchip_message_dialog')) - - def prompt_auth(self, msg): - self.done.clear() - self.message = msg - self.win.emit(SIGNAL('btchip_auth_dialog')) - self.done.wait() - return self.response - - def auth_dialog(self): - response = QInputDialog.getText(None, "Ledger Wallet Authentication", self.message, QLineEdit.Password) - if not response[1]: - self.response = None - else: - self.response = str(response[0]) - self.done.set() - - def message_dialog(self): - self.d = QDialog() - self.d.setModal(1) - self.d.setWindowTitle('Ledger') - self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) - l = QLabel(self.message) - vbox = QVBoxLayout(self.d) - vbox.addWidget(l) - self.d.show() - - def dialog_stop(self): - if self.d is not None: - self.d.hide() - self.d = None - -class CmdlinePlugin(Plugin): - @hook - def cmdline_load_wallet(self, wallet): - self.wallet = wallet - self.wallet.plugin = self - if self.handler is None: - self.handler = BTChipCmdLineHandler() - - -class BTChipCmdLineHandler: - - def stop(self): - pass - - def show_message(self, msg): - print_msg(msg) - - def prompt_auth(self, msg): - import getpass - print_msg(msg) - response = getpass.getpass('') - if len(response) == 0: - return None - return response diff --git a/plugins/cosigner_pool.py b/plugins/cosigner_pool.py @@ -1,203 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - lightweight Bitcoin client -# Copyright (C) 2014 Thomas Voegtlin -# -# 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 socket -import threading -import time -import xmlrpclib - -from PyQt4.QtGui import * -from PyQt4.QtCore import * - -from electrum import bitcoin, util -from electrum import transaction -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ - -from electrum_gui.qt.transaction_dialog import show_transaction - -import sys -import traceback - - -PORT = 12344 -HOST = 'ecdsa.net' -server = xmlrpclib.ServerProxy('http://%s:%d'%(HOST,PORT), allow_none=True) - - -class Listener(util.DaemonThread): - - def __init__(self, parent): - util.DaemonThread.__init__(self) - self.daemon = True - self.parent = parent - self.received = set() - self.keyhashes = [] - - def set_keyhashes(self, keyhashes): - self.keyhashes = keyhashes - - def clear(self, keyhash): - server.delete(keyhash) - self.received.remove(keyhash) - - def run(self): - while self.running: - if not self.keyhashes: - time.sleep(2) - continue - for keyhash in self.keyhashes: - if keyhash in self.received: - continue - try: - message = server.get(keyhash) - except Exception as e: - self.print_error("cannot contact cosigner pool") - time.sleep(30) - continue - if message: - self.received.add(keyhash) - self.print_error("received message for", keyhash) - self.parent.obj.emit(SIGNAL("cosigner:receive"), keyhash, - message) - # poll every 30 seconds - time.sleep(30) - - -class QtPlugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.listener = None - self.obj = QObject() - self.obj.connect(self.obj, SIGNAL('cosigner:receive'), self.on_receive) - self.keys = [] - self.cosigner_list = [] - - @hook - def on_new_window(self, window): - self.update(window) - - @hook - def on_close_window(self, window): - self.update(window) - - def is_available(self): - return True - - def update(self, window): - wallet = window.wallet - if wallet.wallet_type not in ['2of2', '2of3']: - return - if self.listener is None: - self.print_error("starting listener") - self.listener = Listener(self) - self.listener.start() - elif self.listener: - self.print_error("shutting down listener") - self.listener.stop() - self.listener = None - self.keys = [] - self.cosigner_list = [] - for key, xpub in wallet.master_public_keys.items(): - K = bitcoin.deserialize_xkey(xpub)[-1].encode('hex') - _hash = bitcoin.Hash(K).encode('hex') - if wallet.master_private_keys.get(key): - self.keys.append((key, _hash, window)) - else: - self.cosigner_list.append((window, xpub, K, _hash)) - if self.listener: - self.listener.set_keyhashes([t[1] for t in self.keys]) - - @hook - def transaction_dialog(self, d): - d.cosigner_send_button = b = QPushButton(_("Send to cosigner")) - b.clicked.connect(lambda: self.do_send(d.tx)) - d.buttons.insert(0, b) - self.transaction_dialog_update(d) - - @hook - def transaction_dialog_update(self, d): - if d.tx.is_complete() or d.wallet.can_sign(d.tx): - d.cosigner_send_button.hide() - return - for window, xpub, K, _hash in self.cosigner_list: - if window.wallet == d.wallet and self.cosigner_can_sign(d.tx, xpub): - d.cosigner_send_button.show() - break - else: - d.cosigner_send_button.hide() - - def cosigner_can_sign(self, tx, cosigner_xpub): - from electrum.transaction import x_to_xpub - xpub_set = set([]) - for txin in tx.inputs: - for x_pubkey in txin['x_pubkeys']: - xpub = x_to_xpub(x_pubkey) - if xpub: - xpub_set.add(xpub) - - return cosigner_xpub in xpub_set - - def do_send(self, tx): - for window, xpub, K, _hash in self.cosigner_list: - if not self.cosigner_can_sign(tx, xpub): - continue - message = bitcoin.encrypt_message(tx.raw, K) - try: - server.put(_hash, message) - except Exception as e: - traceback.print_exc(file=sys.stdout) - window.show_message("Failed to send transaction to cosigning pool.") - return - window.show_message("Your transaction was sent to the cosigning pool.\nOpen your cosigner wallet to retrieve it.") - - def on_receive(self, keyhash, message): - self.print_error("signal arrived for", keyhash) - for key, _hash, window in self.keys: - if _hash == keyhash: - break - else: - self.print_error("keyhash not found") - return - - wallet = window.wallet - if wallet.use_encryption: - password = window.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease enter your password to decrypt it.') - if not password: - return - else: - password = None - if not window.question(_("An encrypted transaction was retrieved from cosigning pool.\nDo you want to open it now?")): - return - - xprv = wallet.get_master_private_key(key, password) - if not xprv: - return - try: - k = bitcoin.deserialize_xkey(xprv)[-1].encode('hex') - EC = bitcoin.EC_KEY(k.decode('hex')) - message = EC.decrypt_message(message) - except Exception as e: - traceback.print_exc(file=sys.stdout) - window.show_message(str(e)) - return - - self.listener.clear(keyhash) - tx = transaction.Transaction(message) - show_transaction(tx, window, prompt_if_unsaved=True) diff --git a/plugins/cosigner_pool/__init__.py b/plugins/cosigner_pool/__init__.py @@ -0,0 +1,9 @@ +from electrum.i18n import _ +fullname = _('Cosigner Pool') +description = ' '.join([ + _("This plugin facilitates the use of multi-signatures wallets."), + _("It sends and receives partially signed transactions from/to your cosigner wallet."), + _("Transactions are encrypted and stored on a remote server.") +]) +requires_wallet_type = ['2of2', '2of3'] +available_for = ['qt'] diff --git a/plugins/cosigner_pool/qt.py b/plugins/cosigner_pool/qt.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2014 Thomas Voegtlin +# +# 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 socket +import threading +import time +import xmlrpclib + +from PyQt4.QtGui import * +from PyQt4.QtCore import * + +from electrum import bitcoin, util +from electrum import transaction +from electrum.plugins import BasePlugin, hook +from electrum.i18n import _ + +from electrum_gui.qt.transaction_dialog import show_transaction + +import sys +import traceback + + +PORT = 12344 +HOST = 'ecdsa.net' +server = xmlrpclib.ServerProxy('http://%s:%d'%(HOST,PORT), allow_none=True) + + +class Listener(util.DaemonThread): + + def __init__(self, parent): + util.DaemonThread.__init__(self) + self.daemon = True + self.parent = parent + self.received = set() + self.keyhashes = [] + + def set_keyhashes(self, keyhashes): + self.keyhashes = keyhashes + + def clear(self, keyhash): + server.delete(keyhash) + self.received.remove(keyhash) + + def run(self): + while self.running: + if not self.keyhashes: + time.sleep(2) + continue + for keyhash in self.keyhashes: + if keyhash in self.received: + continue + try: + message = server.get(keyhash) + except Exception as e: + self.print_error("cannot contact cosigner pool") + time.sleep(30) + continue + if message: + self.received.add(keyhash) + self.print_error("received message for", keyhash) + self.parent.obj.emit(SIGNAL("cosigner:receive"), keyhash, + message) + # poll every 30 seconds + time.sleep(30) + + +class Plugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.listener = None + self.obj = QObject() + self.obj.connect(self.obj, SIGNAL('cosigner:receive'), self.on_receive) + self.keys = [] + self.cosigner_list = [] + + @hook + def on_new_window(self, window): + self.update(window) + + @hook + def on_close_window(self, window): + self.update(window) + + def is_available(self): + return True + + def update(self, window): + wallet = window.wallet + if wallet.wallet_type not in ['2of2', '2of3']: + return + if self.listener is None: + self.print_error("starting listener") + self.listener = Listener(self) + self.listener.start() + elif self.listener: + self.print_error("shutting down listener") + self.listener.stop() + self.listener = None + self.keys = [] + self.cosigner_list = [] + for key, xpub in wallet.master_public_keys.items(): + K = bitcoin.deserialize_xkey(xpub)[-1].encode('hex') + _hash = bitcoin.Hash(K).encode('hex') + if wallet.master_private_keys.get(key): + self.keys.append((key, _hash, window)) + else: + self.cosigner_list.append((window, xpub, K, _hash)) + if self.listener: + self.listener.set_keyhashes([t[1] for t in self.keys]) + + @hook + def transaction_dialog(self, d): + d.cosigner_send_button = b = QPushButton(_("Send to cosigner")) + b.clicked.connect(lambda: self.do_send(d.tx)) + d.buttons.insert(0, b) + self.transaction_dialog_update(d) + + @hook + def transaction_dialog_update(self, d): + if d.tx.is_complete() or d.wallet.can_sign(d.tx): + d.cosigner_send_button.hide() + return + for window, xpub, K, _hash in self.cosigner_list: + if window.wallet == d.wallet and self.cosigner_can_sign(d.tx, xpub): + d.cosigner_send_button.show() + break + else: + d.cosigner_send_button.hide() + + def cosigner_can_sign(self, tx, cosigner_xpub): + from electrum.transaction import x_to_xpub + xpub_set = set([]) + for txin in tx.inputs: + for x_pubkey in txin['x_pubkeys']: + xpub = x_to_xpub(x_pubkey) + if xpub: + xpub_set.add(xpub) + + return cosigner_xpub in xpub_set + + def do_send(self, tx): + for window, xpub, K, _hash in self.cosigner_list: + if not self.cosigner_can_sign(tx, xpub): + continue + message = bitcoin.encrypt_message(tx.raw, K) + try: + server.put(_hash, message) + except Exception as e: + traceback.print_exc(file=sys.stdout) + window.show_message("Failed to send transaction to cosigning pool.") + return + window.show_message("Your transaction was sent to the cosigning pool.\nOpen your cosigner wallet to retrieve it.") + + def on_receive(self, keyhash, message): + self.print_error("signal arrived for", keyhash) + for key, _hash, window in self.keys: + if _hash == keyhash: + break + else: + self.print_error("keyhash not found") + return + + wallet = window.wallet + if wallet.use_encryption: + password = window.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease enter your password to decrypt it.') + if not password: + return + else: + password = None + if not window.question(_("An encrypted transaction was retrieved from cosigning pool.\nDo you want to open it now?")): + return + + xprv = wallet.get_master_private_key(key, password) + if not xprv: + return + try: + k = bitcoin.deserialize_xkey(xprv)[-1].encode('hex') + EC = bitcoin.EC_KEY(k.decode('hex')) + message = EC.decrypt_message(message) + except Exception as e: + traceback.print_exc(file=sys.stdout) + window.show_message(str(e)) + return + + self.listener.clear(keyhash) + tx = transaction.Transaction(message) + show_transaction(tx, window, prompt_if_unsaved=True) diff --git a/plugins/email_requests.py b/plugins/email_requests.py @@ -1,211 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - Lightweight Bitcoin Client -# Copyright (C) 2015 Thomas Voegtlin -# -# 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/>. - -from __future__ import absolute_import - -import socket -import time -import threading -import base64 -from decimal import Decimal -from Queue import Queue - -import smtplib -import imaplib -import email -from email.MIMEMultipart import MIMEMultipart -from email.MIMEBase import MIMEBase -from email import Encoders - -from PyQt4.QtGui import * -from PyQt4.QtCore import * -import PyQt4.QtCore as QtCore -import PyQt4.QtGui as QtGui - -from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED -from electrum.plugins import BasePlugin, hook -from electrum import util -from electrum.paymentrequest import PaymentRequest -from electrum.i18n import _ -from electrum_gui.qt.util import text_dialog, EnterButton - - - -class Processor(threading.Thread): - polling_interval = 5*60 - - def __init__(self, imap_server, username, password, callback): - threading.Thread.__init__(self) - self.daemon = True - self.username = username - self.password = password - self.imap_server = imap_server - self.on_receive = callback - - def poll(self): - try: - self.M.select() - except: - return - typ, data = self.M.search(None, 'ALL') - for num in data[0].split(): - typ, msg_data = self.M.fetch(num, '(RFC822)') - msg = email.message_from_string(msg_data[0][1]) - p = msg.get_payload() - if not msg.is_multipart(): - p = [p] - continue - for item in p: - if item.get_content_type() == "application/bitcoin-paymentrequest": - pr_str = item.get_payload() - pr_str = base64.b64decode(pr_str) - self.on_receive(pr_str) - - def run(self): - self.M = imaplib.IMAP4_SSL(self.imap_server) - self.M.login(self.username, self.password) - while True: - self.poll() - time.sleep(self.polling_interval) - self.M.close() - self.M.logout() - - def send(self, recipient, message, payment_request): - msg = MIMEMultipart() - msg['Subject'] = message - msg['To'] = recipient - msg['From'] = self.username - part = MIMEBase('application', "bitcoin-paymentrequest") - part.set_payload(payment_request) - Encoders.encode_base64(part) - part.add_header('Content-Disposition', 'attachment; filename="payreq.btc"') - msg.attach(part) - s = smtplib.SMTP_SSL(self.imap_server, timeout=2) - s.login(self.username, self.password) - s.sendmail(self.username, [recipient], msg.as_string()) - s.quit() - - -class QtPlugin(BasePlugin): - - def fullname(self): - return 'Email' - - def description(self): - return _("Send and receive payment requests via email") - - def is_available(self): - return True - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.imap_server = self.config.get('email_server', '') - self.username = self.config.get('email_username', '') - self.password = self.config.get('email_password', '') - if self.imap_server and self.username and self.password: - self.processor = Processor(self.imap_server, self.username, self.password, self.on_receive) - self.processor.start() - self.obj = QObject() - self.obj.connect(self.obj, SIGNAL('email:new_invoice'), self.new_invoice) - - def on_receive(self, pr_str): - self.print_error('received payment request') - self.pr = PaymentRequest(pr_str) - self.obj.emit(SIGNAL('email:new_invoice')) - - def new_invoice(self): - self.parent.invoices.add(self.pr) - #window.update_invoices_list() - - @hook - def receive_list_menu(self, menu, addr): - window = menu.parentWidget() - menu.addAction(_("Send via e-mail"), lambda: self.send(window, addr)) - - def send(self, window, addr): - from electrum import paymentrequest - r = window.wallet.receive_requests.get(addr) - message = r.get('memo', '') - if r.get('signature'): - pr = paymentrequest.serialize_request(r) - else: - pr = paymentrequest.make_request(self.config, r) - if not pr: - return - recipient, ok = QtGui.QInputDialog.getText(window, 'Send request', 'Email invoice to:') - if not ok: - return - recipient = str(recipient) - payload = pr.SerializeToString() - self.print_error('sending mail to', recipient) - try: - self.processor.send(recipient, message, payload) - except BaseException as e: - window.show_message(str(e)) - return - - window.show_message(_('Request sent.')) - - - def requires_settings(self): - return True - - def settings_widget(self, window): - self.settings_window = window - return EnterButton(_('Settings'), self.settings_dialog) - - def settings_dialog(self, x): - from electrum_gui.qt.util import Buttons, CloseButton, OkButton - - d = QDialog(self.settings_window) - d.setWindowTitle("Email settings") - d.setMinimumSize(500, 200) - - vbox = QVBoxLayout(d) - vbox.addWidget(QLabel(_('Server hosting your email acount'))) - grid = QGridLayout() - vbox.addLayout(grid) - grid.addWidget(QLabel('Server (IMAP)'), 0, 0) - server_e = QLineEdit() - server_e.setText(self.imap_server) - grid.addWidget(server_e, 0, 1) - - grid.addWidget(QLabel('Username'), 1, 0) - username_e = QLineEdit() - username_e.setText(self.username) - grid.addWidget(username_e, 1, 1) - - grid.addWidget(QLabel('Password'), 2, 0) - password_e = QLineEdit() - password_e.setText(self.password) - grid.addWidget(password_e, 2, 1) - - vbox.addStretch() - vbox.addLayout(Buttons(CloseButton(d), OkButton(d))) - - if not d.exec_(): - return - - server = str(server_e.text()) - self.config.set_key('email_server', server) - - username = str(username_e.text()) - self.config.set_key('email_username', username) - - password = str(password_e.text()) - self.config.set_key('email_password', password) diff --git a/plugins/email_requests/__init__.py b/plugins/email_requests/__init__.py @@ -0,0 +1,5 @@ +from electrum.i18n import _ + +fullname = _('Email') +description = _("Send and receive payment request with an email account") +available_for = ['qt'] diff --git a/plugins/email_requests/qt.py b/plugins/email_requests/qt.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# +# Electrum - Lightweight Bitcoin Client +# Copyright (C) 2015 Thomas Voegtlin +# +# 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/>. + +from __future__ import absolute_import + +import socket +import time +import threading +import base64 +from decimal import Decimal +from Queue import Queue + +import smtplib +import imaplib +import email +from email.MIMEMultipart import MIMEMultipart +from email.MIMEBase import MIMEBase +from email import Encoders + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +import PyQt4.QtCore as QtCore +import PyQt4.QtGui as QtGui + +from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED +from electrum.plugins import BasePlugin, hook +from electrum import util +from electrum.paymentrequest import PaymentRequest +from electrum.i18n import _ +from electrum_gui.qt.util import text_dialog, EnterButton + + + +class Processor(threading.Thread): + polling_interval = 5*60 + + def __init__(self, imap_server, username, password, callback): + threading.Thread.__init__(self) + self.daemon = True + self.username = username + self.password = password + self.imap_server = imap_server + self.on_receive = callback + + def poll(self): + try: + self.M.select() + except: + return + typ, data = self.M.search(None, 'ALL') + for num in data[0].split(): + typ, msg_data = self.M.fetch(num, '(RFC822)') + msg = email.message_from_string(msg_data[0][1]) + p = msg.get_payload() + if not msg.is_multipart(): + p = [p] + continue + for item in p: + if item.get_content_type() == "application/bitcoin-paymentrequest": + pr_str = item.get_payload() + pr_str = base64.b64decode(pr_str) + self.on_receive(pr_str) + + def run(self): + self.M = imaplib.IMAP4_SSL(self.imap_server) + self.M.login(self.username, self.password) + while True: + self.poll() + time.sleep(self.polling_interval) + self.M.close() + self.M.logout() + + def send(self, recipient, message, payment_request): + msg = MIMEMultipart() + msg['Subject'] = message + msg['To'] = recipient + msg['From'] = self.username + part = MIMEBase('application', "bitcoin-paymentrequest") + part.set_payload(payment_request) + Encoders.encode_base64(part) + part.add_header('Content-Disposition', 'attachment; filename="payreq.btc"') + msg.attach(part) + s = smtplib.SMTP_SSL(self.imap_server, timeout=2) + s.login(self.username, self.password) + s.sendmail(self.username, [recipient], msg.as_string()) + s.quit() + + +class Plugin(BasePlugin): + + def fullname(self): + return 'Email' + + def description(self): + return _("Send and receive payment requests via email") + + def is_available(self): + return True + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.imap_server = self.config.get('email_server', '') + self.username = self.config.get('email_username', '') + self.password = self.config.get('email_password', '') + if self.imap_server and self.username and self.password: + self.processor = Processor(self.imap_server, self.username, self.password, self.on_receive) + self.processor.start() + self.obj = QObject() + self.obj.connect(self.obj, SIGNAL('email:new_invoice'), self.new_invoice) + + def on_receive(self, pr_str): + self.print_error('received payment request') + self.pr = PaymentRequest(pr_str) + self.obj.emit(SIGNAL('email:new_invoice')) + + def new_invoice(self): + self.parent.invoices.add(self.pr) + #window.update_invoices_list() + + @hook + def receive_list_menu(self, menu, addr): + window = menu.parentWidget() + menu.addAction(_("Send via e-mail"), lambda: self.send(window, addr)) + + def send(self, window, addr): + from electrum import paymentrequest + r = window.wallet.receive_requests.get(addr) + message = r.get('memo', '') + if r.get('signature'): + pr = paymentrequest.serialize_request(r) + else: + pr = paymentrequest.make_request(self.config, r) + if not pr: + return + recipient, ok = QtGui.QInputDialog.getText(window, 'Send request', 'Email invoice to:') + if not ok: + return + recipient = str(recipient) + payload = pr.SerializeToString() + self.print_error('sending mail to', recipient) + try: + self.processor.send(recipient, message, payload) + except BaseException as e: + window.show_message(str(e)) + return + + window.show_message(_('Request sent.')) + + + def requires_settings(self): + return True + + def settings_widget(self, window): + self.settings_window = window + return EnterButton(_('Settings'), self.settings_dialog) + + def settings_dialog(self, x): + from electrum_gui.qt.util import Buttons, CloseButton, OkButton + + d = QDialog(self.settings_window) + d.setWindowTitle("Email settings") + d.setMinimumSize(500, 200) + + vbox = QVBoxLayout(d) + vbox.addWidget(QLabel(_('Server hosting your email acount'))) + grid = QGridLayout() + vbox.addLayout(grid) + grid.addWidget(QLabel('Server (IMAP)'), 0, 0) + server_e = QLineEdit() + server_e.setText(self.imap_server) + grid.addWidget(server_e, 0, 1) + + grid.addWidget(QLabel('Username'), 1, 0) + username_e = QLineEdit() + username_e.setText(self.username) + grid.addWidget(username_e, 1, 1) + + grid.addWidget(QLabel('Password'), 2, 0) + password_e = QLineEdit() + password_e.setText(self.password) + grid.addWidget(password_e, 2, 1) + + vbox.addStretch() + vbox.addLayout(Buttons(CloseButton(d), OkButton(d))) + + if not d.exec_(): + return + + server = str(server_e.text()) + self.config.set_key('email_server', server) + + username = str(username_e.text()) + self.config.set_key('email_username', username) + + password = str(password_e.text()) + self.config.set_key('email_password', password) diff --git a/plugins/exchange_rate.py b/plugins/exchange_rate.py @@ -1,537 +0,0 @@ -from datetime import datetime -import inspect -import requests -import sys -from threading import Thread -import time -import traceback -import csv -from decimal import Decimal -from functools import partial - -from electrum.bitcoin import COIN -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ -from electrum.util import PrintError, ThreadJob, timestamp_to_datetime -from electrum.util import format_satoshis - - -# See https://en.wikipedia.org/wiki/ISO_4217 -CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, - 'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0, - 'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3, - 'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0, - 'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0, - 'VUV': 0, 'XAF': 0, 'XAG': 2, 'XAU': 4, 'XOF': 0, - 'XPF': 0} - -class ExchangeBase(PrintError): - - def __init__(self, on_quotes, on_history): - self.history = {} - self.quotes = {} - self.on_quotes = on_quotes - self.on_history = on_history - - def protocol(self): - return "https" - - def get_json(self, site, get_string): - url = "".join([self.protocol(), '://', site, get_string]) - response = requests.request('GET', url, - headers={'User-Agent' : 'Electrum'}) - return response.json() - - def get_csv(self, site, get_string): - url = "".join([self.protocol(), '://', site, get_string]) - response = requests.request('GET', url, - headers={'User-Agent' : 'Electrum'}) - reader = csv.DictReader(response.content.split('\n')) - return list(reader) - - def name(self): - return self.__class__.__name__ - - def update_safe(self, ccy): - try: - self.print_error("getting fx quotes for", ccy) - self.quotes = self.get_rates(ccy) - self.print_error("received fx quotes") - self.on_quotes() - except Exception, e: - self.print_error("failed fx quotes:", e) - - def update(self, ccy): - t = Thread(target=self.update_safe, args=(ccy,)) - t.setDaemon(True) - t.start() - - def get_historical_rates_safe(self, ccy): - try: - self.print_error("requesting fx history for", ccy) - self.history[ccy] = self.historical_rates(ccy) - self.print_error("received fx history for", ccy) - self.on_history() - except Exception, e: - self.print_error("failed fx history:", e) - - def get_historical_rates(self, ccy): - result = self.history.get(ccy) - if not result and ccy in self.history_ccys(): - t = Thread(target=self.get_historical_rates_safe, args=(ccy,)) - t.setDaemon(True) - t.start() - return result - - def history_ccys(self): - return [] - - def historical_rate(self, ccy, d_t): - return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d')) - - -class BitcoinAverage(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('api.bitcoinaverage.com', '/ticker/global/all') - return dict([(r, Decimal(json[r]['last'])) - for r in json if r != 'timestamp']) - - def history_ccys(self): - return ['AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'EUR', 'GBP', 'IDR', 'ILS', - 'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD', - 'ZAR'] - - def historical_rates(self, ccy): - history = self.get_csv('api.bitcoinaverage.com', - "/history/%s/per_day_all_time_history.csv" % ccy) - return dict([(h['datetime'][:10], h['average']) - for h in history]) - -class BitcoinVenezuela(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('api.bitcoinvenezuela.com', '/') - rates = [(r, json['BTC'][r]) for r in json['BTC'] - if json['BTC'][r] is not None] # Giving NULL for LTC - return dict(rates) - - def protocol(self): - return "http" - - def history_ccys(self): - return ['ARS', 'EUR', 'USD', 'VEF'] - - def historical_rates(self, ccy): - return self.get_json('api.bitcoinvenezuela.com', - "/historical/index.php?coin=BTC")[ccy +'_BTC'] - -class BTCParalelo(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('btcparalelo.com', '/api/price') - return {'VEF': Decimal(json['price'])} - - def protocol(self): - return "http" - -class Bitcurex(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('pln.bitcurex.com', '/data/ticker.json') - pln_price = json['last'] - return {'PLN': Decimal(pln_price)} - -class Bitmarket(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json') - return {'PLN': Decimal(json['last'])} - -class BitPay(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('bitpay.com', '/api/rates') - return dict([(r['code'], Decimal(r['rate'])) for r in json]) - -class BitStamp(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('www.bitstamp.net', '/api/ticker/') - return {'USD': Decimal(json['last'])} - -class BlockchainInfo(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('blockchain.info', '/ticker') - return dict([(r, Decimal(json[r]['15m'])) for r in json]) - - def name(self): - return "Blockchain" - -class BTCChina(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('data.btcchina.com', '/data/ticker') - return {'CNY': Decimal(json['ticker']['last'])} - -class CaVirtEx(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('www.cavirtex.com', '/api/CAD/ticker.json') - return {'CAD': Decimal(json['last'])} - -class Coinbase(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('coinbase.com', - '/api/v1/currencies/exchange_rates') - return dict([(r[7:].upper(), Decimal(json[r])) - for r in json if r.startswith('btc_to_')]) - -class CoinDesk(ExchangeBase): - def get_rates(self, ccy): - dicts = self.get_json('api.coindesk.com', - '/v1/bpi/supported-currencies.json') - json = self.get_json('api.coindesk.com', - '/v1/bpi/currentprice/%s.json' % ccy) - ccys = [d['currency'] for d in dicts] - result = dict.fromkeys(ccys) - result[ccy] = Decimal(json['bpi'][ccy]['rate_float']) - return result - - def history_starts(self): - return { 'USD': '2012-11-30' } - - def history_ccys(self): - return self.history_starts().keys() - - def historical_rates(self, ccy): - start = self.history_starts()[ccy] - end = datetime.today().strftime('%Y-%m-%d') - # Note ?currency and ?index don't work as documented. Sigh. - query = ('/v1/bpi/historical/close.json?start=%s&end=%s' - % (start, end)) - json = self.get_json('api.coindesk.com', query) - return json['bpi'] - -class itBit(ExchangeBase): - def get_rates(self, ccy): - ccys = ['USD', 'EUR', 'SGD'] - json = self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy) - result = dict.fromkeys(ccys) - if ccy in ccys: - result[ccy] = Decimal(json['lastPrice']) - return result - -class LocalBitcoins(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('localbitcoins.com', - '/bitcoinaverage/ticker-all-currencies/') - return dict([(r, Decimal(json[r]['rates']['last'])) for r in json]) - -class Winkdex(ExchangeBase): - def get_rates(self, ccy): - json = self.get_json('winkdex.com', '/api/v0/price') - return {'USD': Decimal(json['price'] / 100.0)} - - def history_ccys(self): - return ['USD'] - - def historical_rates(self, ccy): - json = self.get_json('winkdex.com', - "/api/v0/series?start_time=1342915200") - history = json['series'][0]['results'] - return dict([(h['timestamp'][:10], h['price'] / 100.0) - for h in history]) - - -class Plugin(BasePlugin, ThreadJob): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.ccy = self.config_ccy() - self.history_used_spot = False - self.ccy_combo = None - self.hist_checkbox = None - self.app = None - is_exchange = lambda obj: (inspect.isclass(obj) - and issubclass(obj, ExchangeBase) - and obj != ExchangeBase) - self.exchanges = dict(inspect.getmembers(sys.modules[__name__], - is_exchange)) - self.set_exchange(self.config_exchange()) - - def ccy_amount_str(self, amount, commas): - prec = CCY_PRECISIONS.get(self.ccy, 2) - fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) - return fmt_str.format(round(amount, prec)) - - def thread_jobs(self): - return [self] - - def run(self): - # This runs from the network thread which catches exceptions - if self.timeout <= time.time(): - self.timeout = time.time() + 150 - self.exchange.update(self.ccy) - - def config_ccy(self): - '''Use when dynamic fetching is needed''' - return self.config.get("currency", "EUR") - - def config_exchange(self): - return self.config.get('use_exchange', 'Blockchain') - - def config_history(self): - return self.config.get('history_rates', 'unchecked') != 'unchecked' - - def show_history(self): - return self.config_history() and self.exchange.history_ccys() - - - def set_exchange(self, name): - class_ = self.exchanges.get(name) or self.exchanges.values()[0] - name = class_.__name__ - self.print_error("using exchange", name) - if self.config_exchange() != name: - self.config.set_key('use_exchange', name, True) - - on_quotes = lambda: self.app.emit(SIGNAL('new_fx_quotes')) - on_history = lambda: self.app.emit(SIGNAL('new_fx_history')) - self.exchange = class_(on_quotes, on_history) - # A new exchange means new fx quotes, initially empty. Force - # a quote refresh - self.timeout = 0 - self.get_historical_rates() - #self.on_fx_quotes() - - - - def exchange_rate(self): - '''Returns None, or the exchange rate as a Decimal''' - rate = self.exchange.quotes.get(self.ccy) - if rate: - return Decimal(rate) - - @hook - def format_amount_and_units(self, btc_balance): - rate = self.exchange_rate() - return '' if rate is None else " (%s %s)" % (self.value_str(btc_balance, rate), self.ccy) - - @hook - def get_fiat_status_text(self, btc_balance): - rate = self.exchange_rate() - return _(" (No FX rate available)") if rate is None else "1 BTC~%s %s" % (self.value_str(COIN, rate), self.ccy) - - def get_historical_rates(self): - if self.show_history(): - self.exchange.get_historical_rates(self.ccy) - - def requires_settings(self): - return True - - def value_str(self, satoshis, rate): - if satoshis is None: # Can happen with incomplete history - return _("Unknown") - if rate: - value = Decimal(satoshis) / COIN * Decimal(rate) - return "%s" % (self.ccy_amount_str(value, True)) - return _("No data") - - @hook - def historical_value_str(self, satoshis, d_t): - rate = self.exchange.historical_rate(self.ccy, d_t) - # Frequently there is no rate for today, until tomorrow :) - # Use spot quotes in that case - if rate is None and (datetime.today().date() - d_t.date()).days <= 2: - rate = self.exchange.quotes.get(self.ccy) - self.history_used_spot = True - return self.value_str(satoshis, rate) - - @hook - def history_tab_headers(self, headers): - if self.show_history(): - headers.extend(['%s '%self.ccy + _('Amount'), '%s '%self.ccy + _('Balance')]) - - @hook - def history_tab_update_begin(self): - self.history_used_spot = False - - @hook - def history_tab_update(self, tx, entry): - if not self.show_history(): - return - tx_hash, conf, value, timestamp, balance = tx - if conf <= 0: - date = datetime.today() - else: - date = timestamp_to_datetime(timestamp) - for amount in [value, balance]: - text = self.historical_value_str(amount, date) - entry.append(text) - - - - -from PyQt4.QtGui import * -from PyQt4.QtCore import * -from electrum_gui.qt.util import * -from electrum_gui.qt.amountedit import AmountEdit - - -class QtPlugin(Plugin): - - - def connect_fields(self, window, btc_e, fiat_e, fee_e): - - def edit_changed(edit): - edit.setStyleSheet(BLACK_FG) - fiat_e.is_last_edited = (edit == fiat_e) - amount = edit.get_amount() - rate = self.exchange_rate() - if rate is None or amount is None: - if edit is fiat_e: - btc_e.setText("") - if fee_e: - fee_e.setText("") - else: - fiat_e.setText("") - else: - if edit is fiat_e: - btc_e.setAmount(int(amount / Decimal(rate) * COIN)) - if fee_e: window.update_fee() - btc_e.setStyleSheet(BLUE_FG) - else: - fiat_e.setText(self.ccy_amount_str( - amount * Decimal(rate) / COIN, False)) - fiat_e.setStyleSheet(BLUE_FG) - - fiat_e.textEdited.connect(partial(edit_changed, fiat_e)) - btc_e.textEdited.connect(partial(edit_changed, btc_e)) - fiat_e.is_last_edited = False - - @hook - def init_qt(self, gui): - self.app = gui.app - - @hook - def do_clear(self, window): - window.fiat_send_e.setText('') - - def close(self): - # Get rid of hooks before updating status bars. - BasePlugin.close(self) - self.app.emit(SIGNAL('close_fx_plugin')) - - def restore_window(self, window): - window.update_status() - window.history_list.refresh_headers() - window.fiat_send_e.hide() - window.fiat_receive_e.hide() - - def on_fx_history(self, window): - '''Called when historical fx quotes are updated''' - window.history_list.update() - - def on_fx_quotes(self, window): - '''Called when fresh spot fx quotes come in''' - window.update_status() - self.populate_ccy_combo() - # Refresh edits with the new rate - edit = window.fiat_send_e if window.fiat_send_e.is_last_edited else window.amount_e - edit.textEdited.emit(edit.text()) - edit = window.fiat_receive_e if window.fiat_receive_e.is_last_edited else window.receive_amount_e - edit.textEdited.emit(edit.text()) - # History tab needs updating if it used spot - if self.history_used_spot: - self.on_fx_history(window) - - def on_ccy_combo_change(self): - '''Called when the chosen currency changes''' - ccy = str(self.ccy_combo.currentText()) - if ccy and ccy != self.ccy: - self.ccy = ccy - self.config.set_key('currency', ccy, True) - self.app.emit(SIGNAL('new_fx_quotes')) - self.get_historical_rates() # Because self.ccy changes - self.hist_checkbox_update() - - def hist_checkbox_update(self): - if self.hist_checkbox: - self.hist_checkbox.setEnabled(self.ccy in self.exchange.history_ccys()) - self.hist_checkbox.setChecked(self.config_history()) - - def populate_ccy_combo(self): - # There should be at most one instance of the settings dialog - combo = self.ccy_combo - # NOTE: bool(combo) is False if it is empty. Nuts. - if combo is not None: - combo.blockSignals(True) - combo.clear() - combo.addItems(sorted(self.exchange.quotes.keys())) - combo.blockSignals(False) - combo.setCurrentIndex(combo.findText(self.ccy)) - - @hook - def on_new_window(self, window): - # Additional send and receive edit boxes - send_e = AmountEdit(self.config_ccy) - window.send_grid.addWidget(send_e, 4, 2, Qt.AlignLeft) - window.amount_e.frozen.connect( - lambda: send_e.setFrozen(window.amount_e.isReadOnly())) - receive_e = AmountEdit(self.config_ccy) - window.receive_grid.addWidget(receive_e, 2, 2, Qt.AlignLeft) - window.fiat_send_e = send_e - window.fiat_receive_e = receive_e - self.connect_fields(window, window.amount_e, send_e, window.fee_e) - self.connect_fields(window, window.receive_amount_e, receive_e, None) - window.history_list.refresh_headers() - window.update_status() - window.connect(window.app, SIGNAL('new_fx_quotes'), lambda: self.on_fx_quotes(window)) - window.connect(window.app, SIGNAL('new_fx_history'), lambda: self.on_fx_history(window)) - window.connect(window.app, SIGNAL('close_fx_plugin'), lambda: self.restore_window(window)) - window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers) - - def settings_widget(self, window): - return EnterButton(_('Settings'), self.settings_dialog) - - def settings_dialog(self): - d = QDialog() - d.setWindowTitle("Settings") - layout = QGridLayout(d) - layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0) - layout.addWidget(QLabel(_('Currency: ')), 1, 0) - layout.addWidget(QLabel(_('History Rates: ')), 2, 0) - - # Currency list - self.ccy_combo = QComboBox() - self.ccy_combo.currentIndexChanged.connect(self.on_ccy_combo_change) - self.populate_ccy_combo() - - def on_change_ex(idx): - exchange = str(combo_ex.currentText()) - if exchange != self.exchange.name(): - self.set_exchange(exchange) - self.hist_checkbox_update() - - def on_change_hist(checked): - if checked: - self.config.set_key('history_rates', 'checked') - self.get_historical_rates() - else: - self.config.set_key('history_rates', 'unchecked') - self.app.emit(SIGNAL('refresh_headers')) - - def ok_clicked(): - self.timeout = 0 - self.ccy_combo = None - d.accept() - - combo_ex = QComboBox() - combo_ex.addItems(sorted(self.exchanges.keys())) - combo_ex.setCurrentIndex(combo_ex.findText(self.config_exchange())) - combo_ex.currentIndexChanged.connect(on_change_ex) - - self.hist_checkbox = QCheckBox() - self.hist_checkbox.stateChanged.connect(on_change_hist) - self.hist_checkbox_update() - - ok_button = QPushButton(_("OK")) - ok_button.clicked.connect(lambda: ok_clicked()) - - layout.addWidget(self.ccy_combo,1,1) - layout.addWidget(combo_ex,0,1) - layout.addWidget(self.hist_checkbox,2,1) - layout.addWidget(ok_button,3,1) - - return d.exec_() diff --git a/plugins/exchange_rate/__init__.py b/plugins/exchange_rate/__init__.py @@ -0,0 +1,5 @@ +from electrum.i18n import _ + +fullname = _("Exchange rates") +description = _("Exchange rates and currency conversion tools.") +available_for = ['qt','kivy'] diff --git a/plugins/exchange_rate/exchange_rate.py b/plugins/exchange_rate/exchange_rate.py @@ -0,0 +1,365 @@ +from datetime import datetime +import inspect +import requests +import sys +from threading import Thread +import time +import traceback +import csv +from decimal import Decimal + +from electrum.bitcoin import COIN +from electrum.plugins import BasePlugin, hook +from electrum.i18n import _ +from electrum.util import PrintError, ThreadJob, timestamp_to_datetime +from electrum.util import format_satoshis + + +# See https://en.wikipedia.org/wiki/ISO_4217 +CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, + 'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0, + 'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3, + 'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0, + 'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0, + 'VUV': 0, 'XAF': 0, 'XAG': 2, 'XAU': 4, 'XOF': 0, + 'XPF': 0} + +class ExchangeBase(PrintError): + + def __init__(self, on_quotes, on_history): + self.history = {} + self.quotes = {} + self.on_quotes = on_quotes + self.on_history = on_history + + def protocol(self): + return "https" + + def get_json(self, site, get_string): + url = "".join([self.protocol(), '://', site, get_string]) + response = requests.request('GET', url, + headers={'User-Agent' : 'Electrum'}) + return response.json() + + def get_csv(self, site, get_string): + url = "".join([self.protocol(), '://', site, get_string]) + response = requests.request('GET', url, + headers={'User-Agent' : 'Electrum'}) + reader = csv.DictReader(response.content.split('\n')) + return list(reader) + + def name(self): + return self.__class__.__name__ + + def update_safe(self, ccy): + try: + self.print_error("getting fx quotes for", ccy) + self.quotes = self.get_rates(ccy) + self.print_error("received fx quotes") + self.on_quotes() + except Exception, e: + self.print_error("failed fx quotes:", e) + + def update(self, ccy): + t = Thread(target=self.update_safe, args=(ccy,)) + t.setDaemon(True) + t.start() + + def get_historical_rates_safe(self, ccy): + try: + self.print_error("requesting fx history for", ccy) + self.history[ccy] = self.historical_rates(ccy) + self.print_error("received fx history for", ccy) + self.on_history() + except Exception, e: + self.print_error("failed fx history:", e) + + def get_historical_rates(self, ccy): + result = self.history.get(ccy) + if not result and ccy in self.history_ccys(): + t = Thread(target=self.get_historical_rates_safe, args=(ccy,)) + t.setDaemon(True) + t.start() + return result + + def history_ccys(self): + return [] + + def historical_rate(self, ccy, d_t): + return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d')) + + +class BitcoinAverage(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('api.bitcoinaverage.com', '/ticker/global/all') + return dict([(r, Decimal(json[r]['last'])) + for r in json if r != 'timestamp']) + + def history_ccys(self): + return ['AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'EUR', 'GBP', 'IDR', 'ILS', + 'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD', + 'ZAR'] + + def historical_rates(self, ccy): + history = self.get_csv('api.bitcoinaverage.com', + "/history/%s/per_day_all_time_history.csv" % ccy) + return dict([(h['datetime'][:10], h['average']) + for h in history]) + +class BitcoinVenezuela(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('api.bitcoinvenezuela.com', '/') + rates = [(r, json['BTC'][r]) for r in json['BTC'] + if json['BTC'][r] is not None] # Giving NULL for LTC + return dict(rates) + + def protocol(self): + return "http" + + def history_ccys(self): + return ['ARS', 'EUR', 'USD', 'VEF'] + + def historical_rates(self, ccy): + return self.get_json('api.bitcoinvenezuela.com', + "/historical/index.php?coin=BTC")[ccy +'_BTC'] + +class BTCParalelo(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('btcparalelo.com', '/api/price') + return {'VEF': Decimal(json['price'])} + + def protocol(self): + return "http" + +class Bitcurex(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('pln.bitcurex.com', '/data/ticker.json') + pln_price = json['last'] + return {'PLN': Decimal(pln_price)} + +class Bitmarket(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json') + return {'PLN': Decimal(json['last'])} + +class BitPay(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('bitpay.com', '/api/rates') + return dict([(r['code'], Decimal(r['rate'])) for r in json]) + +class BitStamp(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('www.bitstamp.net', '/api/ticker/') + return {'USD': Decimal(json['last'])} + +class BlockchainInfo(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('blockchain.info', '/ticker') + return dict([(r, Decimal(json[r]['15m'])) for r in json]) + + def name(self): + return "Blockchain" + +class BTCChina(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('data.btcchina.com', '/data/ticker') + return {'CNY': Decimal(json['ticker']['last'])} + +class CaVirtEx(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('www.cavirtex.com', '/api/CAD/ticker.json') + return {'CAD': Decimal(json['last'])} + +class Coinbase(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('coinbase.com', + '/api/v1/currencies/exchange_rates') + return dict([(r[7:].upper(), Decimal(json[r])) + for r in json if r.startswith('btc_to_')]) + +class CoinDesk(ExchangeBase): + def get_rates(self, ccy): + dicts = self.get_json('api.coindesk.com', + '/v1/bpi/supported-currencies.json') + json = self.get_json('api.coindesk.com', + '/v1/bpi/currentprice/%s.json' % ccy) + ccys = [d['currency'] for d in dicts] + result = dict.fromkeys(ccys) + result[ccy] = Decimal(json['bpi'][ccy]['rate_float']) + return result + + def history_starts(self): + return { 'USD': '2012-11-30' } + + def history_ccys(self): + return self.history_starts().keys() + + def historical_rates(self, ccy): + start = self.history_starts()[ccy] + end = datetime.today().strftime('%Y-%m-%d') + # Note ?currency and ?index don't work as documented. Sigh. + query = ('/v1/bpi/historical/close.json?start=%s&end=%s' + % (start, end)) + json = self.get_json('api.coindesk.com', query) + return json['bpi'] + +class itBit(ExchangeBase): + def get_rates(self, ccy): + ccys = ['USD', 'EUR', 'SGD'] + json = self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy) + result = dict.fromkeys(ccys) + if ccy in ccys: + result[ccy] = Decimal(json['lastPrice']) + return result + +class LocalBitcoins(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('localbitcoins.com', + '/bitcoinaverage/ticker-all-currencies/') + return dict([(r, Decimal(json[r]['rates']['last'])) for r in json]) + +class Winkdex(ExchangeBase): + def get_rates(self, ccy): + json = self.get_json('winkdex.com', '/api/v0/price') + return {'USD': Decimal(json['price'] / 100.0)} + + def history_ccys(self): + return ['USD'] + + def historical_rates(self, ccy): + json = self.get_json('winkdex.com', + "/api/v0/series?start_time=1342915200") + history = json['series'][0]['results'] + return dict([(h['timestamp'][:10], h['price'] / 100.0) + for h in history]) + + +class FxPlugin(BasePlugin, ThreadJob): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.ccy = self.config_ccy() + self.history_used_spot = False + self.ccy_combo = None + self.hist_checkbox = None + is_exchange = lambda obj: (inspect.isclass(obj) + and issubclass(obj, ExchangeBase) + and obj != ExchangeBase) + self.exchanges = dict(inspect.getmembers(sys.modules[__name__], + is_exchange)) + self.set_exchange(self.config_exchange()) + + def ccy_amount_str(self, amount, commas): + prec = CCY_PRECISIONS.get(self.ccy, 2) + fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) + return fmt_str.format(round(amount, prec)) + + def thread_jobs(self): + return [self] + + def run(self): + # This runs from the network thread which catches exceptions + if self.timeout <= time.time(): + self.timeout = time.time() + 150 + self.exchange.update(self.ccy) + + def config_ccy(self): + '''Use when dynamic fetching is needed''' + return self.config.get("currency", "EUR") + + def config_exchange(self): + return self.config.get('use_exchange', 'Blockchain') + + def config_history(self): + return self.config.get('history_rates', 'unchecked') != 'unchecked' + + def show_history(self): + return self.config_history() and self.exchange.history_ccys() + + + def set_exchange(self, name): + class_ = self.exchanges.get(name) or self.exchanges.values()[0] + name = class_.__name__ + self.print_error("using exchange", name) + if self.config_exchange() != name: + self.config.set_key('use_exchange', name, True) + + self.exchange = class_(self.on_quotes, self.on_history) + # A new exchange means new fx quotes, initially empty. Force + # a quote refresh + self.timeout = 0 + self.get_historical_rates() + #self.on_fx_quotes() + + def on_quotes(self): + pass + + def on_history(self): + pass + + def exchange_rate(self): + '''Returns None, or the exchange rate as a Decimal''' + rate = self.exchange.quotes.get(self.ccy) + if rate: + return Decimal(rate) + + @hook + def format_amount_and_units(self, btc_balance): + rate = self.exchange_rate() + return '' if rate is None else " (%s %s)" % (self.value_str(btc_balance, rate), self.ccy) + + @hook + def get_fiat_status_text(self, btc_balance): + rate = self.exchange_rate() + return _(" (No FX rate available)") if rate is None else "1 BTC~%s %s" % (self.value_str(COIN, rate), self.ccy) + + def get_historical_rates(self): + if self.show_history(): + self.exchange.get_historical_rates(self.ccy) + + def requires_settings(self): + return True + + def value_str(self, satoshis, rate): + if satoshis is None: # Can happen with incomplete history + return _("Unknown") + if rate: + value = Decimal(satoshis) / COIN * Decimal(rate) + return "%s" % (self.ccy_amount_str(value, True)) + return _("No data") + + @hook + def historical_value_str(self, satoshis, d_t): + rate = self.exchange.historical_rate(self.ccy, d_t) + # Frequently there is no rate for today, until tomorrow :) + # Use spot quotes in that case + if rate is None and (datetime.today().date() - d_t.date()).days <= 2: + rate = self.exchange.quotes.get(self.ccy) + self.history_used_spot = True + return self.value_str(satoshis, rate) + + @hook + def history_tab_headers(self, headers): + if self.show_history(): + headers.extend(['%s '%self.ccy + _('Amount'), '%s '%self.ccy + _('Balance')]) + + @hook + def history_tab_update_begin(self): + self.history_used_spot = False + + @hook + def history_tab_update(self, tx, entry): + if not self.show_history(): + return + tx_hash, conf, value, timestamp, balance = tx + if conf <= 0: + date = datetime.today() + else: + date = timestamp_to_datetime(timestamp) + for amount in [value, balance]: + text = self.historical_value_str(amount, date) + entry.append(text) + + + + diff --git a/plugins/exchange_rate/kivy.py b/plugins/exchange_rate/kivy.py @@ -0,0 +1,3 @@ +from exchange_rate import FxPlugin +class Plugin(FxPlugin): + pass diff --git a/plugins/exchange_rate/qt.py b/plugins/exchange_rate/qt.py @@ -0,0 +1,184 @@ +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from electrum_gui.qt.util import * +from electrum_gui.qt.amountedit import AmountEdit + + +from electrum.bitcoin import COIN +from electrum.i18n import _ +from decimal import Decimal +from functools import partial +from electrum.plugins import hook +from exchange_rate import FxPlugin + +class Plugin(FxPlugin): + + def connect_fields(self, window, btc_e, fiat_e, fee_e): + + def edit_changed(edit): + edit.setStyleSheet(BLACK_FG) + fiat_e.is_last_edited = (edit == fiat_e) + amount = edit.get_amount() + rate = self.exchange_rate() + if rate is None or amount is None: + if edit is fiat_e: + btc_e.setText("") + if fee_e: + fee_e.setText("") + else: + fiat_e.setText("") + else: + if edit is fiat_e: + btc_e.setAmount(int(amount / Decimal(rate) * COIN)) + if fee_e: window.update_fee() + btc_e.setStyleSheet(BLUE_FG) + else: + fiat_e.setText(self.ccy_amount_str( + amount * Decimal(rate) / COIN, False)) + fiat_e.setStyleSheet(BLUE_FG) + + fiat_e.textEdited.connect(partial(edit_changed, fiat_e)) + btc_e.textEdited.connect(partial(edit_changed, btc_e)) + fiat_e.is_last_edited = False + + @hook + def init_qt(self, gui): + self.app = gui.app + + @hook + def do_clear(self, window): + window.fiat_send_e.setText('') + + def close(self): + # Get rid of hooks before updating status bars. + FxPlugin.close(self) + self.app.emit(SIGNAL('close_fx_plugin')) + + def restore_window(self, window): + window.update_status() + window.history_list.refresh_headers() + window.fiat_send_e.hide() + window.fiat_receive_e.hide() + + def on_quotes(self): + self.app.emit(SIGNAL('new_fx_quotes')) + + def on_history(self): + self.app.emit(SIGNAL('new_fx_history')) + + def on_fx_history(self, window): + '''Called when historical fx quotes are updated''' + window.history_list.update() + + def on_fx_quotes(self, window): + '''Called when fresh spot fx quotes come in''' + window.update_status() + self.populate_ccy_combo() + # Refresh edits with the new rate + edit = window.fiat_send_e if window.fiat_send_e.is_last_edited else window.amount_e + edit.textEdited.emit(edit.text()) + edit = window.fiat_receive_e if window.fiat_receive_e.is_last_edited else window.receive_amount_e + edit.textEdited.emit(edit.text()) + # History tab needs updating if it used spot + if self.history_used_spot: + self.on_fx_history(window) + + def on_ccy_combo_change(self): + '''Called when the chosen currency changes''' + ccy = str(self.ccy_combo.currentText()) + if ccy and ccy != self.ccy: + self.ccy = ccy + self.config.set_key('currency', ccy, True) + self.app.emit(SIGNAL('new_fx_quotes')) + self.get_historical_rates() # Because self.ccy changes + self.hist_checkbox_update() + + def hist_checkbox_update(self): + if self.hist_checkbox: + self.hist_checkbox.setEnabled(self.ccy in self.exchange.history_ccys()) + self.hist_checkbox.setChecked(self.config_history()) + + def populate_ccy_combo(self): + # There should be at most one instance of the settings dialog + combo = self.ccy_combo + # NOTE: bool(combo) is False if it is empty. Nuts. + if combo is not None: + combo.blockSignals(True) + combo.clear() + combo.addItems(sorted(self.exchange.quotes.keys())) + combo.blockSignals(False) + combo.setCurrentIndex(combo.findText(self.ccy)) + + @hook + def on_new_window(self, window): + # Additional send and receive edit boxes + send_e = AmountEdit(self.config_ccy) + window.send_grid.addWidget(send_e, 4, 2, Qt.AlignLeft) + window.amount_e.frozen.connect( + lambda: send_e.setFrozen(window.amount_e.isReadOnly())) + receive_e = AmountEdit(self.config_ccy) + window.receive_grid.addWidget(receive_e, 2, 2, Qt.AlignLeft) + window.fiat_send_e = send_e + window.fiat_receive_e = receive_e + self.connect_fields(window, window.amount_e, send_e, window.fee_e) + self.connect_fields(window, window.receive_amount_e, receive_e, None) + window.history_list.refresh_headers() + window.update_status() + window.connect(window.app, SIGNAL('new_fx_quotes'), lambda: self.on_fx_quotes(window)) + window.connect(window.app, SIGNAL('new_fx_history'), lambda: self.on_fx_history(window)) + window.connect(window.app, SIGNAL('close_fx_plugin'), lambda: self.restore_window(window)) + window.connect(window.app, SIGNAL('refresh_headers'), window.history_list.refresh_headers) + + def settings_widget(self, window): + return EnterButton(_('Settings'), self.settings_dialog) + + def settings_dialog(self): + d = QDialog() + d.setWindowTitle("Settings") + layout = QGridLayout(d) + layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0) + layout.addWidget(QLabel(_('Currency: ')), 1, 0) + layout.addWidget(QLabel(_('History Rates: ')), 2, 0) + + # Currency list + self.ccy_combo = QComboBox() + self.ccy_combo.currentIndexChanged.connect(self.on_ccy_combo_change) + self.populate_ccy_combo() + + def on_change_ex(idx): + exchange = str(combo_ex.currentText()) + if exchange != self.exchange.name(): + self.set_exchange(exchange) + self.hist_checkbox_update() + + def on_change_hist(checked): + if checked: + self.config.set_key('history_rates', 'checked') + self.get_historical_rates() + else: + self.config.set_key('history_rates', 'unchecked') + self.app.emit(SIGNAL('refresh_headers')) + + def ok_clicked(): + self.timeout = 0 + self.ccy_combo = None + d.accept() + + combo_ex = QComboBox() + combo_ex.addItems(sorted(self.exchanges.keys())) + combo_ex.setCurrentIndex(combo_ex.findText(self.config_exchange())) + combo_ex.currentIndexChanged.connect(on_change_ex) + + self.hist_checkbox = QCheckBox() + self.hist_checkbox.stateChanged.connect(on_change_hist) + self.hist_checkbox_update() + + ok_button = QPushButton(_("OK")) + ok_button.clicked.connect(lambda: ok_clicked()) + + layout.addWidget(self.ccy_combo,1,1) + layout.addWidget(combo_ex,0,1) + layout.addWidget(self.hist_checkbox,2,1) + layout.addWidget(ok_button,3,1) + + return d.exec_() diff --git a/plugins/greenaddress_instant.py b/plugins/greenaddress_instant.py @@ -1,97 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - lightweight Bitcoin client -# Copyright (C) 2014 Thomas Voegtlin -# -# 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 base64 -import urllib -import sys -import requests - -from PyQt4.QtGui import QMessageBox, QApplication, QPushButton - -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ - - - -class QtPlugin(BasePlugin): - - button_label = _("Verify GA instant") - - @hook - def transaction_dialog(self, d): - d.verify_button = QPushButton(self.button_label) - d.verify_button.clicked.connect(lambda: self.do_verify(d)) - d.buttons.insert(0, d.verify_button) - self.transaction_dialog_update(d) - - def get_my_addr(self, d): - """Returns the address for given tx which can be used to request - instant confirmation verification from GreenAddress""" - for addr, _ in d.tx.get_outputs(): - if d.wallet.is_mine(addr): - return addr - return None - - @hook - def transaction_dialog_update(self, d): - if d.tx.is_complete() and self.get_my_addr(d): - d.verify_button.show() - else: - d.verify_button.hide() - - def do_verify(self, d): - tx = d.tx - wallet = d.wallet - window = d.parent - # 1. get the password and sign the verification request - password = None - if wallet.use_encryption: - msg = _('GreenAddress requires your signature \n' - 'to verify that transaction is instant.\n' - 'Please enter your password to sign a\n' - 'verification request.') - password = window.password_dialog(msg) - if not password: - return - try: - d.verify_button.setText(_('Verifying...')) - QApplication.processEvents() # update the button label - - addr = self.get_my_addr(d) - message = "Please verify if %s is GreenAddress instant confirmed" % tx.hash() - sig = wallet.sign_message(addr, message, password) - sig = base64.b64encode(sig) - - # 2. send the request - response = requests.request("GET", ("https://greenaddress.it/verify/?signature=%s&txhash=%s" % (urllib.quote(sig), tx.hash())), - headers = {'User-Agent': 'Electrum'}) - response = response.json() - - # 3. display the result - if response.get('verified'): - QMessageBox.information(None, _('Verification successful!'), - _('%s is covered by GreenAddress instant confirmation') % (tx.hash()), _('OK')) - else: - QMessageBox.critical(None, _('Verification failed!'), - _('%s is not covered by GreenAddress instant confirmation') % (tx.hash()), _('OK')) - except BaseException as e: - import traceback - traceback.print_exc(file=sys.stdout) - QMessageBox.information(None, _('Error'), str(e), _('OK')) - finally: - d.verify_button.setText(self.button_label) diff --git a/plugins/greenaddress_instant/__init__.py b/plugins/greenaddress_instant/__init__.py @@ -0,0 +1,5 @@ +from electrum.i18n import _ + +fullname = 'GreenAddress instant' +description = _("Allows validating if your transactions have instant confirmations by GreenAddress") +available_for = ['qt'] diff --git a/plugins/greenaddress_instant/qt.py b/plugins/greenaddress_instant/qt.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2014 Thomas Voegtlin +# +# 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 base64 +import urllib +import sys +import requests + +from PyQt4.QtGui import QMessageBox, QApplication, QPushButton + +from electrum.plugins import BasePlugin, hook +from electrum.i18n import _ + + + +class Plugin(BasePlugin): + + button_label = _("Verify GA instant") + + @hook + def transaction_dialog(self, d): + d.verify_button = QPushButton(self.button_label) + d.verify_button.clicked.connect(lambda: self.do_verify(d)) + d.buttons.insert(0, d.verify_button) + self.transaction_dialog_update(d) + + def get_my_addr(self, d): + """Returns the address for given tx which can be used to request + instant confirmation verification from GreenAddress""" + for addr, _ in d.tx.get_outputs(): + if d.wallet.is_mine(addr): + return addr + return None + + @hook + def transaction_dialog_update(self, d): + if d.tx.is_complete() and self.get_my_addr(d): + d.verify_button.show() + else: + d.verify_button.hide() + + def do_verify(self, d): + tx = d.tx + wallet = d.wallet + window = d.parent + # 1. get the password and sign the verification request + password = None + if wallet.use_encryption: + msg = _('GreenAddress requires your signature \n' + 'to verify that transaction is instant.\n' + 'Please enter your password to sign a\n' + 'verification request.') + password = window.password_dialog(msg) + if not password: + return + try: + d.verify_button.setText(_('Verifying...')) + QApplication.processEvents() # update the button label + + addr = self.get_my_addr(d) + message = "Please verify if %s is GreenAddress instant confirmed" % tx.hash() + sig = wallet.sign_message(addr, message, password) + sig = base64.b64encode(sig) + + # 2. send the request + response = requests.request("GET", ("https://greenaddress.it/verify/?signature=%s&txhash=%s" % (urllib.quote(sig), tx.hash())), + headers = {'User-Agent': 'Electrum'}) + response = response.json() + + # 3. display the result + if response.get('verified'): + QMessageBox.information(None, _('Verification successful!'), + _('%s is covered by GreenAddress instant confirmation') % (tx.hash()), _('OK')) + else: + QMessageBox.critical(None, _('Verification failed!'), + _('%s is not covered by GreenAddress instant confirmation') % (tx.hash()), _('OK')) + except BaseException as e: + import traceback + traceback.print_exc(file=sys.stdout) + QMessageBox.information(None, _('Error'), str(e), _('OK')) + finally: + d.verify_button.setText(self.button_label) diff --git a/plugins/keepkey.py b/plugins/keepkey.py @@ -1,691 +0,0 @@ -from binascii import unhexlify -from struct import pack -from sys import stderr -from time import sleep -import unicodedata -import threading -import re -from functools import partial - - -import electrum -from electrum import bitcoin - -from electrum.account import BIP32_Account -from electrum.bitcoin import EncodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160, xpub_from_pubkey -from electrum.i18n import _ -from electrum.plugins import BasePlugin, hook, always_hook, run_hook -from electrum.transaction import Transaction, deserialize, is_extended_pubkey, x_to_xpub -from electrum.wallet import BIP32_HD_Wallet -from electrum.util import print_error, print_msg -from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root - - -try: - from keepkeylib.client import types - from keepkeylib.client import proto, BaseClient, ProtocolMixin - from keepkeylib.transport import ConnectionError - from keepkeylib.transport_hid import HidTransport - KEEPKEY = True -except ImportError: - KEEPKEY = False - -import keepkeylib.ckd_public as ckd_public - -def log(msg): - stderr.write("%s\n" % msg) - stderr.flush() - -def give_error(message): - print_error(message) - raise Exception(message) - - - - -class KeepKeyWallet(BIP32_HD_Wallet): - wallet_type = 'keepkey' - root_derivation = "m/44'/0'" - - def __init__(self, storage): - BIP32_HD_Wallet.__init__(self, storage) - self.mpk = None - self.device_checked = False - self.proper_device = False - self.force_watching_only = False - - def get_action(self): - if not self.accounts: - return 'create_accounts' - - def can_import(self): - return False - - def can_sign_xpubkey(self, x_pubkey): - xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) - return xpub in self.master_public_keys.values() - - def can_export(self): - return False - - def can_create_accounts(self): - return True - - def can_change_password(self): - return False - - def is_watching_only(self): - return self.force_watching_only - - def get_client(self): - return self.plugin.get_client() - - def address_id(self, address): - account_id, (change, address_index) = self.get_address_index(address) - return "44'/0'/%s'/%d/%d" % (account_id, change, address_index) - - def create_main_account(self, password): - self.create_account('Main account', None) #name, empty password - - def mnemonic_to_seed(self, mnemonic, passphrase): - # keepkey uses bip39 - import pbkdf2, hashlib, hmac - PBKDF2_ROUNDS = 2048 - mnemonic = unicodedata.normalize('NFKD', ' '.join(mnemonic.split())) - passphrase = unicodedata.normalize('NFKD', passphrase) - return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) - - def derive_xkeys(self, root, derivation, password): - x = self.master_private_keys.get(root) - if x: - root_xprv = pw_decode(x, password) - xprv, xpub = bip32_private_derivation(root_xprv, root, derivation) - return xpub, xprv - else: - derivation = derivation.replace(self.root_name,"44'/0'/") - xpub = self.get_public_key(derivation) - return xpub, None - - def get_public_key(self, bip32_path): - address_n = self.plugin.get_client().expand_path(bip32_path) - node = self.plugin.get_client().get_public_node(address_n).node - xpub = "0488B21E".decode('hex') + chr(node.depth) + self.i4b(node.fingerprint) + self.i4b(node.child_num) + node.chain_code + node.public_key - return EncodeBase58Check(xpub) - - def get_master_public_key(self): - if not self.mpk: - self.mpk = self.get_public_key("44'/0'") - return self.mpk - - def i4b(self, x): - return pack('>I', x) - - def add_keypairs(self, tx, keypairs, password): - #do nothing - no priv keys available - pass - - def decrypt_message(self, pubkey, message, password): - raise BaseException( _('Decrypt method is not implemented in KeepKey') ) - #address = public_key_to_bc_address(pubkey.decode('hex')) - #address_path = self.address_id(address) - #address_n = self.get_client().expand_path(address_path) - #try: - # decrypted_msg = self.get_client().decrypt_message(address_n, b64decode(message)) - #except Exception, e: - # give_error(e) - #finally: - # twd.stop() - #return str(decrypted_msg) - - def sign_message(self, address, message, password): - if self.has_seed(): - return BIP32_HD_Wallet.sign_message(self, address, message, password) - if not self.check_proper_device(): - give_error('Wrong device or password') - try: - address_path = self.address_id(address) - address_n = self.plugin.get_client().expand_path(address_path) - except Exception, e: - give_error(e) - try: - msg_sig = self.plugin.get_client().sign_message('Bitcoin', address_n, message) - except Exception, e: - give_error(e) - finally: - self.plugin.handler.stop() - return msg_sig.signature - - def sign_transaction(self, tx, password): - if self.has_seed(): - return BIP32_HD_Wallet.sign_transaction(self, tx, password) - if tx.is_complete(): - return - if not self.check_proper_device(): - give_error('Wrong device or password') - # previous transactions used as inputs - prev_tx = {} - # path of the xpubs that are involved - xpub_path = {} - for txin in tx.inputs: - tx_hash = txin['prevout_hash'] - - ptx = self.transactions.get(tx_hash) - if ptx is None: - ptx = self.network.synchronous_get(('blockchain.transaction.get', [tx_hash])) - ptx = Transaction(ptx) - prev_tx[tx_hash] = ptx - - for x_pubkey in txin['x_pubkeys']: - account_derivation = None - if not is_extended_pubkey(x_pubkey): - continue - xpub = x_to_xpub(x_pubkey) - for k, v in self.master_public_keys.items(): - if v == xpub: - account_id = re.match("x/(\d+)'", k).group(1) - account_derivation = "44'/0'/%s'"%account_id - if account_derivation is not None: - xpub_path[xpub] = account_derivation - - self.plugin.sign_transaction(tx, prev_tx, xpub_path) - - def check_proper_device(self): - self.get_client().ping('t') - if not self.device_checked: - address = self.addresses(False)[0] - address_id = self.address_id(address) - n = self.get_client().expand_path(address_id) - device_address = self.get_client().get_address('Bitcoin', n) - self.device_checked = True - - if device_address != address: - self.proper_device = False - else: - self.proper_device = True - - return self.proper_device - - - -class Plugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self._is_available = self._init() - self.wallet = None - self.handler = None - self.client = None - self.transport = None - - def constructor(self, s): - return KeepKeyWallet(s) - - def _init(self): - return KEEPKEY - - def is_available(self): - if not self._is_available: - return False - if not self.wallet: - return False - if self.wallet.storage.get('wallet_type') != 'keepkey': - return False - return True - - def set_enabled(self, enabled): - self.wallet.storage.put('use_' + self.name, enabled) - - def is_enabled(self): - if not self.is_available(): - return False - if self.wallet.has_seed(): - return False - return True - - def compare_version(self, major, minor=0, patch=0): - features = self.get_client().features - v = [features.major_version, features.minor_version, features.patch_version] - self.print_error('firmware version', v) - return cmp(v, [major, minor, patch]) - - def atleast_version(self, major, minor=0, patch=0): - return self.compare_version(major, minor, patch) >= 0 - - def get_client(self): - if not KEEPKEY: - give_error('please install github.com/keepkey/python-keepkey') - - if not self.client or self.client.bad: - d = HidTransport.enumerate() - if not d: - give_error('Could not connect to your KeepKey. Please verify the cable is connected and that no other app is using it.') - self.transport = HidTransport(d[0]) - self.client = QtGuiKeepKeyClient(self.transport) - self.client.handler = self.handler - self.client.set_tx_api(self) - self.client.bad = False - if not self.atleast_version(1, 0, 0): - self.client = None - give_error('Outdated KeepKey firmware. Please update the firmware from https://www.keepkey.com') - return self.client - - @hook - def close_wallet(self): - print_error("keepkey: clear session") - if self.client: - self.client.clear_session() - self.client.transport.close() - self.client = None - self.wallet = None - - - def show_address(self, address): - if not self.wallet.check_proper_device(): - give_error('Wrong device or password') - try: - address_path = self.wallet.address_id(address) - address_n = self.get_client().expand_path(address_path) - except Exception, e: - give_error(e) - try: - self.get_client().get_address('Bitcoin', address_n, True) - except Exception, e: - give_error(e) - finally: - self.handler.stop() - - - def sign_transaction(self, tx, prev_tx, xpub_path): - self.prev_tx = prev_tx - self.xpub_path = xpub_path - client = self.get_client() - inputs = self.tx_inputs(tx, True) - outputs = self.tx_outputs(tx) - try: - signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] - except Exception, e: - self.handler.stop() - give_error(e) - - self.handler.stop() - - raw = signed_tx.encode('hex') - tx.update_signatures(raw) - - - def tx_inputs(self, tx, for_sig=False): - inputs = [] - for txin in tx.inputs: - txinputtype = types.TxInputType() - if txin.get('is_coinbase'): - prev_hash = "\0"*32 - prev_index = 0xffffffff # signed int -1 - else: - if for_sig: - x_pubkeys = txin['x_pubkeys'] - if len(x_pubkeys) == 1: - x_pubkey = x_pubkeys[0] - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - else: - def f(x_pubkey): - if is_extended_pubkey(x_pubkey): - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - else: - xpub = xpub_from_pubkey(x_pubkey.decode('hex')) - s = [] - node = ckd_public.deserialize(xpub) - return types.HDNodePathType(node=node, address_n=s) - pubkeys = map(f, x_pubkeys) - multisig = types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), - m=txin.get('num_sig'), - ) - txinputtype = types.TxInputType( - script_type=types.SPENDMULTISIG, - multisig= multisig - ) - # find which key is mine - for x_pubkey in x_pubkeys: - if is_extended_pubkey(x_pubkey): - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - if xpub in self.xpub_path: - xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - break - - prev_hash = unhexlify(txin['prevout_hash']) - prev_index = txin['prevout_n'] - - txinputtype.prev_hash = prev_hash - txinputtype.prev_index = prev_index - - if 'scriptSig' in txin: - script_sig = txin['scriptSig'].decode('hex') - txinputtype.script_sig = script_sig - - if 'sequence' in txin: - sequence = txin['sequence'] - txinputtype.sequence = sequence - - inputs.append(txinputtype) - - return inputs - - def tx_outputs(self, tx): - outputs = [] - - for type, address, amount in tx.outputs: - assert type == 'address' - txoutputtype = types.TxOutputType() - if self.wallet.is_change(address): - address_path = self.wallet.address_id(address) - address_n = self.get_client().expand_path(address_path) - txoutputtype.address_n.extend(address_n) - else: - txoutputtype.address = address - txoutputtype.amount = amount - addrtype, hash_160 = bc_address_to_hash_160(address) - if addrtype == 0: - txoutputtype.script_type = types.PAYTOADDRESS - elif addrtype == 5: - txoutputtype.script_type = types.PAYTOSCRIPTHASH - else: - raise BaseException('addrtype') - outputs.append(txoutputtype) - - return outputs - - def electrum_tx_to_txtype(self, tx): - t = types.TransactionType() - d = deserialize(tx.raw) - t.version = d['version'] - t.lock_time = d['lockTime'] - inputs = self.tx_inputs(tx) - t.inputs.extend(inputs) - for vout in d['outputs']: - o = t.bin_outputs.add() - o.amount = vout['value'] - o.script_pubkey = vout['scriptPubKey'].decode('hex') - return t - - def get_tx(self, tx_hash): - tx = self.prev_tx[tx_hash] - tx.deserialize() - return self.electrum_tx_to_txtype(tx) - - - - -from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton -import PyQt4.QtCore as QtCore -from electrum_gui.qt.util import * -from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow -from electrum_gui.qt.installwizard import InstallWizard -from keepkeylib.qt.pinmatrix import PinMatrixWidget - - -class QtPlugin(Plugin): - - @hook - def load_wallet(self, wallet, window): - self.print_error("load_wallet") - self.wallet = wallet - self.wallet.plugin = self - self.keepkey_button = StatusBarButton(QIcon(":icons/keepkey.png"), _("KeepKey"), partial(self.settings_dialog, window)) - if type(window) is ElectrumWindow: - window.statusBar().addPermanentWidget(self.keepkey_button) - if self.handler is None: - self.handler = KeepKeyQtHandler(window) - try: - self.get_client().ping('t') - except BaseException as e: - QMessageBox.information(window, _('Error'), _("KeepKey device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) - self.wallet.force_watching_only = True - return - if self.wallet.addresses() and not self.wallet.check_proper_device(): - QMessageBox.information(window, _('Error'), _("This wallet does not match your KeepKey device"), _('OK')) - self.wallet.force_watching_only = True - - @hook - def installwizard_load_wallet(self, wallet, window): - if type(wallet) != KeepKeyWallet: - return - self.load_wallet(wallet, window) - - @hook - def installwizard_restore(self, wizard, storage): - if storage.get('wallet_type') != 'keepkey': - return - seed = wizard.enter_seed_dialog("Enter your KeepKey seed", None, func=lambda x:True) - if not seed: - return - wallet = KeepKeyWallet(storage) - self.wallet = wallet - handler = KeepKeyQtHandler(wizard) - passphrase = handler.get_passphrase(_("Please enter your KeepKey passphrase.") + '\n' + _("Press OK if you do not use one.")) - if passphrase is None: - return - password = wizard.password_dialog() - wallet.add_seed(seed, password) - wallet.add_cosigner_seed(seed, 'x/', password, passphrase) - wallet.create_main_account(password) - # disable keepkey plugin - self.set_enabled(False) - return wallet - - @hook - def receive_menu(self, menu, addrs): - if not self.wallet.is_watching_only() and self.atleast_version(1, 3) and len(addrs) == 1: - menu.addAction(_("Show on TREZOR"), lambda: self.show_address(addrs[0])) - - def settings_dialog(self, window): - try: - device_id = self.get_client().get_device_id() - except BaseException as e: - window.show_message(str(e)) - return - get_label = lambda: self.get_client().features.label - update_label = lambda: current_label_label.setText("Label: %s" % get_label()) - d = QDialog() - layout = QGridLayout(d) - layout.addWidget(QLabel("KeepKey Options"),0,0) - layout.addWidget(QLabel("ID:"),1,0) - layout.addWidget(QLabel(" %s" % device_id),1,1) - - def modify_label(): - response = QInputDialog().getText(None, "Set New KeepKey Label", "New KeepKey Label: (upon submission confirm on KeepKey)") - if not response[1]: - return - new_label = str(response[0]) - self.handler.show_message("Please confirm label change on KeepKey") - status = self.get_client().apply_settings(label=new_label) - self.handler.stop() - update_label() - - current_label_label = QLabel() - update_label() - change_label_button = QPushButton("Modify") - change_label_button.clicked.connect(modify_label) - layout.addWidget(current_label_label,3,0) - layout.addWidget(change_label_button,3,1) - d.exec_() - - -class CmdlinePlugin(Plugin): - - @hook - def cmdline_load_wallet(self, wallet): - self.wallet = wallet - self.wallet.plugin = self - if self.handler is None: - self.handler = KeepKeyCmdLineHandler() - - - -class KeepKeyGuiMixin(object): - - def __init__(self, *args, **kwargs): - super(KeepKeyGuiMixin, self).__init__(*args, **kwargs) - - def callback_ButtonRequest(self, msg): - if msg.code == 3: - message = "Confirm transaction outputs on KeepKey device to continue" - elif msg.code == 8: - message = "Confirm transaction fee on KeepKey device to continue" - elif msg.code == 7: - message = "Confirm message to sign on KeepKey device to continue" - elif msg.code == 10: - message = "Confirm address on KeepKey device to continue" - else: - message = "Check KeepKey device to continue" - self.handler.show_message(msg.code, message, self) - return proto.ButtonAck() - - def callback_PinMatrixRequest(self, msg): - if msg.type == 1: - desc = 'current PIN' - elif msg.type == 2: - desc = 'new PIN' - elif msg.type == 3: - desc = 'new PIN again' - else: - desc = 'PIN' - pin = self.handler.get_pin("Please enter KeepKey %s" % desc) - if not pin: - return proto.Cancel() - return proto.PinMatrixAck(pin=pin) - - def callback_PassphraseRequest(self, req): - msg = _("Please enter your KeepKey passphrase.") - passphrase = self.handler.get_passphrase(msg) - if passphrase is None: - return proto.Cancel() - return proto.PassphraseAck(passphrase=passphrase) - - def callback_WordRequest(self, msg): - #TODO - log("Enter one word of mnemonic: ") - word = raw_input() - return proto.WordAck(word=word) - - -class KeepKeyCmdLineHandler: - - def get_passphrase(self, msg): - import getpass - print_msg(msg) - return getpass.getpass('') - - def get_pin(self, msg): - t = { 'a':'7', 'b':'8', 'c':'9', 'd':'4', 'e':'5', 'f':'6', 'g':'1', 'h':'2', 'i':'3'} - print_msg(msg) - print_msg("a b c\nd e f\ng h i\n-----") - o = raw_input() - return ''.join(map(lambda x: t[x], o)) - - def stop(self): - pass - - def show_message(self, msg): - print_msg(msg) - - -class KeepKeyQtHandler: - - def __init__(self, win): - self.win = win - self.win.connect(win, SIGNAL('keepkey_done'), self.dialog_stop) - self.win.connect(win, SIGNAL('message_dialog'), self.message_dialog) - self.win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog) - self.win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog) - self.done = threading.Event() - - def stop(self): - self.win.emit(SIGNAL('keepkey_done')) - - def show_message(self, msg_code, msg, client): - self.messsage_code = msg_code - self.message = msg - self.client = client - self.win.emit(SIGNAL('message_dialog')) - - def get_pin(self, msg): - self.done.clear() - self.message = msg - self.win.emit(SIGNAL('pin_dialog')) - self.done.wait() - return self.response - - def get_passphrase(self, msg): - self.done.clear() - self.message = msg - self.win.emit(SIGNAL('passphrase_dialog')) - self.done.wait() - return self.passphrase - - def pin_dialog(self): - d = QDialog(None) - d.setModal(1) - d.setWindowTitle(_("Enter PIN")) - d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) - matrix = PinMatrixWidget() - vbox = QVBoxLayout() - vbox.addWidget(QLabel(self.message)) - vbox.addWidget(matrix) - vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) - d.setLayout(vbox) - if not d.exec_(): - self.response = None - self.response = str(matrix.get_value()) - self.done.set() - - def passphrase_dialog(self): - if type(self.win) is ElectrumWindow: - passphrase = self.win.password_dialog(_("Please enter your KeepKey passphrase")) - self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' - else: - assert type(self.win) is InstallWizard - from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog - d = QDialog() - d.setModal(1) - d.setLayout(make_password_dialog(d, None, self.message, False)) - confirmed, p, passphrase = run_password_dialog(d, None, None) - if not confirmed: - QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) - self.passphrase = None - else: - self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' - self.done.set() - - def message_dialog(self): - self.d = QDialog() - self.d.setModal(1) - self.d.setWindowTitle('Please Check KeepKey Device') - self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) - l = QLabel(self.message) - vbox = QVBoxLayout(self.d) - vbox.addWidget(l) - - if self.messsage_code in (3, 8): - vbox.addLayout(Buttons(CancelButton(self.d))) - self.d.connect(self.d, SIGNAL('rejected()'), self.client.cancel) - - self.d.show() - - def dialog_stop(self): - self.d.hide() - - -if KEEPKEY: - class QtGuiKeepKeyClient(ProtocolMixin, KeepKeyGuiMixin, BaseClient): - def call_raw(self, msg): - try: - resp = BaseClient.call_raw(self, msg) - except ConnectionError: - self.bad = True - raise - - return resp diff --git a/plugins/keepkey/__init__.py b/plugins/keepkey/__init__.py @@ -0,0 +1,8 @@ +from electrum.i18n import _ + +fullname = 'KeepKey' +description = _('Provides support for KeepKey hardware wallet') +requires = [('keepkeylib','github.com/keepkey/python-keepkey')] +requires_wallet_type = ['keepkey'] +registers_wallet_type = ('hardware', 'keepkey', _("KeepKey wallet")) +available_for = ['qt', 'cmdline'] diff --git a/plugins/keepkey/keepkey.py b/plugins/keepkey/keepkey.py @@ -0,0 +1,597 @@ +from binascii import unhexlify +from struct import pack +from sys import stderr +from time import sleep +import unicodedata +import threading +import re +from functools import partial + + +import electrum +from electrum import bitcoin + +from electrum.account import BIP32_Account +from electrum.bitcoin import EncodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160, xpub_from_pubkey +from electrum.i18n import _ +from electrum.plugins import BasePlugin, hook, always_hook, run_hook +from electrum.transaction import Transaction, deserialize, is_extended_pubkey, x_to_xpub +from electrum.wallet import BIP32_HD_Wallet +from electrum.util import print_error, print_msg +from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root + + +try: + from keepkeylib.client import types + from keepkeylib.client import proto, BaseClient, ProtocolMixin + from keepkeylib.transport import ConnectionError + from keepkeylib.transport_hid import HidTransport + KEEPKEY = True +except ImportError: + KEEPKEY = False + +import keepkeylib.ckd_public as ckd_public + +def log(msg): + stderr.write("%s\n" % msg) + stderr.flush() + +def give_error(message): + print_error(message) + raise Exception(message) + + + + +class KeepKeyWallet(BIP32_HD_Wallet): + wallet_type = 'keepkey' + root_derivation = "m/44'/0'" + + def __init__(self, storage): + BIP32_HD_Wallet.__init__(self, storage) + self.mpk = None + self.device_checked = False + self.proper_device = False + self.force_watching_only = False + + def get_action(self): + if not self.accounts: + return 'create_accounts' + + def can_import(self): + return False + + def can_sign_xpubkey(self, x_pubkey): + xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) + return xpub in self.master_public_keys.values() + + def can_export(self): + return False + + def can_create_accounts(self): + return True + + def can_change_password(self): + return False + + def is_watching_only(self): + return self.force_watching_only + + def get_client(self): + return self.plugin.get_client() + + def address_id(self, address): + account_id, (change, address_index) = self.get_address_index(address) + return "44'/0'/%s'/%d/%d" % (account_id, change, address_index) + + def create_main_account(self, password): + self.create_account('Main account', None) #name, empty password + + def mnemonic_to_seed(self, mnemonic, passphrase): + # keepkey uses bip39 + import pbkdf2, hashlib, hmac + PBKDF2_ROUNDS = 2048 + mnemonic = unicodedata.normalize('NFKD', ' '.join(mnemonic.split())) + passphrase = unicodedata.normalize('NFKD', passphrase) + return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) + + def derive_xkeys(self, root, derivation, password): + x = self.master_private_keys.get(root) + if x: + root_xprv = pw_decode(x, password) + xprv, xpub = bip32_private_derivation(root_xprv, root, derivation) + return xpub, xprv + else: + derivation = derivation.replace(self.root_name,"44'/0'/") + xpub = self.get_public_key(derivation) + return xpub, None + + def get_public_key(self, bip32_path): + address_n = self.plugin.get_client().expand_path(bip32_path) + node = self.plugin.get_client().get_public_node(address_n).node + xpub = "0488B21E".decode('hex') + chr(node.depth) + self.i4b(node.fingerprint) + self.i4b(node.child_num) + node.chain_code + node.public_key + return EncodeBase58Check(xpub) + + def get_master_public_key(self): + if not self.mpk: + self.mpk = self.get_public_key("44'/0'") + return self.mpk + + def i4b(self, x): + return pack('>I', x) + + def add_keypairs(self, tx, keypairs, password): + #do nothing - no priv keys available + pass + + def decrypt_message(self, pubkey, message, password): + raise BaseException( _('Decrypt method is not implemented in KeepKey') ) + #address = public_key_to_bc_address(pubkey.decode('hex')) + #address_path = self.address_id(address) + #address_n = self.get_client().expand_path(address_path) + #try: + # decrypted_msg = self.get_client().decrypt_message(address_n, b64decode(message)) + #except Exception, e: + # give_error(e) + #finally: + # twd.stop() + #return str(decrypted_msg) + + def sign_message(self, address, message, password): + if self.has_seed(): + return BIP32_HD_Wallet.sign_message(self, address, message, password) + if not self.check_proper_device(): + give_error('Wrong device or password') + try: + address_path = self.address_id(address) + address_n = self.plugin.get_client().expand_path(address_path) + except Exception, e: + give_error(e) + try: + msg_sig = self.plugin.get_client().sign_message('Bitcoin', address_n, message) + except Exception, e: + give_error(e) + finally: + self.plugin.handler.stop() + return msg_sig.signature + + def sign_transaction(self, tx, password): + if self.has_seed(): + return BIP32_HD_Wallet.sign_transaction(self, tx, password) + if tx.is_complete(): + return + if not self.check_proper_device(): + give_error('Wrong device or password') + # previous transactions used as inputs + prev_tx = {} + # path of the xpubs that are involved + xpub_path = {} + for txin in tx.inputs: + tx_hash = txin['prevout_hash'] + + ptx = self.transactions.get(tx_hash) + if ptx is None: + ptx = self.network.synchronous_get(('blockchain.transaction.get', [tx_hash])) + ptx = Transaction(ptx) + prev_tx[tx_hash] = ptx + + for x_pubkey in txin['x_pubkeys']: + account_derivation = None + if not is_extended_pubkey(x_pubkey): + continue + xpub = x_to_xpub(x_pubkey) + for k, v in self.master_public_keys.items(): + if v == xpub: + account_id = re.match("x/(\d+)'", k).group(1) + account_derivation = "44'/0'/%s'"%account_id + if account_derivation is not None: + xpub_path[xpub] = account_derivation + + self.plugin.sign_transaction(tx, prev_tx, xpub_path) + + def check_proper_device(self): + self.get_client().ping('t') + if not self.device_checked: + address = self.addresses(False)[0] + address_id = self.address_id(address) + n = self.get_client().expand_path(address_id) + device_address = self.get_client().get_address('Bitcoin', n) + self.device_checked = True + + if device_address != address: + self.proper_device = False + else: + self.proper_device = True + + return self.proper_device + + + +class KeepKeyPlugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self._is_available = self._init() + self.wallet = None + self.handler = None + self.client = None + self.transport = None + + def constructor(self, s): + return KeepKeyWallet(s) + + def _init(self): + return KEEPKEY + + def is_available(self): + if not self._is_available: + return False + if not self.wallet: + return False + if self.wallet.storage.get('wallet_type') != 'keepkey': + return False + return True + + def set_enabled(self, enabled): + self.wallet.storage.put('use_' + self.name, enabled) + + def is_enabled(self): + if not self.is_available(): + return False + if self.wallet.has_seed(): + return False + return True + + def compare_version(self, major, minor=0, patch=0): + features = self.get_client().features + v = [features.major_version, features.minor_version, features.patch_version] + self.print_error('firmware version', v) + return cmp(v, [major, minor, patch]) + + def atleast_version(self, major, minor=0, patch=0): + return self.compare_version(major, minor, patch) >= 0 + + def get_client(self): + if not KEEPKEY: + give_error('please install github.com/keepkey/python-keepkey') + + if not self.client or self.client.bad: + d = HidTransport.enumerate() + if not d: + give_error('Could not connect to your KeepKey. Please verify the cable is connected and that no other app is using it.') + self.transport = HidTransport(d[0]) + self.client = QtGuiKeepKeyClient(self.transport) + self.client.handler = self.handler + self.client.set_tx_api(self) + self.client.bad = False + if not self.atleast_version(1, 0, 0): + self.client = None + give_error('Outdated KeepKey firmware. Please update the firmware from https://www.keepkey.com') + return self.client + + @hook + def close_wallet(self): + print_error("keepkey: clear session") + if self.client: + self.client.clear_session() + self.client.transport.close() + self.client = None + self.wallet = None + + + def show_address(self, address): + if not self.wallet.check_proper_device(): + give_error('Wrong device or password') + try: + address_path = self.wallet.address_id(address) + address_n = self.get_client().expand_path(address_path) + except Exception, e: + give_error(e) + try: + self.get_client().get_address('Bitcoin', address_n, True) + except Exception, e: + give_error(e) + finally: + self.handler.stop() + + + def sign_transaction(self, tx, prev_tx, xpub_path): + self.prev_tx = prev_tx + self.xpub_path = xpub_path + client = self.get_client() + inputs = self.tx_inputs(tx, True) + outputs = self.tx_outputs(tx) + try: + signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] + except Exception, e: + self.handler.stop() + give_error(e) + + self.handler.stop() + + raw = signed_tx.encode('hex') + tx.update_signatures(raw) + + + def tx_inputs(self, tx, for_sig=False): + inputs = [] + for txin in tx.inputs: + txinputtype = types.TxInputType() + if txin.get('is_coinbase'): + prev_hash = "\0"*32 + prev_index = 0xffffffff # signed int -1 + else: + if for_sig: + x_pubkeys = txin['x_pubkeys'] + if len(x_pubkeys) == 1: + x_pubkey = x_pubkeys[0] + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + else: + def f(x_pubkey): + if is_extended_pubkey(x_pubkey): + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + else: + xpub = xpub_from_pubkey(x_pubkey.decode('hex')) + s = [] + node = ckd_public.deserialize(xpub) + return types.HDNodePathType(node=node, address_n=s) + pubkeys = map(f, x_pubkeys) + multisig = types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), + m=txin.get('num_sig'), + ) + txinputtype = types.TxInputType( + script_type=types.SPENDMULTISIG, + multisig= multisig + ) + # find which key is mine + for x_pubkey in x_pubkeys: + if is_extended_pubkey(x_pubkey): + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + if xpub in self.xpub_path: + xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + break + + prev_hash = unhexlify(txin['prevout_hash']) + prev_index = txin['prevout_n'] + + txinputtype.prev_hash = prev_hash + txinputtype.prev_index = prev_index + + if 'scriptSig' in txin: + script_sig = txin['scriptSig'].decode('hex') + txinputtype.script_sig = script_sig + + if 'sequence' in txin: + sequence = txin['sequence'] + txinputtype.sequence = sequence + + inputs.append(txinputtype) + + return inputs + + def tx_outputs(self, tx): + outputs = [] + + for type, address, amount in tx.outputs: + assert type == 'address' + txoutputtype = types.TxOutputType() + if self.wallet.is_change(address): + address_path = self.wallet.address_id(address) + address_n = self.get_client().expand_path(address_path) + txoutputtype.address_n.extend(address_n) + else: + txoutputtype.address = address + txoutputtype.amount = amount + addrtype, hash_160 = bc_address_to_hash_160(address) + if addrtype == 0: + txoutputtype.script_type = types.PAYTOADDRESS + elif addrtype == 5: + txoutputtype.script_type = types.PAYTOSCRIPTHASH + else: + raise BaseException('addrtype') + outputs.append(txoutputtype) + + return outputs + + def electrum_tx_to_txtype(self, tx): + t = types.TransactionType() + d = deserialize(tx.raw) + t.version = d['version'] + t.lock_time = d['lockTime'] + inputs = self.tx_inputs(tx) + t.inputs.extend(inputs) + for vout in d['outputs']: + o = t.bin_outputs.add() + o.amount = vout['value'] + o.script_pubkey = vout['scriptPubKey'].decode('hex') + return t + + def get_tx(self, tx_hash): + tx = self.prev_tx[tx_hash] + tx.deserialize() + return self.electrum_tx_to_txtype(tx) + + + + + +class CmdlinePlugin(Plugin): + + @hook + def cmdline_load_wallet(self, wallet): + self.wallet = wallet + self.wallet.plugin = self + if self.handler is None: + self.handler = KeepKeyCmdLineHandler() + + + +class KeepKeyGuiMixin(object): + + def __init__(self, *args, **kwargs): + super(KeepKeyGuiMixin, self).__init__(*args, **kwargs) + + def callback_ButtonRequest(self, msg): + if msg.code == 3: + message = "Confirm transaction outputs on KeepKey device to continue" + elif msg.code == 8: + message = "Confirm transaction fee on KeepKey device to continue" + elif msg.code == 7: + message = "Confirm message to sign on KeepKey device to continue" + elif msg.code == 10: + message = "Confirm address on KeepKey device to continue" + else: + message = "Check KeepKey device to continue" + self.handler.show_message(msg.code, message, self) + return proto.ButtonAck() + + def callback_PinMatrixRequest(self, msg): + if msg.type == 1: + desc = 'current PIN' + elif msg.type == 2: + desc = 'new PIN' + elif msg.type == 3: + desc = 'new PIN again' + else: + desc = 'PIN' + pin = self.handler.get_pin("Please enter KeepKey %s" % desc) + if not pin: + return proto.Cancel() + return proto.PinMatrixAck(pin=pin) + + def callback_PassphraseRequest(self, req): + msg = _("Please enter your KeepKey passphrase.") + passphrase = self.handler.get_passphrase(msg) + if passphrase is None: + return proto.Cancel() + return proto.PassphraseAck(passphrase=passphrase) + + def callback_WordRequest(self, msg): + #TODO + log("Enter one word of mnemonic: ") + word = raw_input() + return proto.WordAck(word=word) + + +class KeepKeyCmdLineHandler: + + def get_passphrase(self, msg): + import getpass + print_msg(msg) + return getpass.getpass('') + + def get_pin(self, msg): + t = { 'a':'7', 'b':'8', 'c':'9', 'd':'4', 'e':'5', 'f':'6', 'g':'1', 'h':'2', 'i':'3'} + print_msg(msg) + print_msg("a b c\nd e f\ng h i\n-----") + o = raw_input() + return ''.join(map(lambda x: t[x], o)) + + def stop(self): + pass + + def show_message(self, msg): + print_msg(msg) + + +class KeepKeyQtHandler: + + def __init__(self, win): + self.win = win + self.win.connect(win, SIGNAL('keepkey_done'), self.dialog_stop) + self.win.connect(win, SIGNAL('message_dialog'), self.message_dialog) + self.win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog) + self.win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog) + self.done = threading.Event() + + def stop(self): + self.win.emit(SIGNAL('keepkey_done')) + + def show_message(self, msg_code, msg, client): + self.messsage_code = msg_code + self.message = msg + self.client = client + self.win.emit(SIGNAL('message_dialog')) + + def get_pin(self, msg): + self.done.clear() + self.message = msg + self.win.emit(SIGNAL('pin_dialog')) + self.done.wait() + return self.response + + def get_passphrase(self, msg): + self.done.clear() + self.message = msg + self.win.emit(SIGNAL('passphrase_dialog')) + self.done.wait() + return self.passphrase + + def pin_dialog(self): + d = QDialog(None) + d.setModal(1) + d.setWindowTitle(_("Enter PIN")) + d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) + matrix = PinMatrixWidget() + vbox = QVBoxLayout() + vbox.addWidget(QLabel(self.message)) + vbox.addWidget(matrix) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + d.setLayout(vbox) + if not d.exec_(): + self.response = None + self.response = str(matrix.get_value()) + self.done.set() + + def passphrase_dialog(self): + if type(self.win) is ElectrumWindow: + passphrase = self.win.password_dialog(_("Please enter your KeepKey passphrase")) + self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' + else: + assert type(self.win) is InstallWizard + from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog + d = QDialog() + d.setModal(1) + d.setLayout(make_password_dialog(d, None, self.message, False)) + confirmed, p, passphrase = run_password_dialog(d, None, None) + if not confirmed: + QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) + self.passphrase = None + else: + self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' + self.done.set() + + def message_dialog(self): + self.d = QDialog() + self.d.setModal(1) + self.d.setWindowTitle('Please Check KeepKey Device') + self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) + l = QLabel(self.message) + vbox = QVBoxLayout(self.d) + vbox.addWidget(l) + + if self.messsage_code in (3, 8): + vbox.addLayout(Buttons(CancelButton(self.d))) + self.d.connect(self.d, SIGNAL('rejected()'), self.client.cancel) + + self.d.show() + + def dialog_stop(self): + self.d.hide() + + +if KEEPKEY: + class QtGuiKeepKeyClient(ProtocolMixin, KeepKeyGuiMixin, BaseClient): + def call_raw(self, msg): + try: + resp = BaseClient.call_raw(self, msg) + except ConnectionError: + self.bad = True + raise + + return resp diff --git a/plugins/keepkey/qt.py b/plugins/keepkey/qt.py @@ -0,0 +1,95 @@ +from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton +import PyQt4.QtCore as QtCore +from electrum_gui.qt.util import * +from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow +from electrum_gui.qt.installwizard import InstallWizard +from keepkeylib.qt.pinmatrix import PinMatrixWidget + +from keepkey import KeepKeyPlugin + +class Plugin(KeepKeyPlugin): + + @hook + def load_wallet(self, wallet, window): + self.print_error("load_wallet") + self.wallet = wallet + self.wallet.plugin = self + self.keepkey_button = StatusBarButton(QIcon(":icons/keepkey.png"), _("KeepKey"), partial(self.settings_dialog, window)) + if type(window) is ElectrumWindow: + window.statusBar().addPermanentWidget(self.keepkey_button) + if self.handler is None: + self.handler = KeepKeyQtHandler(window) + try: + self.get_client().ping('t') + except BaseException as e: + QMessageBox.information(window, _('Error'), _("KeepKey device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) + self.wallet.force_watching_only = True + return + if self.wallet.addresses() and not self.wallet.check_proper_device(): + QMessageBox.information(window, _('Error'), _("This wallet does not match your KeepKey device"), _('OK')) + self.wallet.force_watching_only = True + + @hook + def installwizard_load_wallet(self, wallet, window): + if type(wallet) != KeepKeyWallet: + return + self.load_wallet(wallet, window) + + @hook + def installwizard_restore(self, wizard, storage): + if storage.get('wallet_type') != 'keepkey': + return + seed = wizard.enter_seed_dialog("Enter your KeepKey seed", None, func=lambda x:True) + if not seed: + return + wallet = KeepKeyWallet(storage) + self.wallet = wallet + handler = KeepKeyQtHandler(wizard) + passphrase = handler.get_passphrase(_("Please enter your KeepKey passphrase.") + '\n' + _("Press OK if you do not use one.")) + if passphrase is None: + return + password = wizard.password_dialog() + wallet.add_seed(seed, password) + wallet.add_cosigner_seed(seed, 'x/', password, passphrase) + wallet.create_main_account(password) + # disable keepkey plugin + self.set_enabled(False) + return wallet + + @hook + def receive_menu(self, menu, addrs): + if not self.wallet.is_watching_only() and self.atleast_version(1, 3) and len(addrs) == 1: + menu.addAction(_("Show on TREZOR"), lambda: self.show_address(addrs[0])) + + def settings_dialog(self, window): + try: + device_id = self.get_client().get_device_id() + except BaseException as e: + window.show_message(str(e)) + return + get_label = lambda: self.get_client().features.label + update_label = lambda: current_label_label.setText("Label: %s" % get_label()) + d = QDialog() + layout = QGridLayout(d) + layout.addWidget(QLabel("KeepKey Options"),0,0) + layout.addWidget(QLabel("ID:"),1,0) + layout.addWidget(QLabel(" %s" % device_id),1,1) + + def modify_label(): + response = QInputDialog().getText(None, "Set New KeepKey Label", "New KeepKey Label: (upon submission confirm on KeepKey)") + if not response[1]: + return + new_label = str(response[0]) + self.handler.show_message("Please confirm label change on KeepKey") + status = self.get_client().apply_settings(label=new_label) + self.handler.stop() + update_label() + + current_label_label = QLabel() + update_label() + change_label_button = QPushButton("Modify") + change_label_button.clicked.connect(modify_label) + layout.addWidget(current_label_label,3,0) + layout.addWidget(change_label_button,3,1) + d.exec_() + diff --git a/plugins/labels.py b/plugins/labels.py @@ -1,209 +0,0 @@ -import socket -import requests -import threading -import hashlib -import json -import sys -import traceback -from functools import partial - -import aes -import base64 - -import electrum -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ - - -class Plugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.target_host = 'sync.bytesized-hosting.com:9090' - self.wallets = {} - - def encode(self, wallet, msg): - password, iv, wallet_id = self.wallets[wallet] - encrypted = electrum.bitcoin.aes_encrypt_with_iv(password, iv, - msg.encode('utf8')) - return base64.b64encode(encrypted) - - def decode(self, wallet, message): - password, iv, wallet_id = self.wallets[wallet] - decoded = base64.b64decode(message) - decrypted = electrum.bitcoin.aes_decrypt_with_iv(password, iv, decoded) - return decrypted.decode('utf8') - - def get_nonce(self, wallet): - # nonce is the nonce to be used with the next change - nonce = wallet.storage.get('wallet_nonce') - if nonce is None: - nonce = 1 - self.set_nonce(wallet, nonce) - return nonce - - def set_nonce(self, wallet, nonce, force_write=True): - self.print_error("set", wallet.basename(), "nonce to", nonce) - wallet.storage.put("wallet_nonce", nonce, force_write) - - @hook - def set_label(self, wallet, item, label): - if not wallet in self.wallets: - return - nonce = self.get_nonce(wallet) - wallet_id = self.wallets[wallet][2] - bundle = {"walletId": wallet_id, - "walletNonce": nonce, - "externalId": self.encode(wallet, item), - "encryptedLabel": self.encode(wallet, label)} - t = threading.Thread(target=self.do_request, - args=["POST", "/label", False, bundle]) - t.setDaemon(True) - t.start() - # Caller will write the wallet - self.set_nonce(wallet, nonce + 1, force_write=False) - - def do_request(self, method, url = "/labels", is_batch=False, data=None): - url = 'https://' + self.target_host + url - kwargs = {'headers': {}} - if method == 'GET' and data: - kwargs['params'] = data - elif method == 'POST' and data: - kwargs['data'] = json.dumps(data) - kwargs['headers']['Content-Type'] = 'application/json' - response = requests.request(method, url, **kwargs) - if response.status_code != 200: - raise BaseException(response.status_code, response.text) - response = response.json() - if "error" in response: - raise BaseException(response["error"]) - return response - - def push_thread(self, wallet): - wallet_id = self.wallets[wallet][2] - bundle = {"labels": [], - "walletId": wallet_id, - "walletNonce": self.get_nonce(wallet)} - for key, value in wallet.labels.iteritems(): - try: - encoded_key = self.encode(wallet, key) - encoded_value = self.encode(wallet, value) - except: - self.print_error('cannot encode', repr(key), repr(value)) - continue - bundle["labels"].append({'encryptedLabel': encoded_value, - 'externalId': encoded_key}) - self.do_request("POST", "/labels", True, bundle) - - def pull_thread(self, wallet, force): - wallet_id = self.wallets[wallet][2] - nonce = 1 if force else self.get_nonce(wallet) - 1 - self.print_error("asking for labels since nonce", nonce) - try: - response = self.do_request("GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id) )) - if response["labels"] is None: - self.print_error('no new labels') - return - result = {} - for label in response["labels"]: - try: - key = self.decode(wallet, label["externalId"]) - value = self.decode(wallet, label["encryptedLabel"]) - except: - continue - try: - json.dumps(key) - json.dumps(value) - except: - self.print_error('error: no json', key) - continue - result[key] = value - - for key, value in result.items(): - if force or not wallet.labels.get(key): - wallet.labels[key] = value - - self.print_error("received %d labels" % len(response)) - # do not write to disk because we're in a daemon thread - wallet.storage.put('labels', wallet.labels, False) - self.set_nonce(wallet, response["nonce"] + 1, False) - self.on_pulled(wallet) - - except Exception as e: - traceback.print_exc(file=sys.stderr) - self.print_error("could not retrieve labels") - - - - - -from PyQt4.QtGui import * -from PyQt4.QtCore import * -import PyQt4.QtCore as QtCore -import PyQt4.QtGui as QtGui -from electrum_gui.qt import HelpButton, EnterButton -from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton - -class QtPlugin(Plugin): - - def __init__(self, *args): - Plugin.__init__(self, *args) - self.obj = QObject() - - def requires_settings(self): - return True - - def settings_widget(self, window): - return EnterButton(_('Settings'), - partial(self.settings_dialog, window)) - - def settings_dialog(self, window): - d = QDialog(window) - vbox = QVBoxLayout(d) - layout = QGridLayout() - vbox.addLayout(layout) - layout.addWidget(QLabel("Label sync options: "), 2, 0) - self.upload = ThreadedButton("Force upload", - partial(self.push_thread, window.wallet), - self.done_processing) - layout.addWidget(self.upload, 2, 1) - self.download = ThreadedButton("Force download", - partial(self.pull_thread, window.wallet, True), - self.done_processing) - layout.addWidget(self.download, 2, 2) - self.accept = OkButton(d, _("Done")) - vbox.addLayout(Buttons(CancelButton(d), self.accept)) - if d.exec_(): - return True - else: - return False - - def on_pulled(self, wallet): - self.obj.emit(SIGNAL('labels_changed'), wallet) - - def done_processing(self): - QMessageBox.information(None, _("Labels synchronised"), - _("Your labels have been synchronised.")) - - @hook - def on_new_window(self, window): - window.connect(window.app, SIGNAL('labels_changed'), window.update_tabs) - wallet = window.wallet - nonce = self.get_nonce(wallet) - self.print_error("wallet", wallet.basename(), "nonce is", nonce) - mpk = ''.join(sorted(wallet.get_master_public_keys().values())) - if not mpk: - return - password = hashlib.sha1(mpk).digest().encode('hex')[:32] - iv = hashlib.sha256(password).digest()[:16] - wallet_id = hashlib.sha256(mpk).digest().encode('hex') - self.wallets[wallet] = (password, iv, wallet_id) - # If there is an auth token we can try to actually start syncing - t = threading.Thread(target=self.pull_thread, args=(wallet, False)) - t.setDaemon(True) - t.start() - - @hook - def on_close_window(self, window): - self.wallets.pop(window.wallet) - diff --git a/plugins/labels/__init__.py b/plugins/labels/__init__.py @@ -0,0 +1,9 @@ +from electrum.i18n import _ + +fullname = _('LabelSync') +description = '\n'.join([ + _("Synchronize your labels across multiple Electrum installs by using a remote database to save your data. Labels, transactions ids and addresses are encrypted before they are sent to the remote server."), + _("The label sync's server software is open-source as well and can be found on github.com/maran/electrum-sync-server") +]) +available_for = ['qt', 'kivy'] + diff --git a/plugins/labels/kivy.py b/plugins/labels/kivy.py @@ -0,0 +1,3 @@ +from labels import LabelsPlugin +class Plugin(LabelsPlugin): + pass diff --git a/plugins/labels/labels.py b/plugins/labels/labels.py @@ -0,0 +1,138 @@ +import requests +import threading +import json +import sys +import traceback + +import aes +import base64 + +import electrum +from electrum.plugins import BasePlugin, hook +from electrum.i18n import _ + + + + +class LabelsPlugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.target_host = 'sync.bytesized-hosting.com:9090' + self.wallets = {} + + def encode(self, wallet, msg): + password, iv, wallet_id = self.wallets[wallet] + encrypted = electrum.bitcoin.aes_encrypt_with_iv(password, iv, + msg.encode('utf8')) + return base64.b64encode(encrypted) + + def decode(self, wallet, message): + password, iv, wallet_id = self.wallets[wallet] + decoded = base64.b64decode(message) + decrypted = electrum.bitcoin.aes_decrypt_with_iv(password, iv, decoded) + return decrypted.decode('utf8') + + def get_nonce(self, wallet): + # nonce is the nonce to be used with the next change + nonce = wallet.storage.get('wallet_nonce') + if nonce is None: + nonce = 1 + self.set_nonce(wallet, nonce) + return nonce + + def set_nonce(self, wallet, nonce, force_write=True): + self.print_error("set", wallet.basename(), "nonce to", nonce) + wallet.storage.put("wallet_nonce", nonce, force_write) + + @hook + def set_label(self, wallet, item, label): + if not wallet in self.wallets: + return + nonce = self.get_nonce(wallet) + wallet_id = self.wallets[wallet][2] + bundle = {"walletId": wallet_id, + "walletNonce": nonce, + "externalId": self.encode(wallet, item), + "encryptedLabel": self.encode(wallet, label)} + t = threading.Thread(target=self.do_request, + args=["POST", "/label", False, bundle]) + t.setDaemon(True) + t.start() + # Caller will write the wallet + self.set_nonce(wallet, nonce + 1, force_write=False) + + def do_request(self, method, url = "/labels", is_batch=False, data=None): + url = 'https://' + self.target_host + url + kwargs = {'headers': {}} + if method == 'GET' and data: + kwargs['params'] = data + elif method == 'POST' and data: + kwargs['data'] = json.dumps(data) + kwargs['headers']['Content-Type'] = 'application/json' + response = requests.request(method, url, **kwargs) + if response.status_code != 200: + raise BaseException(response.status_code, response.text) + response = response.json() + if "error" in response: + raise BaseException(response["error"]) + return response + + def push_thread(self, wallet): + wallet_id = self.wallets[wallet][2] + bundle = {"labels": [], + "walletId": wallet_id, + "walletNonce": self.get_nonce(wallet)} + for key, value in wallet.labels.iteritems(): + try: + encoded_key = self.encode(wallet, key) + encoded_value = self.encode(wallet, value) + except: + self.print_error('cannot encode', repr(key), repr(value)) + continue + bundle["labels"].append({'encryptedLabel': encoded_value, + 'externalId': encoded_key}) + self.do_request("POST", "/labels", True, bundle) + + def pull_thread(self, wallet, force): + wallet_id = self.wallets[wallet][2] + nonce = 1 if force else self.get_nonce(wallet) - 1 + self.print_error("asking for labels since nonce", nonce) + try: + response = self.do_request("GET", ("/labels/since/%d/for/%s" % (nonce, wallet_id) )) + if response["labels"] is None: + self.print_error('no new labels') + return + result = {} + for label in response["labels"]: + try: + key = self.decode(wallet, label["externalId"]) + value = self.decode(wallet, label["encryptedLabel"]) + except: + continue + try: + json.dumps(key) + json.dumps(value) + except: + self.print_error('error: no json', key) + continue + result[key] = value + + for key, value in result.items(): + if force or not wallet.labels.get(key): + wallet.labels[key] = value + + self.print_error("received %d labels" % len(response)) + # do not write to disk because we're in a daemon thread + wallet.storage.put('labels', wallet.labels, False) + self.set_nonce(wallet, response["nonce"] + 1, False) + self.on_pulled(wallet) + + except Exception as e: + traceback.print_exc(file=sys.stderr) + self.print_error("could not retrieve labels") + + + + + diff --git a/plugins/labels/qt.py b/plugins/labels/qt.py @@ -0,0 +1,80 @@ +import hashlib +import threading +from functools import partial + +from PyQt4.QtGui import * +from PyQt4.QtCore import * +import PyQt4.QtCore as QtCore +import PyQt4.QtGui as QtGui + +from electrum.plugins import hook +from electrum.i18n import _ +from electrum_gui.qt import HelpButton, EnterButton +from electrum_gui.qt.util import ThreadedButton, Buttons, CancelButton, OkButton + +from labels import LabelsPlugin + + +class Plugin(LabelsPlugin): + + def __init__(self, *args): + LabelsPlugin.__init__(self, *args) + self.obj = QObject() + + def requires_settings(self): + return True + + def settings_widget(self, window): + return EnterButton(_('Settings'), + partial(self.settings_dialog, window)) + + def settings_dialog(self, window): + d = QDialog(window) + vbox = QVBoxLayout(d) + layout = QGridLayout() + vbox.addLayout(layout) + layout.addWidget(QLabel("Label sync options: "), 2, 0) + self.upload = ThreadedButton("Force upload", + partial(self.push_thread, window.wallet), + self.done_processing) + layout.addWidget(self.upload, 2, 1) + self.download = ThreadedButton("Force download", + partial(self.pull_thread, window.wallet, True), + self.done_processing) + layout.addWidget(self.download, 2, 2) + self.accept = OkButton(d, _("Done")) + vbox.addLayout(Buttons(CancelButton(d), self.accept)) + if d.exec_(): + return True + else: + return False + + def on_pulled(self, wallet): + self.obj.emit(SIGNAL('labels_changed'), wallet) + + def done_processing(self): + QMessageBox.information(None, _("Labels synchronised"), + _("Your labels have been synchronised.")) + + @hook + def on_new_window(self, window): + window.connect(window.app, SIGNAL('labels_changed'), window.update_tabs) + wallet = window.wallet + nonce = self.get_nonce(wallet) + self.print_error("wallet", wallet.basename(), "nonce is", nonce) + mpk = ''.join(sorted(wallet.get_master_public_keys().values())) + if not mpk: + return + password = hashlib.sha1(mpk).digest().encode('hex')[:32] + iv = hashlib.sha256(password).digest()[:16] + wallet_id = hashlib.sha256(mpk).digest().encode('hex') + self.wallets[wallet] = (password, iv, wallet_id) + # If there is an auth token we can try to actually start syncing + t = threading.Thread(target=self.pull_thread, args=(wallet, False)) + t.setDaemon(True) + t.start() + + @hook + def on_close_window(self, window): + self.wallets.pop(window.wallet) + diff --git a/plugins/ledger/__init__.py b/plugins/ledger/__init__.py @@ -0,0 +1,8 @@ +from electrum.i18n import _ + +fullname = 'Ledger Wallet' +description = 'Provides support for Ledger hardware wallet' +requires = [('btchip', 'github.com/ledgerhq/btchip-python')] +requires_wallet_type = ['btchip'] +registers_wallet_type = ('hardware', 'btchip', _("Ledger wallet")) +available_for = ['qt', 'cmdline'] diff --git a/plugins/ledger/ledger.py b/plugins/ledger/ledger.py @@ -0,0 +1,522 @@ +from binascii import unhexlify +from binascii import hexlify +from struct import pack,unpack +from sys import stderr +from time import sleep + +import electrum +from electrum.account import BIP32_Account +from electrum.bitcoin import EncodeBase58Check, DecodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160 +from electrum.i18n import _ +from electrum.plugins import BasePlugin, hook +from electrum.transaction import deserialize +from electrum.wallet import BIP32_HD_Wallet, BIP32_Wallet + +from electrum.util import format_satoshis_plain, print_error, print_msg +import hashlib +import threading + +try: + from btchip.btchipComm import getDongle, DongleWait + from btchip.btchip import btchip + from btchip.btchipUtils import compress_public_key,format_transaction, get_regular_input_script + from btchip.bitcoinTransaction import bitcoinTransaction + from btchip.btchipPersoWizard import StartBTChipPersoDialog + from btchip.btchipFirmwareWizard import checkFirmware, updateFirmware + from btchip.btchipException import BTChipException + BTCHIP = True + BTCHIP_DEBUG = False +except ImportError: + BTCHIP = False + + +class BTChipWallet(BIP32_HD_Wallet): + wallet_type = 'btchip' + root_derivation = "m/44'/0'" + + def __init__(self, storage): + BIP32_HD_Wallet.__init__(self, storage) + self.transport = None + self.client = None + self.mpk = None + self.device_checked = False + self.signing = False + self.force_watching_only = False + + def give_error(self, message, clear_client = False): + print_error(message) + if not self.signing: + QMessageBox.warning(QDialog(), _('Warning'), _(message), _('OK')) + else: + self.signing = False + if clear_client and self.client is not None: + self.client.bad = True + self.device_checked = False + raise Exception(message) + + def get_action(self): + if not self.accounts: + return 'create_accounts' + + def can_sign_xpubkey(self, x_pubkey): + xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) + return xpub in self.master_public_keys.values() + + def can_create_accounts(self): + return False + + def synchronize(self): + # synchronize existing accounts + BIP32_Wallet.synchronize(self) + # no further accounts for the moment + + def can_change_password(self): + return False + + def is_watching_only(self): + return self.force_watching_only + + def get_client(self, noPin=False): + if not BTCHIP: + self.give_error('please install github.com/btchip/btchip-python') + + aborted = False + if not self.client or self.client.bad: + try: + d = getDongle(BTCHIP_DEBUG) + self.client = btchip(d) + self.client.handler = self.plugin.handler + firmware = self.client.getFirmwareVersion()['version'].split(".") + if not checkFirmware(firmware): + d.close() + try: + updateFirmware() + except Exception, e: + aborted = True + raise e + d = getDongle(BTCHIP_DEBUG) + self.client = btchip(d) + try: + self.client.getOperationMode() + except BTChipException, e: + if (e.sw == 0x6985): + d.close() + dialog = StartBTChipPersoDialog() + dialog.exec_() + # Then fetch the reference again as it was invalidated + d = getDongle(BTCHIP_DEBUG) + self.client = btchip(d) + else: + raise e + if not noPin: + # Immediately prompts for the PIN + remaining_attempts = self.client.getVerifyPinRemainingAttempts() + if remaining_attempts <> 1: + msg = "Enter your Ledger PIN - remaining attempts : " + str(remaining_attempts) + else: + msg = "Enter your Ledger PIN - WARNING : LAST ATTEMPT. If the PIN is not correct, the dongle will be wiped." + confirmed, p, pin = self.password_dialog(msg) + if not confirmed: + aborted = True + raise Exception('Aborted by user - please unplug the dongle and plug it again before retrying') + pin = pin.encode() + self.client.verifyPin(pin) + + except BTChipException, e: + try: + self.client.dongle.close() + except: + pass + self.client = None + if (e.sw == 0x6faa): + raise Exception("Dongle is temporarily locked - please unplug it and replug it again") + if ((e.sw & 0xFFF0) == 0x63c0): + raise Exception("Invalid PIN - please unplug the dongle and plug it again before retrying") + raise e + except Exception, e: + try: + self.client.dongle.close() + except: + pass + self.client = None + if not aborted: + raise Exception("Could not connect to your Ledger wallet. Please verify access permissions, PIN, or unplug the dongle and plug it again") + else: + raise e + self.client.bad = False + self.device_checked = False + self.proper_device = False + return self.client + + def address_id(self, address): + account_id, (change, address_index) = self.get_address_index(address) + return "44'/0'/%s'/%d/%d" % (account_id, change, address_index) + + def create_main_account(self, password): + self.create_account('Main account', None) #name, empty password + + def derive_xkeys(self, root, derivation, password): + derivation = derivation.replace(self.root_name,"44'/0'/") + xpub = self.get_public_key(derivation) + return xpub, None + + def get_private_key(self, address, password): + return [] + + def get_public_key(self, bip32_path): + # S-L-O-W - we don't handle the fingerprint directly, so compute it manually from the previous node + # This only happens once so it's bearable + self.get_client() # prompt for the PIN before displaying the dialog if necessary + self.plugin.handler.show_message("Computing master public key") + try: + splitPath = bip32_path.split('/') + fingerprint = 0 + if len(splitPath) > 1: + prevPath = "/".join(splitPath[0:len(splitPath) - 1]) + nodeData = self.get_client().getWalletPublicKey(prevPath) + publicKey = compress_public_key(nodeData['publicKey']) + h = hashlib.new('ripemd160') + h.update(hashlib.sha256(publicKey).digest()) + fingerprint = unpack(">I", h.digest()[0:4])[0] + nodeData = self.get_client().getWalletPublicKey(bip32_path) + publicKey = compress_public_key(nodeData['publicKey']) + depth = len(splitPath) + lastChild = splitPath[len(splitPath) - 1].split('\'') + if len(lastChild) == 1: + childnum = int(lastChild[0]) + else: + childnum = 0x80000000 | int(lastChild[0]) + xpub = "0488B21E".decode('hex') + chr(depth) + self.i4b(fingerprint) + self.i4b(childnum) + str(nodeData['chainCode']) + str(publicKey) + except Exception, e: + self.give_error(e, True) + finally: + self.plugin.handler.stop() + + return EncodeBase58Check(xpub) + + def get_master_public_key(self): + try: + if not self.mpk: + self.mpk = self.get_public_key("44'/0'") + return self.mpk + except Exception, e: + self.give_error(e, True) + + def i4b(self, x): + return pack('>I', x) + + def add_keypairs(self, tx, keypairs, password): + #do nothing - no priv keys available + pass + + def decrypt_message(self, pubkey, message, password): + self.give_error("Not supported") + + def sign_message(self, address, message, password): + if self.has_seed(): + return BIP32_HD_Wallet.sign_message(self, address, message, password) + use2FA = False + self.signing = True + self.get_client() # prompt for the PIN before displaying the dialog if necessary + if not self.check_proper_device(): + self.give_error('Wrong device or password') + address_path = self.address_id(address) + self.plugin.handler.show_message("Signing message ...") + try: + info = self.get_client().signMessagePrepare(address_path, message) + pin = "" + if info['confirmationNeeded']: + # TODO : handle different confirmation types. For the time being only supports keyboard 2FA + use2FA = True + confirmed, p, pin = self.password_dialog() + if not confirmed: + raise Exception('Aborted by user') + pin = pin.encode() + self.client.bad = True + self.device_checked = False + self.get_client(True) + signature = self.get_client().signMessageSign(pin) + except BTChipException, e: + if e.sw == 0x6a80: + self.give_error("Unfortunately, this message cannot be signed by the Ledger wallet. Only alphanumerical messages shorter than 140 characters are supported. Please remove any extra characters (tab, carriage return) and retry.") + else: + self.give_error(e, True) + except Exception, e: + self.give_error(e, True) + finally: + self.plugin.handler.stop() + self.client.bad = use2FA + self.signing = False + + # Parse the ASN.1 signature + + rLength = signature[3] + r = signature[4 : 4 + rLength] + sLength = signature[4 + rLength + 1] + s = signature[4 + rLength + 2:] + if rLength == 33: + r = r[1:] + if sLength == 33: + s = s[1:] + r = str(r) + s = str(s) + + # And convert it + return chr(27 + 4 + (signature[0] & 0x01)) + r + s + + def sign_transaction(self, tx, password): + if self.has_seed(): + return BIP32_HD_Wallet.sign_transaction(self, tx, password) + if tx.is_complete(): + return + #if tx.error: + # raise BaseException(tx.error) + self.signing = True + inputs = [] + inputsPaths = [] + pubKeys = [] + trustedInputs = [] + redeemScripts = [] + signatures = [] + preparedTrustedInputs = [] + changePath = "" + changeAmount = None + output = None + outputAmount = None + use2FA = False + pin = "" + rawTx = tx.serialize() + # Fetch inputs of the transaction to sign + for txinput in tx.inputs: + if ('is_coinbase' in txinput and txinput['is_coinbase']): + self.give_error("Coinbase not supported") # should never happen + inputs.append([ self.transactions[txinput['prevout_hash']].raw, + txinput['prevout_n'] ]) + address = txinput['address'] + inputsPaths.append(self.address_id(address)) + pubKeys.append(self.get_public_keys(address)) + + # Recognize outputs - only one output and one change is authorized + if len(tx.outputs) > 2: # should never happen + self.give_error("Transaction with more than 2 outputs not supported") + for type, address, amount in tx.outputs: + assert type == 'address' + if self.is_change(address): + changePath = self.address_id(address) + changeAmount = amount + else: + if output <> None: # should never happen + self.give_error("Multiple outputs with no change not supported") + output = address + outputAmount = amount + + self.get_client() # prompt for the PIN before displaying the dialog if necessary + if not self.check_proper_device(): + self.give_error('Wrong device or password') + + self.plugin.handler.show_message("Signing Transaction ...") + try: + # Get trusted inputs from the original transactions + for utxo in inputs: + txtmp = bitcoinTransaction(bytearray(utxo[0].decode('hex'))) + trustedInputs.append(self.get_client().getTrustedInput(txtmp, utxo[1])) + # TODO : Support P2SH later + redeemScripts.append(txtmp.outputs[utxo[1]].script) + # Sign all inputs + firstTransaction = True + inputIndex = 0 + while inputIndex < len(inputs): + self.get_client().startUntrustedTransaction(firstTransaction, inputIndex, + trustedInputs, redeemScripts[inputIndex]) + outputData = self.get_client().finalizeInput(output, format_satoshis_plain(outputAmount), + format_satoshis_plain(self.get_tx_fee(tx)), changePath, bytearray(rawTx.decode('hex'))) + if firstTransaction: + transactionOutput = outputData['outputData'] + if outputData['confirmationNeeded']: + # TODO : handle different confirmation types. For the time being only supports keyboard 2FA + self.plugin.handler.stop() + if 'keycardData' in outputData: + pin2 = "" + for keycardIndex in range(len(outputData['keycardData'])): + msg = "Do not enter your device PIN here !\r\n\r\n" + \ + "Your Ledger Wallet wants to talk to you and tell you a unique second factor code.\r\n" + \ + "For this to work, please match the character between stars of the output address using your security card\r\n\r\n" + \ + "Output address : " + for index in range(len(output)): + if index == outputData['keycardData'][keycardIndex]: + msg = msg + "*" + output[index] + "*" + else: + msg = msg + output[index] + msg = msg + "\r\n" + confirmed, p, pin = self.password_dialog(msg) + if not confirmed: + raise Exception('Aborted by user') + try: + pin2 = pin2 + chr(int(pin[0], 16)) + except: + raise Exception('Invalid PIN character') + pin = pin2 + else: + use2FA = True + confirmed, p, pin = self.password_dialog() + if not confirmed: + raise Exception('Aborted by user') + pin = pin.encode() + self.client.bad = True + self.device_checked = False + self.get_client(True) + self.plugin.handler.show_message("Signing ...") + else: + # Sign input with the provided PIN + inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], + pin) + inputSignature[0] = 0x30 # force for 1.4.9+ + signatures.append(inputSignature) + inputIndex = inputIndex + 1 + firstTransaction = False + except Exception, e: + self.give_error(e, True) + finally: + self.plugin.handler.stop() + + # Reformat transaction + inputIndex = 0 + while inputIndex < len(inputs): + # TODO : Support P2SH later + inputScript = get_regular_input_script(signatures[inputIndex], pubKeys[inputIndex][0].decode('hex')) + preparedTrustedInputs.append([ trustedInputs[inputIndex]['value'], inputScript ]) + inputIndex = inputIndex + 1 + updatedTransaction = format_transaction(transactionOutput, preparedTrustedInputs) + updatedTransaction = hexlify(updatedTransaction) + tx.update(updatedTransaction) + self.client.bad = use2FA + self.signing = False + + def check_proper_device(self): + pubKey = DecodeBase58Check(self.master_public_keys["x/0'"])[45:] + if not self.device_checked: + self.plugin.handler.show_message("Checking device") + try: + nodeData = self.get_client().getWalletPublicKey("44'/0'/0'") + except Exception, e: + self.give_error(e, True) + finally: + self.plugin.handler.stop() + pubKeyDevice = compress_public_key(nodeData['publicKey']) + self.device_checked = True + if pubKey != pubKeyDevice: + self.proper_device = False + else: + self.proper_device = True + + return self.proper_device + + def password_dialog(self, msg=None): + if not msg: + msg = _("Do not enter your device PIN here !\r\n\r\n" \ + "Your Ledger Wallet wants to talk to you and tell you a unique second factor code.\r\n" \ + "For this to work, please open a text editor (on a different computer / device if you believe this computer is compromised) and put your cursor into it, unplug your Ledger Wallet and plug it back in.\r\n" \ + "It should show itself to your computer as a keyboard and output the second factor along with a summary of the transaction it is signing into the text-editor.\r\n\r\n" \ + "Check that summary and then enter the second factor code here.\r\n" \ + "Before clicking OK, re-plug the device once more (unplug it and plug it again if you read the second factor code on the same computer)") + response = self.plugin.handler.prompt_auth(msg) + if response is None: + return False, None, None + return True, response, response + + +class LedgerPlugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self._is_available = self._init() + self.wallet = None + self.handler = None + + def constructor(self, s): + return BTChipWallet(s) + + def _init(self): + return BTCHIP + + def is_available(self): + if not self._is_available: + return False + if not self.wallet: + return False + if self.wallet.storage.get('wallet_type') != 'btchip': + return False + return True + + def set_enabled(self, enabled): + self.wallet.storage.put('use_' + self.name, enabled) + + def is_enabled(self): + if not self.is_available(): + return False + if self.wallet.has_seed(): + return False + return True + + def btchip_is_connected(self): + try: + self.wallet.get_client().getFirmwareVersion() + except: + return False + return True + + @hook + def close_wallet(self): + self.wallet = None + + @hook + def installwizard_load_wallet(self, wallet, window): + if type(wallet) != BTChipWallet: + return + self.load_wallet(wallet, window) + + @hook + def installwizard_restore(self, wizard, storage): + if storage.get('wallet_type') != 'btchip': + return + wallet = BTChipWallet(storage) + try: + wallet.create_main_account(None) + except BaseException as e: + QMessageBox.information(None, _('Error'), str(e), _('OK')) + return + return wallet + + @hook + def sign_tx(self, window, tx): + tx.error = None + try: + self.wallet.sign_transaction(tx, None) + except Exception as e: + tx.error = str(e) + + +class CmdlinePlugin(Plugin): + @hook + def cmdline_load_wallet(self, wallet): + self.wallet = wallet + self.wallet.plugin = self + if self.handler is None: + self.handler = BTChipCmdLineHandler() + + +class BTChipCmdLineHandler: + + def stop(self): + pass + + def show_message(self, msg): + print_msg(msg) + + def prompt_auth(self, msg): + import getpass + print_msg(msg) + response = getpass.getpass('') + if len(response) == 0: + return None + return response diff --git a/plugins/ledger/qt.py b/plugins/ledger/qt.py @@ -0,0 +1,66 @@ +from PyQt4.Qt import QApplication, QMessageBox, QDialog, QInputDialog, QLineEdit, QVBoxLayout, QLabel, QThread, SIGNAL +import PyQt4.QtCore as QtCore +from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog + +class Plugin(LedgerPlugin): + + @hook + def load_wallet(self, wallet, window): + self.wallet = wallet + self.wallet.plugin = self + if self.handler is None: + self.handler = BTChipQTHandler(window) + if self.btchip_is_connected(): + if not self.wallet.check_proper_device(): + QMessageBox.information(window, _('Error'), _("This wallet does not match your Ledger device"), _('OK')) + self.wallet.force_watching_only = True + else: + QMessageBox.information(window, _('Error'), _("Ledger device not detected.\nContinuing in watching-only mode."), _('OK')) + self.wallet.force_watching_only = True + + +class BTChipQTHandler: + + def __init__(self, win): + self.win = win + self.win.connect(win, SIGNAL('btchip_done'), self.dialog_stop) + self.win.connect(win, SIGNAL('btchip_message_dialog'), self.message_dialog) + self.win.connect(win, SIGNAL('btchip_auth_dialog'), self.auth_dialog) + self.done = threading.Event() + + def stop(self): + self.win.emit(SIGNAL('btchip_done')) + + def show_message(self, msg): + self.message = msg + self.win.emit(SIGNAL('btchip_message_dialog')) + + def prompt_auth(self, msg): + self.done.clear() + self.message = msg + self.win.emit(SIGNAL('btchip_auth_dialog')) + self.done.wait() + return self.response + + def auth_dialog(self): + response = QInputDialog.getText(None, "Ledger Wallet Authentication", self.message, QLineEdit.Password) + if not response[1]: + self.response = None + else: + self.response = str(response[0]) + self.done.set() + + def message_dialog(self): + self.d = QDialog() + self.d.setModal(1) + self.d.setWindowTitle('Ledger') + self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) + l = QLabel(self.message) + vbox = QVBoxLayout(self.d) + vbox.addWidget(l) + self.d.show() + + def dialog_stop(self): + if self.d is not None: + self.d.hide() + self.d = None diff --git a/plugins/plot.py b/plugins/plot.py @@ -1,118 +0,0 @@ -from PyQt4.QtGui import * -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ - - -import datetime -from electrum.util import format_satoshis -from electrum.bitcoin import COIN - -try: - import matplotlib.pyplot as plt - import matplotlib.dates as md - from matplotlib.patches import Ellipse - from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker - flag_matlib=True -except: - flag_matlib=False - - -class QtPlugin(BasePlugin): - - def is_available(self): - if flag_matlib: - return True - else: - return False - - @hook - def export_history_dialog(self, window, hbox): - wallet = window.wallet - history = wallet.get_history() - if len(history) > 0: - b = QPushButton(_("Preview plot")) - hbox.addWidget(b) - b.clicked.connect(lambda: self.do_plot(wallet, history)) - else: - b = QPushButton(_("No history to plot")) - hbox.addWidget(b) - - - def do_plot(self, wallet, history): - balance_Val=[] - fee_val=[] - value_val=[] - datenums=[] - unknown_trans = 0 - pending_trans = 0 - counter_trans = 0 - balance = 0 - for item in history: - tx_hash, confirmations, value, timestamp, balance = item - if confirmations: - if timestamp is not None: - try: - datenums.append(md.date2num(datetime.datetime.fromtimestamp(timestamp))) - balance_Val.append(1000.*balance/COIN) - except [RuntimeError, TypeError, NameError] as reason: - unknown_trans += 1 - pass - else: - unknown_trans += 1 - else: - pending_trans += 1 - - value_val.append(1000.*value/COIN) - if tx_hash: - label, is_default_label = wallet.get_label(tx_hash) - label = label.encode('utf-8') - else: - label = "" - - - f, axarr = plt.subplots(2, sharex=True) - - plt.subplots_adjust(bottom=0.2) - plt.xticks( rotation=25 ) - ax=plt.gca() - x=19 - test11="Unknown transactions = "+str(unknown_trans)+" Pending transactions = "+str(pending_trans)+" ." - box1 = TextArea(" Test : Number of pending transactions", textprops=dict(color="k")) - box1.set_text(test11) - - - box = HPacker(children=[box1], - align="center", - pad=0.1, sep=15) - - anchored_box = AnchoredOffsetbox(loc=3, - child=box, pad=0.5, - frameon=True, - bbox_to_anchor=(0.5, 1.02), - bbox_transform=ax.transAxes, - borderpad=0.5, - ) - - - ax.add_artist(anchored_box) - - - plt.ylabel('mBTC') - plt.xlabel('Dates') - xfmt = md.DateFormatter('%Y-%m-%d') - ax.xaxis.set_major_formatter(xfmt) - - - axarr[0].plot(datenums,balance_Val,marker='o',linestyle='-',color='blue',label='Balance') - axarr[0].legend(loc='upper left') - axarr[0].set_title('History Transactions') - - - xfmt = md.DateFormatter('%Y-%m-%d') - ax.xaxis.set_major_formatter(xfmt) - axarr[1].plot(datenums,value_val,marker='o',linestyle='-',color='green',label='Value') - - - axarr[1].legend(loc='upper left') - # plt.annotate('unknown transaction = %d \n pending transactions = %d' %(unknown_trans,pending_trans),xy=(0.7,0.05),xycoords='axes fraction',size=12) - plt.show() diff --git a/plugins/plot/__init__.py b/plugins/plot/__init__.py @@ -0,0 +1,6 @@ +from electrum.i18n import _ + +fullname = 'Plot History' +description = _("Ability to plot transaction history in graphical mode.") +requires = [('matplotlib', 'matplotlib')] +available_for = ['qt'] diff --git a/plugins/plot/qt.py b/plugins/plot/qt.py @@ -0,0 +1,118 @@ +from PyQt4.QtGui import * +from electrum.plugins import BasePlugin, hook +from electrum.i18n import _ + + +import datetime +from electrum.util import format_satoshis +from electrum.bitcoin import COIN + +try: + import matplotlib.pyplot as plt + import matplotlib.dates as md + from matplotlib.patches import Ellipse + from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker + flag_matlib=True +except: + flag_matlib=False + + +class Plugin(BasePlugin): + + def is_available(self): + if flag_matlib: + return True + else: + return False + + @hook + def export_history_dialog(self, window, hbox): + wallet = window.wallet + history = wallet.get_history() + if len(history) > 0: + b = QPushButton(_("Preview plot")) + hbox.addWidget(b) + b.clicked.connect(lambda: self.do_plot(wallet, history)) + else: + b = QPushButton(_("No history to plot")) + hbox.addWidget(b) + + + def do_plot(self, wallet, history): + balance_Val=[] + fee_val=[] + value_val=[] + datenums=[] + unknown_trans = 0 + pending_trans = 0 + counter_trans = 0 + balance = 0 + for item in history: + tx_hash, confirmations, value, timestamp, balance = item + if confirmations: + if timestamp is not None: + try: + datenums.append(md.date2num(datetime.datetime.fromtimestamp(timestamp))) + balance_Val.append(1000.*balance/COIN) + except [RuntimeError, TypeError, NameError] as reason: + unknown_trans += 1 + pass + else: + unknown_trans += 1 + else: + pending_trans += 1 + + value_val.append(1000.*value/COIN) + if tx_hash: + label, is_default_label = wallet.get_label(tx_hash) + label = label.encode('utf-8') + else: + label = "" + + + f, axarr = plt.subplots(2, sharex=True) + + plt.subplots_adjust(bottom=0.2) + plt.xticks( rotation=25 ) + ax=plt.gca() + x=19 + test11="Unknown transactions = "+str(unknown_trans)+" Pending transactions = "+str(pending_trans)+" ." + box1 = TextArea(" Test : Number of pending transactions", textprops=dict(color="k")) + box1.set_text(test11) + + + box = HPacker(children=[box1], + align="center", + pad=0.1, sep=15) + + anchored_box = AnchoredOffsetbox(loc=3, + child=box, pad=0.5, + frameon=True, + bbox_to_anchor=(0.5, 1.02), + bbox_transform=ax.transAxes, + borderpad=0.5, + ) + + + ax.add_artist(anchored_box) + + + plt.ylabel('mBTC') + plt.xlabel('Dates') + xfmt = md.DateFormatter('%Y-%m-%d') + ax.xaxis.set_major_formatter(xfmt) + + + axarr[0].plot(datenums,balance_Val,marker='o',linestyle='-',color='blue',label='Balance') + axarr[0].legend(loc='upper left') + axarr[0].set_title('History Transactions') + + + xfmt = md.DateFormatter('%Y-%m-%d') + ax.xaxis.set_major_formatter(xfmt) + axarr[1].plot(datenums,value_val,marker='o',linestyle='-',color='green',label='Value') + + + axarr[1].legend(loc='upper left') + # plt.annotate('unknown transaction = %d \n pending transactions = %d' %(unknown_trans,pending_trans),xy=(0.7,0.05),xycoords='axes fraction',size=12) + plt.show() diff --git a/plugins/trezor.py b/plugins/trezor.py @@ -1,673 +0,0 @@ -from binascii import unhexlify -from struct import pack -from sys import stderr -from time import sleep -import unicodedata -import threading -import re -from functools import partial - - -import electrum -from electrum import bitcoin - -from electrum.account import BIP32_Account -from electrum.bitcoin import EncodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160, xpub_from_pubkey -from electrum.i18n import _ -from electrum.plugins import BasePlugin, hook, always_hook, run_hook -from electrum.transaction import Transaction, deserialize, is_extended_pubkey, x_to_xpub -from electrum.wallet import BIP32_HD_Wallet -from electrum.util import print_error, print_msg -from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root - -try: - from trezorlib.client import types - from trezorlib.client import proto, BaseClient, ProtocolMixin - from trezorlib.transport import ConnectionError - from trezorlib.transport_hid import HidTransport - TREZOR = True -except ImportError: - TREZOR = False - -import trezorlib.ckd_public as ckd_public - -def log(msg): - stderr.write("%s\n" % msg) - stderr.flush() - -def give_error(message): - print_error(message) - raise Exception(message) - - -class TrezorWallet(BIP32_HD_Wallet): - wallet_type = 'trezor' - root_derivation = "m/44'/0'" - - def __init__(self, storage): - BIP32_HD_Wallet.__init__(self, storage) - self.mpk = None - self.device_checked = False - self.proper_device = False - self.force_watching_only = False - - def get_action(self): - if not self.accounts: - return 'create_accounts' - - def can_import(self): - return False - - def can_sign_xpubkey(self, x_pubkey): - xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) - return xpub in self.master_public_keys.values() - - def can_export(self): - return False - - def can_create_accounts(self): - return True - - def can_change_password(self): - return False - - def is_watching_only(self): - return self.force_watching_only - - def get_client(self): - return self.plugin.get_client() - - def address_id(self, address): - account_id, (change, address_index) = self.get_address_index(address) - return "44'/0'/%s'/%d/%d" % (account_id, change, address_index) - - def create_main_account(self, password): - self.create_account('Main account', None) #name, empty password - - def mnemonic_to_seed(self, mnemonic, passphrase): - # trezor uses bip39 - import pbkdf2, hashlib, hmac - PBKDF2_ROUNDS = 2048 - mnemonic = unicodedata.normalize('NFKD', ' '.join(mnemonic.split())) - passphrase = unicodedata.normalize('NFKD', passphrase) - return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) - - def derive_xkeys(self, root, derivation, password): - x = self.master_private_keys.get(root) - if x: - root_xprv = pw_decode(x, password) - xprv, xpub = bip32_private_derivation(root_xprv, root, derivation) - return xpub, xprv - else: - derivation = derivation.replace(self.root_name,"44'/0'/") - xpub = self.get_public_key(derivation) - return xpub, None - - def get_public_key(self, bip32_path): - address_n = self.plugin.get_client().expand_path(bip32_path) - node = self.plugin.get_client().get_public_node(address_n).node - xpub = "0488B21E".decode('hex') + chr(node.depth) + self.i4b(node.fingerprint) + self.i4b(node.child_num) + node.chain_code + node.public_key - return EncodeBase58Check(xpub) - - def get_master_public_key(self): - if not self.mpk: - self.mpk = self.get_public_key("44'/0'") - return self.mpk - - def i4b(self, x): - return pack('>I', x) - - def add_keypairs(self, tx, keypairs, password): - #do nothing - no priv keys available - pass - - def decrypt_message(self, pubkey, message, password): - raise BaseException( _('Decrypt method is not implemented in Trezor') ) - #address = public_key_to_bc_address(pubkey.decode('hex')) - #address_path = self.address_id(address) - #address_n = self.get_client().expand_path(address_path) - #try: - # decrypted_msg = self.get_client().decrypt_message(address_n, b64decode(message)) - #except Exception, e: - # give_error(e) - #finally: - # twd.stop() - #return str(decrypted_msg) - - def sign_message(self, address, message, password): - if self.has_seed(): - return BIP32_HD_Wallet.sign_message(self, address, message, password) - if not self.check_proper_device(): - give_error('Wrong device or password') - try: - address_path = self.address_id(address) - address_n = self.plugin.get_client().expand_path(address_path) - except Exception, e: - give_error(e) - try: - msg_sig = self.plugin.get_client().sign_message('Bitcoin', address_n, message) - except Exception, e: - give_error(e) - finally: - self.plugin.handler.stop() - return msg_sig.signature - - def sign_transaction(self, tx, password): - if self.has_seed(): - return BIP32_HD_Wallet.sign_transaction(self, tx, password) - if tx.is_complete(): - return - if not self.check_proper_device(): - give_error('Wrong device or password') - # previous transactions used as inputs - prev_tx = {} - # path of the xpubs that are involved - xpub_path = {} - for txin in tx.inputs: - tx_hash = txin['prevout_hash'] - - ptx = self.transactions.get(tx_hash) - if ptx is None: - ptx = self.network.synchronous_get(('blockchain.transaction.get', [tx_hash])) - ptx = Transaction(ptx) - prev_tx[tx_hash] = ptx - - for x_pubkey in txin['x_pubkeys']: - account_derivation = None - if not is_extended_pubkey(x_pubkey): - continue - xpub = x_to_xpub(x_pubkey) - for k, v in self.master_public_keys.items(): - if v == xpub: - account_id = re.match("x/(\d+)'", k).group(1) - account_derivation = "44'/0'/%s'"%account_id - if account_derivation is not None: - xpub_path[xpub] = account_derivation - - self.plugin.sign_transaction(tx, prev_tx, xpub_path) - - def check_proper_device(self): - self.get_client().ping('t') - if not self.device_checked: - address = self.addresses(False)[0] - address_id = self.address_id(address) - n = self.get_client().expand_path(address_id) - device_address = self.get_client().get_address('Bitcoin', n) - self.device_checked = True - self.proper_device = (device_address == address) - - return self.proper_device - - - -class Plugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self._is_available = self._init() - self.wallet = None - self.handler = None - self.client = None - self.transport = None - - def constructor(self, s): - return TrezorWallet(s) - - def _init(self): - return TREZOR - - def is_available(self): - if not self._is_available: - return False - if not self.wallet: - return False - if self.wallet.storage.get('wallet_type') != 'trezor': - return False - return True - - def set_enabled(self, enabled): - self.wallet.storage.put('use_' + self.name, enabled) - - def is_enabled(self): - if not self.is_available(): - return False - if self.wallet.has_seed(): - return False - return True - - def compare_version(self, major, minor=0, patch=0): - features = self.get_client().features - v = [features.major_version, features.minor_version, features.patch_version] - self.print_error('firmware version', v) - return cmp(v, [major, minor, patch]) - - def atleast_version(self, major, minor=0, patch=0): - return self.compare_version(major, minor, patch) >= 0 - - def get_client(self): - if not TREZOR: - give_error('please install github.com/trezor/python-trezor') - - if not self.client or self.client.bad: - d = HidTransport.enumerate() - if not d: - give_error('Could not connect to your Trezor. Please verify the cable is connected and that no other app is using it.') - self.transport = HidTransport(d[0]) - self.client = QtGuiTrezorClient(self.transport) - self.client.handler = self.handler - self.client.set_tx_api(self) - self.client.bad = False - if not self.atleast_version(1, 2, 1): - self.client = None - give_error('Outdated Trezor firmware. Please update the firmware from https://www.mytrezor.com') - return self.client - - @hook - def close_wallet(self): - print_error("trezor: clear session") - if self.client: - self.client.clear_session() - self.client.transport.close() - self.client = None - self.wallet = None - - @hook - def cmdline_load_wallet(self, wallet): - self.wallet = wallet - self.wallet.plugin = self - if self.handler is None: - self.handler = TrezorCmdLineHandler() - - def sign_transaction(self, tx, prev_tx, xpub_path): - self.prev_tx = prev_tx - self.xpub_path = xpub_path - client = self.get_client() - inputs = self.tx_inputs(tx, True) - outputs = self.tx_outputs(tx) - #try: - signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] - #except Exception, e: - # give_error(e) - #finally: - self.handler.stop() - - raw = signed_tx.encode('hex') - tx.update_signatures(raw) - - - def tx_inputs(self, tx, for_sig=False): - inputs = [] - for txin in tx.inputs: - txinputtype = types.TxInputType() - if txin.get('is_coinbase'): - prev_hash = "\0"*32 - prev_index = 0xffffffff # signed int -1 - else: - if for_sig: - x_pubkeys = txin['x_pubkeys'] - if len(x_pubkeys) == 1: - x_pubkey = x_pubkeys[0] - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - else: - def f(x_pubkey): - if is_extended_pubkey(x_pubkey): - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - else: - xpub = xpub_from_pubkey(x_pubkey.decode('hex')) - s = [] - node = ckd_public.deserialize(xpub) - return types.HDNodePathType(node=node, address_n=s) - pubkeys = map(f, x_pubkeys) - multisig = types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), - m=txin.get('num_sig'), - ) - txinputtype = types.TxInputType( - script_type=types.SPENDMULTISIG, - multisig= multisig - ) - # find which key is mine - for x_pubkey in x_pubkeys: - if is_extended_pubkey(x_pubkey): - xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) - if xpub in self.xpub_path: - xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - break - - prev_hash = unhexlify(txin['prevout_hash']) - prev_index = txin['prevout_n'] - - txinputtype.prev_hash = prev_hash - txinputtype.prev_index = prev_index - - if 'scriptSig' in txin: - script_sig = txin['scriptSig'].decode('hex') - txinputtype.script_sig = script_sig - - if 'sequence' in txin: - sequence = txin['sequence'] - txinputtype.sequence = sequence - - inputs.append(txinputtype) - - return inputs - - def tx_outputs(self, tx): - outputs = [] - - for type, address, amount in tx.outputs: - assert type == 'address' - txoutputtype = types.TxOutputType() - if self.wallet.is_change(address): - address_path = self.wallet.address_id(address) - address_n = self.get_client().expand_path(address_path) - txoutputtype.address_n.extend(address_n) - else: - txoutputtype.address = address - txoutputtype.amount = amount - addrtype, hash_160 = bc_address_to_hash_160(address) - if addrtype == 0: - txoutputtype.script_type = types.PAYTOADDRESS - elif addrtype == 5: - txoutputtype.script_type = types.PAYTOSCRIPTHASH - else: - raise BaseException('addrtype') - outputs.append(txoutputtype) - - return outputs - - def electrum_tx_to_txtype(self, tx): - t = types.TransactionType() - d = deserialize(tx.raw) - t.version = d['version'] - t.lock_time = d['lockTime'] - inputs = self.tx_inputs(tx) - t.inputs.extend(inputs) - for vout in d['outputs']: - o = t.bin_outputs.add() - o.amount = vout['value'] - o.script_pubkey = vout['scriptPubKey'].decode('hex') - return t - - def get_tx(self, tx_hash): - tx = self.prev_tx[tx_hash] - tx.deserialize() - return self.electrum_tx_to_txtype(tx) - - -from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton -import PyQt4.QtCore as QtCore -from electrum_gui.qt.util import * -from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow -from electrum_gui.qt.installwizard import InstallWizard -from trezorlib.qt.pinmatrix import PinMatrixWidget - -class QtPlugin(Plugin): - - @hook - def load_wallet(self, wallet, window): - self.print_error("load_wallet") - self.wallet = wallet - self.wallet.plugin = self - self.trezor_button = StatusBarButton(QIcon(":icons/trezor.png"), _("Trezor"), partial(self.settings_dialog, window)) - if type(window) is ElectrumWindow: - window.statusBar().addPermanentWidget(self.trezor_button) - if self.handler is None: - self.handler = TrezorQtHandler(window) - try: - self.get_client().ping('t') - except BaseException as e: - QMessageBox.information(window, _('Error'), _("Trezor device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) - self.wallet.force_watching_only = True - return - if self.wallet.addresses() and not self.wallet.check_proper_device(): - QMessageBox.information(window, _('Error'), _("This wallet does not match your Trezor device"), _('OK')) - self.wallet.force_watching_only = True - - @hook - def installwizard_load_wallet(self, wallet, window): - if type(wallet) != TrezorWallet: - return - self.load_wallet(wallet, window) - - @hook - def installwizard_restore(self, wizard, storage): - if storage.get('wallet_type') != 'trezor': - return - seed = wizard.enter_seed_dialog("Enter your Trezor seed", None, func=lambda x:True) - if not seed: - return - wallet = TrezorWallet(storage) - self.wallet = wallet - handler = TrezorQtHandler(wizard) - passphrase = handler.get_passphrase(_("Please enter your Trezor passphrase.") + '\n' + _("Press OK if you do not use one.")) - if passphrase is None: - return - password = wizard.password_dialog() - wallet.add_seed(seed, password) - wallet.add_cosigner_seed(seed, 'x/', password, passphrase) - wallet.create_main_account(password) - # disable trezor plugin - self.set_enabled(False) - return wallet - - @hook - def receive_menu(self, menu, addrs): - if not self.wallet.is_watching_only() and self.atleast_version(1, 3) and len(addrs) == 1: - menu.addAction(_("Show on TREZOR"), lambda: self.show_address(addrs[0])) - - def show_address(self, address): - if not self.wallet.check_proper_device(): - give_error('Wrong device or password') - try: - address_path = self.wallet.address_id(address) - address_n = self.get_client().expand_path(address_path) - except Exception, e: - give_error(e) - try: - self.get_client().get_address('Bitcoin', address_n, True) - except Exception, e: - give_error(e) - finally: - self.handler.stop() - - - def settings_dialog(self, window): - try: - device_id = self.get_client().get_device_id() - except BaseException as e: - window.show_message(str(e)) - return - get_label = lambda: self.get_client().features.label - update_label = lambda: current_label_label.setText("Label: %s" % get_label()) - d = QDialog() - layout = QGridLayout(d) - layout.addWidget(QLabel("Trezor Options"),0,0) - layout.addWidget(QLabel("ID:"),1,0) - layout.addWidget(QLabel(" %s" % device_id),1,1) - - def modify_label(): - response = QInputDialog().getText(None, "Set New Trezor Label", "New Trezor Label: (upon submission confirm on Trezor)") - if not response[1]: - return - new_label = str(response[0]) - self.handler.show_message("Please confirm label change on Trezor") - status = self.get_client().apply_settings(label=new_label) - self.handler.stop() - update_label() - - current_label_label = QLabel() - update_label() - change_label_button = QPushButton("Modify") - change_label_button.clicked.connect(modify_label) - layout.addWidget(current_label_label,3,0) - layout.addWidget(change_label_button,3,1) - d.exec_() - - - - - - - -class TrezorGuiMixin(object): - - def __init__(self, *args, **kwargs): - super(TrezorGuiMixin, self).__init__(*args, **kwargs) - - def callback_ButtonRequest(self, msg): - if msg.code == 3: - message = "Confirm transaction outputs on Trezor device to continue" - elif msg.code == 8: - message = "Confirm transaction fee on Trezor device to continue" - elif msg.code == 7: - message = "Confirm message to sign on Trezor device to continue" - elif msg.code == 10: - message = "Confirm address on Trezor device to continue" - else: - message = "Check Trezor device to continue" - self.handler.show_message(message) - return proto.ButtonAck() - - def callback_PinMatrixRequest(self, msg): - if msg.type == 1: - desc = 'current PIN' - elif msg.type == 2: - desc = 'new PIN' - elif msg.type == 3: - desc = 'new PIN again' - else: - desc = 'PIN' - pin = self.handler.get_pin("Please enter Trezor %s" % desc) - if not pin: - return proto.Cancel() - return proto.PinMatrixAck(pin=pin) - - def callback_PassphraseRequest(self, req): - msg = _("Please enter your Trezor passphrase.") - passphrase = self.handler.get_passphrase(msg) - if passphrase is None: - return proto.Cancel() - return proto.PassphraseAck(passphrase=passphrase) - - def callback_WordRequest(self, msg): - #TODO - log("Enter one word of mnemonic: ") - word = raw_input() - return proto.WordAck(word=word) - - -class TrezorCmdLineHandler: - - def get_passphrase(self, msg): - import getpass - print_msg(msg) - return getpass.getpass('') - - def get_pin(self, msg): - t = { 'a':'7', 'b':'8', 'c':'9', 'd':'4', 'e':'5', 'f':'6', 'g':'1', 'h':'2', 'i':'3'} - print_msg(msg) - print_msg("a b c\nd e f\ng h i\n-----") - o = raw_input() - return ''.join(map(lambda x: t[x], o)) - - def stop(self): - pass - - def show_message(self, msg): - print_msg(msg) - - -class TrezorQtHandler: - - def __init__(self, win): - self.win = win - self.win.connect(win, SIGNAL('trezor_done'), self.dialog_stop) - self.win.connect(win, SIGNAL('message_dialog'), self.message_dialog) - self.win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog) - self.win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog) - self.done = threading.Event() - - def stop(self): - self.win.emit(SIGNAL('trezor_done')) - - def show_message(self, msg): - self.message = msg - self.win.emit(SIGNAL('message_dialog')) - - def get_pin(self, msg): - self.done.clear() - self.message = msg - self.win.emit(SIGNAL('pin_dialog')) - self.done.wait() - return self.response - - def get_passphrase(self, msg): - self.done.clear() - self.message = msg - self.win.emit(SIGNAL('passphrase_dialog')) - self.done.wait() - return self.passphrase - - def pin_dialog(self): - d = QDialog(None) - d.setModal(1) - d.setWindowTitle(_("Enter PIN")) - d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) - matrix = PinMatrixWidget() - vbox = QVBoxLayout() - vbox.addWidget(QLabel(self.message)) - vbox.addWidget(matrix) - vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) - d.setLayout(vbox) - if not d.exec_(): - self.response = None - self.response = str(matrix.get_value()) - self.done.set() - - def passphrase_dialog(self): - if type(self.win) is ElectrumWindow: - passphrase = self.win.password_dialog(_("Please enter your Trezor passphrase")) - self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' - else: - assert type(self.win) is InstallWizard - from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog - d = QDialog() - d.setModal(1) - d.setLayout(make_password_dialog(d, None, self.message, False)) - confirmed, p, passphrase = run_password_dialog(d, None, None) - if not confirmed: - QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) - self.passphrase = None - else: - self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' - self.done.set() - - def message_dialog(self): - self.d = QDialog() - self.d.setModal(1) - self.d.setWindowTitle('Please Check Trezor Device') - self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) - l = QLabel(self.message) - vbox = QVBoxLayout(self.d) - vbox.addWidget(l) - self.d.show() - - def dialog_stop(self): - self.d.hide() - - -if TREZOR: - class QtGuiTrezorClient(ProtocolMixin, TrezorGuiMixin, BaseClient): - def call_raw(self, msg): - try: - resp = BaseClient.call_raw(self, msg) - except ConnectionError: - self.bad = True - raise - - return resp diff --git a/plugins/trezor/__init__.py b/plugins/trezor/__init__.py @@ -0,0 +1,9 @@ +from electrum.i18n import _ + +fullname = 'Trezor Wallet' +description = _('Provides support for Trezor hardware wallet') +requires = [('trezorlib','github.com/trezor/python-trezor')] +requires_wallet_type = ['trezor'] +registers_wallet_type = ('hardware', 'trezor', _("Trezor wallet")) +available_for = ['qt', 'cmdline'] + diff --git a/plugins/trezor/qt.py b/plugins/trezor/qt.py @@ -0,0 +1,200 @@ +from PyQt4.Qt import QMessageBox, QDialog, QVBoxLayout, QLabel, QThread, SIGNAL, QGridLayout, QInputDialog, QPushButton +import PyQt4.QtCore as QtCore +from electrum_gui.qt.util import * +from electrum_gui.qt.main_window import StatusBarButton, ElectrumWindow +from electrum_gui.qt.installwizard import InstallWizard +from trezorlib.qt.pinmatrix import PinMatrixWidget + + +from functools import partial +import unicodedata + +from electrum.i18n import _ +from electrum.plugins import hook, always_hook, run_hook + +from trezor import TrezorPlugin + +class TrezorQtHandler: + + def __init__(self, win): + self.win = win + self.win.connect(win, SIGNAL('trezor_done'), self.dialog_stop) + self.win.connect(win, SIGNAL('message_dialog'), self.message_dialog) + self.win.connect(win, SIGNAL('pin_dialog'), self.pin_dialog) + self.win.connect(win, SIGNAL('passphrase_dialog'), self.passphrase_dialog) + self.done = threading.Event() + + def stop(self): + self.win.emit(SIGNAL('trezor_done')) + + def show_message(self, msg): + self.message = msg + self.win.emit(SIGNAL('message_dialog')) + + def get_pin(self, msg): + self.done.clear() + self.message = msg + self.win.emit(SIGNAL('pin_dialog')) + self.done.wait() + return self.response + + def get_passphrase(self, msg): + self.done.clear() + self.message = msg + self.win.emit(SIGNAL('passphrase_dialog')) + self.done.wait() + return self.passphrase + + def pin_dialog(self): + d = QDialog(None) + d.setModal(1) + d.setWindowTitle(_("Enter PIN")) + d.setWindowFlags(d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) + matrix = PinMatrixWidget() + vbox = QVBoxLayout() + vbox.addWidget(QLabel(self.message)) + vbox.addWidget(matrix) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + d.setLayout(vbox) + if not d.exec_(): + self.response = None + self.response = str(matrix.get_value()) + self.done.set() + + def passphrase_dialog(self): + if type(self.win) is ElectrumWindow: + passphrase = self.win.password_dialog(_("Please enter your Trezor passphrase")) + self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' + else: + assert type(self.win) is InstallWizard + from electrum_gui.qt.password_dialog import make_password_dialog, run_password_dialog + d = QDialog() + d.setModal(1) + d.setLayout(make_password_dialog(d, None, self.message, False)) + confirmed, p, passphrase = run_password_dialog(d, None, None) + if not confirmed: + QMessageBox.critical(None, _('Error'), _("Password request canceled"), _('OK')) + self.passphrase = None + else: + self.passphrase = unicodedata.normalize('NFKD', unicode(passphrase)) if passphrase else '' + self.done.set() + + def message_dialog(self): + self.d = QDialog() + self.d.setModal(1) + self.d.setWindowTitle('Please Check Trezor Device') + self.d.setWindowFlags(self.d.windowFlags() | QtCore.Qt.WindowStaysOnTopHint) + l = QLabel(self.message) + vbox = QVBoxLayout(self.d) + vbox.addWidget(l) + self.d.show() + + def dialog_stop(self): + self.d.hide() + + +class Plugin(TrezorPlugin): + + @hook + def load_wallet(self, wallet, window): + self.print_error("load_wallet") + self.wallet = wallet + self.wallet.plugin = self + self.trezor_button = StatusBarButton(QIcon(":icons/trezor.png"), _("Trezor"), partial(self.settings_dialog, window)) + if type(window) is ElectrumWindow: + window.statusBar().addPermanentWidget(self.trezor_button) + if self.handler is None: + self.handler = TrezorQtHandler(window) + try: + self.get_client().ping('t') + except BaseException as e: + QMessageBox.information(window, _('Error'), _("Trezor device not detected.\nContinuing in watching-only mode." + '\n\nReason:\n' + str(e)), _('OK')) + self.wallet.force_watching_only = True + return + if self.wallet.addresses() and not self.wallet.check_proper_device(): + QMessageBox.information(window, _('Error'), _("This wallet does not match your Trezor device"), _('OK')) + self.wallet.force_watching_only = True + + @hook + def installwizard_load_wallet(self, wallet, window): + if type(wallet) != TrezorWallet: + return + self.load_wallet(wallet, window) + + @hook + def installwizard_restore(self, wizard, storage): + if storage.get('wallet_type') != 'trezor': + return + seed = wizard.enter_seed_dialog("Enter your Trezor seed", None, func=lambda x:True) + if not seed: + return + wallet = TrezorWallet(storage) + self.wallet = wallet + handler = TrezorQtHandler(wizard) + passphrase = handler.get_passphrase(_("Please enter your Trezor passphrase.") + '\n' + _("Press OK if you do not use one.")) + if passphrase is None: + return + password = wizard.password_dialog() + wallet.add_seed(seed, password) + wallet.add_cosigner_seed(seed, 'x/', password, passphrase) + wallet.create_main_account(password) + # disable trezor plugin + self.set_enabled(False) + return wallet + + @hook + def receive_menu(self, menu, addrs): + if not self.wallet.is_watching_only() and self.atleast_version(1, 3) and len(addrs) == 1: + menu.addAction(_("Show on TREZOR"), lambda: self.show_address(addrs[0])) + + def show_address(self, address): + if not self.wallet.check_proper_device(): + give_error('Wrong device or password') + try: + address_path = self.wallet.address_id(address) + address_n = self.get_client().expand_path(address_path) + except Exception, e: + give_error(e) + try: + self.get_client().get_address('Bitcoin', address_n, True) + except Exception, e: + give_error(e) + finally: + self.handler.stop() + + + def settings_dialog(self, window): + try: + device_id = self.get_client().get_device_id() + except BaseException as e: + window.show_message(str(e)) + return + get_label = lambda: self.get_client().features.label + update_label = lambda: current_label_label.setText("Label: %s" % get_label()) + d = QDialog() + layout = QGridLayout(d) + layout.addWidget(QLabel("Trezor Options"),0,0) + layout.addWidget(QLabel("ID:"),1,0) + layout.addWidget(QLabel(" %s" % device_id),1,1) + + def modify_label(): + response = QInputDialog().getText(None, "Set New Trezor Label", "New Trezor Label: (upon submission confirm on Trezor)") + if not response[1]: + return + new_label = str(response[0]) + self.handler.show_message("Please confirm label change on Trezor") + status = self.get_client().apply_settings(label=new_label) + self.handler.stop() + update_label() + + current_label_label = QLabel() + update_label() + change_label_button = QPushButton("Modify") + change_label_button.clicked.connect(modify_label) + layout.addWidget(current_label_label,3,0) + layout.addWidget(change_label_button,3,1) + d.exec_() + + + + diff --git a/plugins/trezor/trezor.py b/plugins/trezor/trezor.py @@ -0,0 +1,486 @@ +from binascii import unhexlify +from struct import pack +from sys import stderr +from time import sleep +import unicodedata +import threading +import re + + +import electrum +from electrum import bitcoin + +from electrum.account import BIP32_Account +from electrum.bitcoin import EncodeBase58Check, public_key_to_bc_address, bc_address_to_hash_160, xpub_from_pubkey +from electrum.i18n import _ +from electrum.plugins import BasePlugin, hook, always_hook, run_hook +from electrum.transaction import Transaction, deserialize, is_extended_pubkey, x_to_xpub +from electrum.wallet import BIP32_HD_Wallet +from electrum.util import print_error, print_msg +from electrum.wallet import pw_decode, bip32_private_derivation, bip32_root + +try: + from trezorlib.client import types + from trezorlib.client import proto, BaseClient, ProtocolMixin + from trezorlib.transport import ConnectionError + from trezorlib.transport_hid import HidTransport + TREZOR = True +except ImportError: + TREZOR = False + +import trezorlib.ckd_public as ckd_public + + + + +def log(msg): + stderr.write("%s\n" % msg) + stderr.flush() + +def give_error(message): + print_error(message) + raise Exception(message) + + +class TrezorWallet(BIP32_HD_Wallet): + wallet_type = 'trezor' + root_derivation = "m/44'/0'" + + def __init__(self, storage): + BIP32_HD_Wallet.__init__(self, storage) + self.mpk = None + self.device_checked = False + self.proper_device = False + self.force_watching_only = False + + def get_action(self): + if not self.accounts: + return 'create_accounts' + + def can_import(self): + return False + + def can_sign_xpubkey(self, x_pubkey): + xpub, sequence = BIP32_Account.parse_xpubkey(x_pubkey) + return xpub in self.master_public_keys.values() + + def can_export(self): + return False + + def can_create_accounts(self): + return True + + def can_change_password(self): + return False + + def is_watching_only(self): + return self.force_watching_only + + def get_client(self): + return self.plugin.get_client() + + def address_id(self, address): + account_id, (change, address_index) = self.get_address_index(address) + return "44'/0'/%s'/%d/%d" % (account_id, change, address_index) + + def create_main_account(self, password): + self.create_account('Main account', None) #name, empty password + + def mnemonic_to_seed(self, mnemonic, passphrase): + # trezor uses bip39 + import pbkdf2, hashlib, hmac + PBKDF2_ROUNDS = 2048 + mnemonic = unicodedata.normalize('NFKD', ' '.join(mnemonic.split())) + passphrase = unicodedata.normalize('NFKD', passphrase) + return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, iterations = PBKDF2_ROUNDS, macmodule = hmac, digestmodule = hashlib.sha512).read(64) + + def derive_xkeys(self, root, derivation, password): + x = self.master_private_keys.get(root) + if x: + root_xprv = pw_decode(x, password) + xprv, xpub = bip32_private_derivation(root_xprv, root, derivation) + return xpub, xprv + else: + derivation = derivation.replace(self.root_name,"44'/0'/") + xpub = self.get_public_key(derivation) + return xpub, None + + def get_public_key(self, bip32_path): + address_n = self.plugin.get_client().expand_path(bip32_path) + node = self.plugin.get_client().get_public_node(address_n).node + xpub = "0488B21E".decode('hex') + chr(node.depth) + self.i4b(node.fingerprint) + self.i4b(node.child_num) + node.chain_code + node.public_key + return EncodeBase58Check(xpub) + + def get_master_public_key(self): + if not self.mpk: + self.mpk = self.get_public_key("44'/0'") + return self.mpk + + def i4b(self, x): + return pack('>I', x) + + def add_keypairs(self, tx, keypairs, password): + #do nothing - no priv keys available + pass + + def decrypt_message(self, pubkey, message, password): + raise BaseException( _('Decrypt method is not implemented in Trezor') ) + #address = public_key_to_bc_address(pubkey.decode('hex')) + #address_path = self.address_id(address) + #address_n = self.get_client().expand_path(address_path) + #try: + # decrypted_msg = self.get_client().decrypt_message(address_n, b64decode(message)) + #except Exception, e: + # give_error(e) + #finally: + # twd.stop() + #return str(decrypted_msg) + + def sign_message(self, address, message, password): + if self.has_seed(): + return BIP32_HD_Wallet.sign_message(self, address, message, password) + if not self.check_proper_device(): + give_error('Wrong device or password') + try: + address_path = self.address_id(address) + address_n = self.plugin.get_client().expand_path(address_path) + except Exception, e: + give_error(e) + try: + msg_sig = self.plugin.get_client().sign_message('Bitcoin', address_n, message) + except Exception, e: + give_error(e) + finally: + self.plugin.handler.stop() + return msg_sig.signature + + def sign_transaction(self, tx, password): + if self.has_seed(): + return BIP32_HD_Wallet.sign_transaction(self, tx, password) + if tx.is_complete(): + return + if not self.check_proper_device(): + give_error('Wrong device or password') + # previous transactions used as inputs + prev_tx = {} + # path of the xpubs that are involved + xpub_path = {} + for txin in tx.inputs: + tx_hash = txin['prevout_hash'] + + ptx = self.transactions.get(tx_hash) + if ptx is None: + ptx = self.network.synchronous_get(('blockchain.transaction.get', [tx_hash])) + ptx = Transaction(ptx) + prev_tx[tx_hash] = ptx + + for x_pubkey in txin['x_pubkeys']: + account_derivation = None + if not is_extended_pubkey(x_pubkey): + continue + xpub = x_to_xpub(x_pubkey) + for k, v in self.master_public_keys.items(): + if v == xpub: + account_id = re.match("x/(\d+)'", k).group(1) + account_derivation = "44'/0'/%s'"%account_id + if account_derivation is not None: + xpub_path[xpub] = account_derivation + + self.plugin.sign_transaction(tx, prev_tx, xpub_path) + + def check_proper_device(self): + self.get_client().ping('t') + if not self.device_checked: + address = self.addresses(False)[0] + address_id = self.address_id(address) + n = self.get_client().expand_path(address_id) + device_address = self.get_client().get_address('Bitcoin', n) + self.device_checked = True + self.proper_device = (device_address == address) + + return self.proper_device + + + +class TrezorPlugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self._is_available = self._init() + self.wallet = None + self.handler = None + self.client = None + self.transport = None + + def constructor(self, s): + return TrezorWallet(s) + + def _init(self): + return TREZOR + + def is_available(self): + if not self._is_available: + return False + if not self.wallet: + return False + if self.wallet.storage.get('wallet_type') != 'trezor': + return False + return True + + def set_enabled(self, enabled): + self.wallet.storage.put('use_' + self.name, enabled) + + def is_enabled(self): + if not self.is_available(): + return False + if self.wallet.has_seed(): + return False + return True + + def compare_version(self, major, minor=0, patch=0): + features = self.get_client().features + v = [features.major_version, features.minor_version, features.patch_version] + self.print_error('firmware version', v) + return cmp(v, [major, minor, patch]) + + def atleast_version(self, major, minor=0, patch=0): + return self.compare_version(major, minor, patch) >= 0 + + def get_client(self): + if not TREZOR: + give_error('please install github.com/trezor/python-trezor') + + if not self.client or self.client.bad: + d = HidTransport.enumerate() + if not d: + give_error('Could not connect to your Trezor. Please verify the cable is connected and that no other app is using it.') + self.transport = HidTransport(d[0]) + self.client = QtGuiTrezorClient(self.transport) + self.client.handler = self.handler + self.client.set_tx_api(self) + self.client.bad = False + if not self.atleast_version(1, 2, 1): + self.client = None + give_error('Outdated Trezor firmware. Please update the firmware from https://www.mytrezor.com') + return self.client + + @hook + def close_wallet(self): + print_error("trezor: clear session") + if self.client: + self.client.clear_session() + self.client.transport.close() + self.client = None + self.wallet = None + + @hook + def cmdline_load_wallet(self, wallet): + self.wallet = wallet + self.wallet.plugin = self + if self.handler is None: + self.handler = TrezorCmdLineHandler() + + def sign_transaction(self, tx, prev_tx, xpub_path): + self.prev_tx = prev_tx + self.xpub_path = xpub_path + client = self.get_client() + inputs = self.tx_inputs(tx, True) + outputs = self.tx_outputs(tx) + #try: + signed_tx = client.sign_tx('Bitcoin', inputs, outputs)[1] + #except Exception, e: + # give_error(e) + #finally: + self.handler.stop() + + raw = signed_tx.encode('hex') + tx.update_signatures(raw) + + + def tx_inputs(self, tx, for_sig=False): + inputs = [] + for txin in tx.inputs: + txinputtype = types.TxInputType() + if txin.get('is_coinbase'): + prev_hash = "\0"*32 + prev_index = 0xffffffff # signed int -1 + else: + if for_sig: + x_pubkeys = txin['x_pubkeys'] + if len(x_pubkeys) == 1: + x_pubkey = x_pubkeys[0] + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + else: + def f(x_pubkey): + if is_extended_pubkey(x_pubkey): + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + else: + xpub = xpub_from_pubkey(x_pubkey.decode('hex')) + s = [] + node = ckd_public.deserialize(xpub) + return types.HDNodePathType(node=node, address_n=s) + pubkeys = map(f, x_pubkeys) + multisig = types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=map(lambda x: x.decode('hex') if x else '', txin.get('signatures')), + m=txin.get('num_sig'), + ) + txinputtype = types.TxInputType( + script_type=types.SPENDMULTISIG, + multisig= multisig + ) + # find which key is mine + for x_pubkey in x_pubkeys: + if is_extended_pubkey(x_pubkey): + xpub, s = BIP32_Account.parse_xpubkey(x_pubkey) + if xpub in self.xpub_path: + xpub_n = self.get_client().expand_path(self.xpub_path[xpub]) + txinputtype.address_n.extend(xpub_n + s) + break + + prev_hash = unhexlify(txin['prevout_hash']) + prev_index = txin['prevout_n'] + + txinputtype.prev_hash = prev_hash + txinputtype.prev_index = prev_index + + if 'scriptSig' in txin: + script_sig = txin['scriptSig'].decode('hex') + txinputtype.script_sig = script_sig + + if 'sequence' in txin: + sequence = txin['sequence'] + txinputtype.sequence = sequence + + inputs.append(txinputtype) + + return inputs + + def tx_outputs(self, tx): + outputs = [] + + for type, address, amount in tx.outputs: + assert type == 'address' + txoutputtype = types.TxOutputType() + if self.wallet.is_change(address): + address_path = self.wallet.address_id(address) + address_n = self.get_client().expand_path(address_path) + txoutputtype.address_n.extend(address_n) + else: + txoutputtype.address = address + txoutputtype.amount = amount + addrtype, hash_160 = bc_address_to_hash_160(address) + if addrtype == 0: + txoutputtype.script_type = types.PAYTOADDRESS + elif addrtype == 5: + txoutputtype.script_type = types.PAYTOSCRIPTHASH + else: + raise BaseException('addrtype') + outputs.append(txoutputtype) + + return outputs + + def electrum_tx_to_txtype(self, tx): + t = types.TransactionType() + d = deserialize(tx.raw) + t.version = d['version'] + t.lock_time = d['lockTime'] + inputs = self.tx_inputs(tx) + t.inputs.extend(inputs) + for vout in d['outputs']: + o = t.bin_outputs.add() + o.amount = vout['value'] + o.script_pubkey = vout['scriptPubKey'].decode('hex') + return t + + def get_tx(self, tx_hash): + tx = self.prev_tx[tx_hash] + tx.deserialize() + return self.electrum_tx_to_txtype(tx) + + + + + +class TrezorGuiMixin(object): + + def __init__(self, *args, **kwargs): + super(TrezorGuiMixin, self).__init__(*args, **kwargs) + + def callback_ButtonRequest(self, msg): + if msg.code == 3: + message = "Confirm transaction outputs on Trezor device to continue" + elif msg.code == 8: + message = "Confirm transaction fee on Trezor device to continue" + elif msg.code == 7: + message = "Confirm message to sign on Trezor device to continue" + elif msg.code == 10: + message = "Confirm address on Trezor device to continue" + else: + message = "Check Trezor device to continue" + self.handler.show_message(message) + return proto.ButtonAck() + + def callback_PinMatrixRequest(self, msg): + if msg.type == 1: + desc = 'current PIN' + elif msg.type == 2: + desc = 'new PIN' + elif msg.type == 3: + desc = 'new PIN again' + else: + desc = 'PIN' + pin = self.handler.get_pin("Please enter Trezor %s" % desc) + if not pin: + return proto.Cancel() + return proto.PinMatrixAck(pin=pin) + + def callback_PassphraseRequest(self, req): + msg = _("Please enter your Trezor passphrase.") + passphrase = self.handler.get_passphrase(msg) + if passphrase is None: + return proto.Cancel() + return proto.PassphraseAck(passphrase=passphrase) + + def callback_WordRequest(self, msg): + #TODO + log("Enter one word of mnemonic: ") + word = raw_input() + return proto.WordAck(word=word) + + +class TrezorCmdLineHandler: + + def get_passphrase(self, msg): + import getpass + print_msg(msg) + return getpass.getpass('') + + def get_pin(self, msg): + t = { 'a':'7', 'b':'8', 'c':'9', 'd':'4', 'e':'5', 'f':'6', 'g':'1', 'h':'2', 'i':'3'} + print_msg(msg) + print_msg("a b c\nd e f\ng h i\n-----") + o = raw_input() + return ''.join(map(lambda x: t[x], o)) + + def stop(self): + pass + + def show_message(self, msg): + print_msg(msg) + + + + +if TREZOR: + class QtGuiTrezorClient(ProtocolMixin, TrezorGuiMixin, BaseClient): + def call_raw(self, msg): + try: + resp = BaseClient.call_raw(self, msg) + except ConnectionError: + self.bad = True + raise + + return resp diff --git a/plugins/trustedcoin.py b/plugins/trustedcoin.py @@ -1,681 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - Lightweight Bitcoin Client -# Copyright (C) 2015 Thomas Voegtlin -# -# 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/>. - -from threading import Thread -import socket -import os -import re -import requests -import json -from hashlib import sha256 -from urlparse import urljoin -from urllib import quote -from functools import partial - -import electrum -from electrum import bitcoin -from electrum.bitcoin import * -from electrum.mnemonic import Mnemonic -from electrum import version -from electrum.wallet import Multisig_Wallet, BIP32_Wallet -from electrum.i18n import _ -from electrum.plugins import BasePlugin, run_hook, hook - - -from decimal import Decimal - -# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server -signing_xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" -billing_xpub = "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU" - -SEED_PREFIX = version.SEED_PREFIX_2FA - - -class TrustedCoinException(Exception): - def __init__(self, message, status_code=0): - Exception.__init__(self, message) - self.status_code = status_code - -class TrustedCoinCosignerClient(object): - def __init__(self, user_agent=None, base_url='https://api.trustedcoin.com/2/', debug=False): - self.base_url = base_url - self.debug = debug - self.user_agent = user_agent - - def send_request(self, method, relative_url, data=None): - kwargs = {'headers': {}} - if self.user_agent: - kwargs['headers']['user-agent'] = self.user_agent - if method == 'get' and data: - kwargs['params'] = data - elif method == 'post' and data: - kwargs['data'] = json.dumps(data) - kwargs['headers']['content-type'] = 'application/json' - url = urljoin(self.base_url, relative_url) - if self.debug: - print '%s %s %s' % (method, url, data) - response = requests.request(method, url, **kwargs) - if self.debug: - print response.text - print - if response.status_code != 200: - message = str(response.text) - if response.headers.get('content-type') == 'application/json': - r = response.json() - if 'message' in r: - message = r['message'] - raise TrustedCoinException(message, response.status_code) - if response.headers.get('content-type') == 'application/json': - return response.json() - else: - return response.text - - def get_terms_of_service(self, billing_plan='electrum-per-tx-otp'): - """ - Returns the TOS for the given billing plan as a plain/text unicode string. - :param billing_plan: the plan to return the terms for - """ - payload = {'billing_plan': billing_plan} - return self.send_request('get', 'tos', payload) - - def create(self, xpubkey1, xpubkey2, email, billing_plan='electrum-per-tx-otp'): - """ - Creates a new cosigner resource. - :param xpubkey1: a bip32 extended public key (customarily the hot key) - :param xpubkey2: a bip32 extended public key (customarily the cold key) - :param email: a contact email - :param billing_plan: the billing plan for the cosigner - """ - payload = { - 'email': email, - 'xpubkey1': xpubkey1, - 'xpubkey2': xpubkey2, - 'billing_plan': billing_plan, - } - return self.send_request('post', 'cosigner', payload) - - def auth(self, id, otp): - """ - Attempt to authenticate for a particular cosigner. - :param id: the id of the cosigner - :param otp: the one time password - """ - payload = {'otp': otp} - return self.send_request('post', 'cosigner/%s/auth' % quote(id), payload) - - def get(self, id): - """ - Attempt to authenticate for a particular cosigner. - :param id: the id of the cosigner - :param otp: the one time password - """ - return self.send_request('get', 'cosigner/%s' % quote(id)) - - def sign(self, id, transaction, otp): - """ - Attempt to authenticate for a particular cosigner. - :param id: the id of the cosigner - :param transaction: the hex encoded [partially signed] compact transaction to sign - :param otp: the one time password - """ - payload = { - 'otp': otp, - 'transaction': transaction - } - return self.send_request('post', 'cosigner/%s/sign' % quote(id), payload) - - def transfer_credit(self, id, recipient, otp, signature_callback): - """ - Tranfer a cosigner's credits to another cosigner. - :param id: the id of the sending cosigner - :param recipient: the id of the recipient cosigner - :param otp: the one time password (of the sender) - :param signature_callback: a callback that signs a text message using xpubkey1/0/0 returning a compact sig - """ - payload = { - 'otp': otp, - 'recipient': recipient, - 'timestamp': int(time.time()), - - } - relative_url = 'cosigner/%s/transfer' % quote(id) - full_url = urljoin(self.base_url, relative_url) - headers = { - 'x-signature': signature_callback(full_url + '\n' + json.dumps(payload)) - } - return self.send_request('post', relative_url, payload, headers) - - -server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VERSION) - -class Wallet_2fa(Multisig_Wallet): - - def __init__(self, storage): - BIP32_Wallet.__init__(self, storage) - self.wallet_type = '2fa' - self.m = 2 - self.n = 3 - self.is_billing = False - self.billing_info = None - - def get_action(self): - xpub1 = self.master_public_keys.get("x1/") - xpub2 = self.master_public_keys.get("x2/") - xpub3 = self.master_public_keys.get("x3/") - if xpub2 is None and not self.storage.get('use_trustedcoin'): - return 'show_disclaimer' - if xpub2 is None: - return 'create_extended_seed' - if xpub3 is None: - return 'create_remote_key' - if not self.accounts: - return 'create_accounts' - - def make_seed(self): - return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX) - - def can_sign_without_server(self): - return self.master_private_keys.get('x2/') is not None - - def extra_fee(self, tx): - if self.can_sign_without_server(): - return 0 - if self.billing_info.get('tx_remaining'): - return 0 - if self.is_billing: - return 0 - # trustedcoin won't charge if the total inputs is lower than their fee - price = int(self.price_per_tx.get(1)) - assert price <= 100000 - if tx.input_value() < price: - self.print_error("not charging for this tx") - return 0 - return price - - def estimated_fee(self, tx, fee_per_kb): - fee = Multisig_Wallet.estimated_fee(self, tx, fee_per_kb) - fee += self.extra_fee(tx) - return fee - - def get_tx_fee(self, tx): - fee = Multisig_Wallet.get_tx_fee(self, tx) - fee += self.extra_fee(tx) - return fee - - def make_unsigned_transaction(self, *args): - tx = BIP32_Wallet.make_unsigned_transaction(self, *args) - fee = self.extra_fee(tx) - if fee: - address = self.billing_info['billing_address'] - tx.outputs.append(('address', address, fee)) - return tx - - def sign_transaction(self, tx, password): - BIP32_Wallet.sign_transaction(self, tx, password) - if tx.is_complete(): - return - if not self.auth_code: - self.print_error("sign_transaction: no auth code") - return - long_user_id, short_id = self.get_user_id() - tx_dict = tx.as_dict() - raw_tx = tx_dict["hex"] - r = server.sign(short_id, raw_tx, self.auth_code) - if r: - raw_tx = r.get('transaction') - tx.update(raw_tx) - self.print_error("twofactor: is complete", tx.is_complete()) - - def get_user_id(self): - def make_long_id(xpub_hot, xpub_cold): - return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold]))) - xpub_hot = self.master_public_keys["x1/"] - xpub_cold = self.master_public_keys["x2/"] - long_id = make_long_id(xpub_hot, xpub_cold) - short_id = hashlib.sha256(long_id).hexdigest() - return long_id, short_id - -# Utility functions - -def make_xpub(xpub, s): - _, _, _, c, cK = deserialize_xkey(xpub) - cK2, c2 = bitcoin._CKD_pub(cK, c, s) - xpub2 = ("0488B21E" + "00" + "00000000" + "00000000").decode("hex") + c2 + cK2 - return EncodeBase58Check(xpub2) - -def restore_third_key(wallet): - long_user_id, short_id = wallet.get_user_id() - xpub3 = make_xpub(signing_xpub, long_user_id) - wallet.add_master_public_key('x3/', xpub3) - -def make_billing_address(wallet, num): - long_id, short_id = wallet.get_user_id() - xpub = make_xpub(billing_xpub, long_id) - _, _, _, c, cK = deserialize_xkey(xpub) - cK, c = bitcoin.CKD_pub(cK, c, num) - address = public_key_to_bc_address( cK ) - return address - -def need_server(wallet, tx): - from electrum.account import BIP32_Account - # Detect if the server is needed - long_id, short_id = wallet.get_user_id() - xpub3 = wallet.master_public_keys['x3/'] - for x in tx.inputs_to_sign(): - if x[0:2] == 'ff': - xpub, sequence = BIP32_Account.parse_xpubkey(x) - if xpub == xpub3: - return True - return False - -class Plugin(BasePlugin): - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.seed_func = lambda x: bitcoin.is_new_seed(x, SEED_PREFIX) - - def constructor(self, s): - return Wallet_2fa(s) - - def is_available(self): - return True - - def set_enabled(self, wallet, enabled): - wallet.storage.put('use_' + self.name, enabled) - - def is_enabled(self): - return True - - @hook - def on_new_window(self, window): - wallet = window.wallet - if wallet.storage.get('wallet_type') == '2fa': - button = StatusBarButton(QIcon(":icons/trustedcoin.png"), - _("TrustedCoin"), - partial(self.settings_dialog, window)) - window.statusBar().addPermanentWidget(button) - t = Thread(target=self.request_billing_info, args=(wallet,)) - t.setDaemon(True) - t.start() - - def request_billing_info(self, wallet): - billing_info = server.get(wallet.get_user_id()[1]) - billing_address = make_billing_address(wallet, billing_info['billing_index']) - assert billing_address == billing_info['billing_address'] - wallet.billing_info = billing_info - wallet.price_per_tx = dict(billing_info['price_per_tx']) - return True - - def create_extended_seed(self, wallet, window): - seed = wallet.make_seed() - if not window.show_seed(seed, None): - return - - if not window.verify_seed(seed, None, self.seed_func): - return - - password = window.password_dialog() - wallet.storage.put('seed_version', wallet.seed_version, True) - wallet.storage.put('use_encryption', password is not None, True) - - words = seed.split() - n = len(words)/2 - wallet.add_cosigner_seed(' '.join(words[0:n]), 'x1/', password) - wallet.add_cosigner_xpub(' '.join(words[n:]), 'x2/') - - msg = [ - _('Your wallet file is:') + " %s"%os.path.abspath(wallet.storage.path), - _('You need to be online in order to complete the creation of your wallet.'), - _('If you generated your seed on an offline computer, click on "%s" to close this window, move your wallet file to an online computer and reopen it with Electrum.') % _('Close'), - _('If you are online, click on "%s" to continue.') % _('Next') - ] - return window.question('\n\n'.join(msg), no_label=_('Close'), yes_label=_('Next')) - - - def show_disclaimer(self, wallet, window): - msg = [ - _("Two-factor authentication is a service provided by TrustedCoin.") + ' ', - _("It uses a multi-signature wallet, where you own 2 of 3 keys.") + ' ', - _("The third key is stored on a remote server that signs transactions on your behalf.") + ' ', - _("To use this service, you will need a smartphone with Google Authenticator.") + '\n\n', - - _("A small fee will be charged on each transaction that uses the remote server.") + ' ', - _("You may check and modify your billing preferences once the installation is complete.") + '\n\n', - - _("Note that your coins are not locked in this service.") + ' ', - _("You may withdraw your funds at any time and at no cost, without the remote server, by using the 'restore wallet' option with your wallet seed.") + '\n\n', - - _('The next step will generate the seed of your wallet.') + ' ', - _('This seed will NOT be saved in your computer, and it must be stored on paper.') + ' ', - _('To be safe from malware, you may want to do this on an offline computer, and move your wallet later to an online computer.') - ] - icon = QPixmap(':icons/trustedcoin.png') - if not window.question(''.join(msg), icon=icon): - return False - self.set_enabled(wallet, True) - return True - - @hook - def do_clear(self, window): - window.wallet.is_billing = False - - @hook - def get_wizard_action(self, window, wallet, action): - if hasattr(self, action): - return getattr(self, action) - - @hook - def installwizard_restore(self, window, storage): - if storage.get('wallet_type') != '2fa': - return - - seed = window.enter_seed_dialog("Enter your seed", None, func=self.seed_func) - if not seed: - return - wallet = Wallet_2fa(storage) - password = window.password_dialog() - - wallet.add_seed(seed, password) - words = seed.split() - n = len(words)/2 - wallet.add_cosigner_seed(' '.join(words[0:n]), 'x1/', password) - wallet.add_cosigner_seed(' '.join(words[n:]), 'x2/', password) - - restore_third_key(wallet) - wallet.create_main_account(password) - # disable plugin - self.set_enabled(wallet, False) - return wallet - - - def create_remote_key(self, wallet, window): - if wallet.storage.get('wallet_type') != '2fa': - raise - return - - email = self.accept_terms_of_use(window) - if not email: - return - - xpub_hot = wallet.master_public_keys["x1/"] - xpub_cold = wallet.master_public_keys["x2/"] - - # Generate third key deterministically. - long_user_id, short_id = wallet.get_user_id() - xpub3 = make_xpub(signing_xpub, long_user_id) - - # secret must be sent by the server - try: - r = server.create(xpub_hot, xpub_cold, email) - except socket.error: - window.show_message('Server not reachable, aborting') - return - except TrustedCoinException as e: - if e.status_code == 409: - r = None - else: - raise e - - if r is None: - otp_secret = None - else: - otp_secret = r.get('otp_secret') - if not otp_secret: - window.show_message(_('Error')) - return - _xpub3 = r['xpubkey_cosigner'] - _id = r['id'] - try: - assert _id == short_id, ("user id error", _id, short_id) - assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3) - except Exception as e: - window.show_message(str(e)) - return - - if not self.setup_google_auth(window, short_id, otp_secret): - return - - wallet.add_master_public_key('x3/', xpub3) - return True - - -from PyQt4.QtGui import * -from PyQt4.QtCore import * -from electrum_gui.qt.util import * -from electrum_gui.qt.qrcodewidget import QRCodeWidget -from electrum_gui.qt.amountedit import AmountEdit -from electrum_gui.qt.main_window import StatusBarButton - -class QtPlugin(Plugin): - - def auth_dialog(self, window): - d = QDialog(window) - d.setModal(1) - vbox = QVBoxLayout(d) - pw = AmountEdit(None, is_int = True) - msg = _('Please enter your Google Authenticator code') - vbox.addWidget(QLabel(msg)) - grid = QGridLayout() - grid.setSpacing(8) - grid.addWidget(QLabel(_('Code')), 1, 0) - grid.addWidget(pw, 1, 1) - vbox.addLayout(grid) - vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) - if not d.exec_(): - return - return pw.get_amount() - - @hook - def sign_tx(self, window, tx): - self.print_error("twofactor:sign_tx") - wallet = window.wallet - if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server(): - auth_code = None - if need_server(wallet, tx): - auth_code = self.auth_dialog(window) - else: - self.print_error("twofactor: xpub3 not needed") - window.wallet.auth_code = auth_code - - @hook - def abort_send(self, window): - wallet = window.wallet - if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server(): - if wallet.billing_info is None: - # request billing info before forming the transaction - task = partial(self.request_billing_info, wallet) - waiting_dialog = WaitingDialog(window, 'please wait...', task) - waiting_dialog.start() - waiting_dialog.wait() - if wallet.billing_info is None: - window.show_message('Could not contact server') - return True - return False - - - def settings_dialog(self, window): - task = partial(self.request_billing_info, window.wallet) - self.waiting_dialog = WaitingDialog(window, 'please wait...', task, partial(self.show_settings_dialog, window)) - self.waiting_dialog.start() - - def show_settings_dialog(self, window, success): - if not success: - window.show_message(_('Server not reachable.')) - return - - wallet = window.wallet - d = QDialog(window) - d.setWindowTitle("TrustedCoin Information") - d.setMinimumSize(500, 200) - vbox = QVBoxLayout(d) - hbox = QHBoxLayout() - - logo = QLabel() - logo.setPixmap(QPixmap(":icons/trustedcoin.png")) - msg = _('This wallet is protected by TrustedCoin\'s two-factor authentication.') + '<br/>'\ - + _("For more information, visit") + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>" - label = QLabel(msg) - label.setOpenExternalLinks(1) - - hbox.addStretch(10) - hbox.addWidget(logo) - hbox.addStretch(10) - hbox.addWidget(label) - hbox.addStretch(10) - - vbox.addLayout(hbox) - vbox.addStretch(10) - - msg = _('TrustedCoin charges a fee per co-signed transaction. You may pay on each transaction (an extra output will be added to your transaction), or you may purchase prepaid transaction using this dialog.') + '<br/>' - label = QLabel(msg) - label.setWordWrap(1) - vbox.addWidget(label) - - vbox.addStretch(10) - grid = QGridLayout() - vbox.addLayout(grid) - - price_per_tx = wallet.price_per_tx - v = price_per_tx.get(1) - grid.addWidget(QLabel(_("Price per transaction (not prepaid):")), 0, 0) - grid.addWidget(QLabel(window.format_amount(v) + ' ' + window.base_unit()), 0, 1) - - i = 1 - - if 10 not in price_per_tx: - price_per_tx[10] = 10 * price_per_tx.get(1) - - for k, v in sorted(price_per_tx.items()): - if k == 1: - continue - grid.addWidget(QLabel("Price for %d prepaid transactions:"%k), i, 0) - grid.addWidget(QLabel("%d x "%k + window.format_amount(v/k) + ' ' + window.base_unit()), i, 1) - b = QPushButton(_("Buy")) - b.clicked.connect(lambda b, k=k, v=v: self.on_buy(window, k, v, d)) - grid.addWidget(b, i, 2) - i += 1 - - n = wallet.billing_info.get('tx_remaining', 0) - grid.addWidget(QLabel(_("Your wallet has %d prepaid transactions.")%n), i, 0) - - # tranfer button - #def on_transfer(): - # server.transfer_credit(self.user_id, recipient, otp, signature_callback) - # pass - #b = QPushButton(_("Transfer")) - #b.clicked.connect(on_transfer) - #grid.addWidget(b, 1, 2) - - #grid.addWidget(QLabel(_("Next Billing Address:")), i, 0) - #grid.addWidget(QLabel(self.billing_info['billing_address']), i, 1) - vbox.addLayout(Buttons(CloseButton(d))) - d.exec_() - - def on_buy(self, window, k, v, d): - d.close() - if window.pluginsdialog: - window.pluginsdialog.close() - wallet = window.wallet - uri = "bitcoin:" + wallet.billing_info['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000) - wallet.is_billing = True - window.pay_to_URI(uri) - window.payto_e.setFrozen(True) - window.message_e.setFrozen(True) - window.amount_e.setFrozen(True) - - def accept_terms_of_use(self, window): - vbox = QVBoxLayout() - window.set_layout(vbox) - vbox.addWidget(QLabel(_("Terms of Service"))) - - tos_e = QTextEdit() - tos_e.setReadOnly(True) - vbox.addWidget(tos_e) - - vbox.addWidget(QLabel(_("Please enter your e-mail address"))) - email_e = QLineEdit() - vbox.addWidget(email_e) - vbox.addStretch() - accept_button = OkButton(window, _('Accept')) - accept_button.setEnabled(False) - vbox.addLayout(Buttons(CancelButton(window), accept_button)) - - def request_TOS(): - tos = server.get_terms_of_service() - self.TOS = tos - window.emit(SIGNAL('twofactor:TOS')) - - def on_result(): - tos_e.setText(self.TOS) - - window.connect(window, SIGNAL('twofactor:TOS'), on_result) - t = Thread(target=request_TOS) - t.setDaemon(True) - t.start() - - regexp = r"[^@]+@[^@]+\.[^@]+" - email_e.textChanged.connect(lambda: accept_button.setEnabled(re.match(regexp,email_e.text()) is not None)) - email_e.setFocus(True) - - if not window.exec_(): - return - - email = str(email_e.text()) - return email - - - def setup_google_auth(self, window, _id, otp_secret): - vbox = QVBoxLayout() - window.set_layout(vbox) - if otp_secret is not None: - uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret) - vbox.addWidget(QLabel("Please scan this QR code in Google Authenticator.")) - qrw = QRCodeWidget(uri) - vbox.addWidget(qrw, 1) - msg = _('Then, enter your Google Authenticator code:') - else: - label = QLabel("This wallet is already registered, but it was never authenticated. To finalize your registration, please enter your Google Authenticator Code. If you do not have this code, delete the wallet file and start a new registration") - label.setWordWrap(1) - vbox.addWidget(label) - msg = _('Google Authenticator code:') - - hbox = QHBoxLayout() - hbox.addWidget(QLabel(msg)) - pw = AmountEdit(None, is_int = True) - pw.setFocus(True) - hbox.addWidget(pw) - hbox.addStretch(1) - vbox.addLayout(hbox) - - b = OkButton(window, _('Next')) - b.setEnabled(False) - vbox.addLayout(Buttons(CancelButton(window), b)) - pw.textChanged.connect(lambda: b.setEnabled(len(pw.text())==6)) - - while True: - if not window.exec_(): - return False - otp = pw.get_amount() - try: - server.auth(_id, otp) - return True - except: - QMessageBox.information(window, _('Message'), _('Incorrect password'), _('OK')) - pw.setText('') - - diff --git a/plugins/trustedcoin/__init__.py b/plugins/trustedcoin/__init__.py @@ -0,0 +1,11 @@ +from electrum.i18n import _ + +fullname = _('Two Factor Authentication') +description = ''.join([ + _("This plugin adds two-factor authentication to your wallet."), '<br/>', + _("For more information, visit"), + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>" +]) +requires_wallet_type = ['2fa'] +registers_wallet_type = ('twofactor', '2fa', _("Wallet with two-factor authentication")) +available_for = ['qt', 'cmdline'] diff --git a/plugins/trustedcoin/qt.py b/plugins/trustedcoin/qt.py @@ -0,0 +1,225 @@ +from PyQt4.QtGui import * +from PyQt4.QtCore import * +from electrum_gui.qt.util import * +from electrum_gui.qt.qrcodewidget import QRCodeWidget +from electrum_gui.qt.amountedit import AmountEdit +from electrum_gui.qt.main_window import StatusBarButton + +class Plugin(TrustedCoinPlugin): + + def auth_dialog(self, window): + d = QDialog(window) + d.setModal(1) + vbox = QVBoxLayout(d) + pw = AmountEdit(None, is_int = True) + msg = _('Please enter your Google Authenticator code') + vbox.addWidget(QLabel(msg)) + grid = QGridLayout() + grid.setSpacing(8) + grid.addWidget(QLabel(_('Code')), 1, 0) + grid.addWidget(pw, 1, 1) + vbox.addLayout(grid) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + if not d.exec_(): + return + return pw.get_amount() + + @hook + def sign_tx(self, window, tx): + self.print_error("twofactor:sign_tx") + wallet = window.wallet + if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server(): + auth_code = None + if need_server(wallet, tx): + auth_code = self.auth_dialog(window) + else: + self.print_error("twofactor: xpub3 not needed") + window.wallet.auth_code = auth_code + + @hook + def abort_send(self, window): + wallet = window.wallet + if type(wallet) is Wallet_2fa and not wallet.can_sign_without_server(): + if wallet.billing_info is None: + # request billing info before forming the transaction + task = partial(self.request_billing_info, wallet) + waiting_dialog = WaitingDialog(window, 'please wait...', task) + waiting_dialog.start() + waiting_dialog.wait() + if wallet.billing_info is None: + window.show_message('Could not contact server') + return True + return False + + + def settings_dialog(self, window): + task = partial(self.request_billing_info, window.wallet) + self.waiting_dialog = WaitingDialog(window, 'please wait...', task, partial(self.show_settings_dialog, window)) + self.waiting_dialog.start() + + def show_settings_dialog(self, window, success): + if not success: + window.show_message(_('Server not reachable.')) + return + + wallet = window.wallet + d = QDialog(window) + d.setWindowTitle("TrustedCoin Information") + d.setMinimumSize(500, 200) + vbox = QVBoxLayout(d) + hbox = QHBoxLayout() + + logo = QLabel() + logo.setPixmap(QPixmap(":icons/trustedcoin.png")) + msg = _('This wallet is protected by TrustedCoin\'s two-factor authentication.') + '<br/>'\ + + _("For more information, visit") + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>" + label = QLabel(msg) + label.setOpenExternalLinks(1) + + hbox.addStretch(10) + hbox.addWidget(logo) + hbox.addStretch(10) + hbox.addWidget(label) + hbox.addStretch(10) + + vbox.addLayout(hbox) + vbox.addStretch(10) + + msg = _('TrustedCoin charges a fee per co-signed transaction. You may pay on each transaction (an extra output will be added to your transaction), or you may purchase prepaid transaction using this dialog.') + '<br/>' + label = QLabel(msg) + label.setWordWrap(1) + vbox.addWidget(label) + + vbox.addStretch(10) + grid = QGridLayout() + vbox.addLayout(grid) + + price_per_tx = wallet.price_per_tx + v = price_per_tx.get(1) + grid.addWidget(QLabel(_("Price per transaction (not prepaid):")), 0, 0) + grid.addWidget(QLabel(window.format_amount(v) + ' ' + window.base_unit()), 0, 1) + + i = 1 + + if 10 not in price_per_tx: + price_per_tx[10] = 10 * price_per_tx.get(1) + + for k, v in sorted(price_per_tx.items()): + if k == 1: + continue + grid.addWidget(QLabel("Price for %d prepaid transactions:"%k), i, 0) + grid.addWidget(QLabel("%d x "%k + window.format_amount(v/k) + ' ' + window.base_unit()), i, 1) + b = QPushButton(_("Buy")) + b.clicked.connect(lambda b, k=k, v=v: self.on_buy(window, k, v, d)) + grid.addWidget(b, i, 2) + i += 1 + + n = wallet.billing_info.get('tx_remaining', 0) + grid.addWidget(QLabel(_("Your wallet has %d prepaid transactions.")%n), i, 0) + + # tranfer button + #def on_transfer(): + # server.transfer_credit(self.user_id, recipient, otp, signature_callback) + # pass + #b = QPushButton(_("Transfer")) + #b.clicked.connect(on_transfer) + #grid.addWidget(b, 1, 2) + + #grid.addWidget(QLabel(_("Next Billing Address:")), i, 0) + #grid.addWidget(QLabel(self.billing_info['billing_address']), i, 1) + vbox.addLayout(Buttons(CloseButton(d))) + d.exec_() + + def on_buy(self, window, k, v, d): + d.close() + if window.pluginsdialog: + window.pluginsdialog.close() + wallet = window.wallet + uri = "bitcoin:" + wallet.billing_info['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000) + wallet.is_billing = True + window.pay_to_URI(uri) + window.payto_e.setFrozen(True) + window.message_e.setFrozen(True) + window.amount_e.setFrozen(True) + + def accept_terms_of_use(self, window): + vbox = QVBoxLayout() + window.set_layout(vbox) + vbox.addWidget(QLabel(_("Terms of Service"))) + + tos_e = QTextEdit() + tos_e.setReadOnly(True) + vbox.addWidget(tos_e) + + vbox.addWidget(QLabel(_("Please enter your e-mail address"))) + email_e = QLineEdit() + vbox.addWidget(email_e) + vbox.addStretch() + accept_button = OkButton(window, _('Accept')) + accept_button.setEnabled(False) + vbox.addLayout(Buttons(CancelButton(window), accept_button)) + + def request_TOS(): + tos = server.get_terms_of_service() + self.TOS = tos + window.emit(SIGNAL('twofactor:TOS')) + + def on_result(): + tos_e.setText(self.TOS) + + window.connect(window, SIGNAL('twofactor:TOS'), on_result) + t = Thread(target=request_TOS) + t.setDaemon(True) + t.start() + + regexp = r"[^@]+@[^@]+\.[^@]+" + email_e.textChanged.connect(lambda: accept_button.setEnabled(re.match(regexp,email_e.text()) is not None)) + email_e.setFocus(True) + + if not window.exec_(): + return + + email = str(email_e.text()) + return email + + + def setup_google_auth(self, window, _id, otp_secret): + vbox = QVBoxLayout() + window.set_layout(vbox) + if otp_secret is not None: + uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret) + vbox.addWidget(QLabel("Please scan this QR code in Google Authenticator.")) + qrw = QRCodeWidget(uri) + vbox.addWidget(qrw, 1) + msg = _('Then, enter your Google Authenticator code:') + else: + label = QLabel("This wallet is already registered, but it was never authenticated. To finalize your registration, please enter your Google Authenticator Code. If you do not have this code, delete the wallet file and start a new registration") + label.setWordWrap(1) + vbox.addWidget(label) + msg = _('Google Authenticator code:') + + hbox = QHBoxLayout() + hbox.addWidget(QLabel(msg)) + pw = AmountEdit(None, is_int = True) + pw.setFocus(True) + hbox.addWidget(pw) + hbox.addStretch(1) + vbox.addLayout(hbox) + + b = OkButton(window, _('Next')) + b.setEnabled(False) + vbox.addLayout(Buttons(CancelButton(window), b)) + pw.textChanged.connect(lambda: b.setEnabled(len(pw.text())==6)) + + while True: + if not window.exec_(): + return False + otp = pw.get_amount() + try: + server.auth(_id, otp) + return True + except: + QMessageBox.information(window, _('Message'), _('Incorrect password'), _('OK')) + pw.setText('') + + diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python +# +# Electrum - Lightweight Bitcoin Client +# Copyright (C) 2015 Thomas Voegtlin +# +# 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/>. + +from threading import Thread +import socket +import os +import re +import requests +import json +from hashlib import sha256 +from urlparse import urljoin +from urllib import quote +from functools import partial + +import electrum +from electrum import bitcoin +from electrum.bitcoin import * +from electrum.mnemonic import Mnemonic +from electrum import version +from electrum.wallet import Multisig_Wallet, BIP32_Wallet +from electrum.i18n import _ +from electrum.plugins import BasePlugin, run_hook, hook + + +from decimal import Decimal + +# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server +signing_xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" +billing_xpub = "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU" + +SEED_PREFIX = version.SEED_PREFIX_2FA + + +class TrustedCoinException(Exception): + def __init__(self, message, status_code=0): + Exception.__init__(self, message) + self.status_code = status_code + +class TrustedCoinCosignerClient(object): + def __init__(self, user_agent=None, base_url='https://api.trustedcoin.com/2/', debug=False): + self.base_url = base_url + self.debug = debug + self.user_agent = user_agent + + def send_request(self, method, relative_url, data=None): + kwargs = {'headers': {}} + if self.user_agent: + kwargs['headers']['user-agent'] = self.user_agent + if method == 'get' and data: + kwargs['params'] = data + elif method == 'post' and data: + kwargs['data'] = json.dumps(data) + kwargs['headers']['content-type'] = 'application/json' + url = urljoin(self.base_url, relative_url) + if self.debug: + print '%s %s %s' % (method, url, data) + response = requests.request(method, url, **kwargs) + if self.debug: + print response.text + print + if response.status_code != 200: + message = str(response.text) + if response.headers.get('content-type') == 'application/json': + r = response.json() + if 'message' in r: + message = r['message'] + raise TrustedCoinException(message, response.status_code) + if response.headers.get('content-type') == 'application/json': + return response.json() + else: + return response.text + + def get_terms_of_service(self, billing_plan='electrum-per-tx-otp'): + """ + Returns the TOS for the given billing plan as a plain/text unicode string. + :param billing_plan: the plan to return the terms for + """ + payload = {'billing_plan': billing_plan} + return self.send_request('get', 'tos', payload) + + def create(self, xpubkey1, xpubkey2, email, billing_plan='electrum-per-tx-otp'): + """ + Creates a new cosigner resource. + :param xpubkey1: a bip32 extended public key (customarily the hot key) + :param xpubkey2: a bip32 extended public key (customarily the cold key) + :param email: a contact email + :param billing_plan: the billing plan for the cosigner + """ + payload = { + 'email': email, + 'xpubkey1': xpubkey1, + 'xpubkey2': xpubkey2, + 'billing_plan': billing_plan, + } + return self.send_request('post', 'cosigner', payload) + + def auth(self, id, otp): + """ + Attempt to authenticate for a particular cosigner. + :param id: the id of the cosigner + :param otp: the one time password + """ + payload = {'otp': otp} + return self.send_request('post', 'cosigner/%s/auth' % quote(id), payload) + + def get(self, id): + """ + Attempt to authenticate for a particular cosigner. + :param id: the id of the cosigner + :param otp: the one time password + """ + return self.send_request('get', 'cosigner/%s' % quote(id)) + + def sign(self, id, transaction, otp): + """ + Attempt to authenticate for a particular cosigner. + :param id: the id of the cosigner + :param transaction: the hex encoded [partially signed] compact transaction to sign + :param otp: the one time password + """ + payload = { + 'otp': otp, + 'transaction': transaction + } + return self.send_request('post', 'cosigner/%s/sign' % quote(id), payload) + + def transfer_credit(self, id, recipient, otp, signature_callback): + """ + Tranfer a cosigner's credits to another cosigner. + :param id: the id of the sending cosigner + :param recipient: the id of the recipient cosigner + :param otp: the one time password (of the sender) + :param signature_callback: a callback that signs a text message using xpubkey1/0/0 returning a compact sig + """ + payload = { + 'otp': otp, + 'recipient': recipient, + 'timestamp': int(time.time()), + + } + relative_url = 'cosigner/%s/transfer' % quote(id) + full_url = urljoin(self.base_url, relative_url) + headers = { + 'x-signature': signature_callback(full_url + '\n' + json.dumps(payload)) + } + return self.send_request('post', relative_url, payload, headers) + + +server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VERSION) + +class Wallet_2fa(Multisig_Wallet): + + def __init__(self, storage): + BIP32_Wallet.__init__(self, storage) + self.wallet_type = '2fa' + self.m = 2 + self.n = 3 + self.is_billing = False + self.billing_info = None + + def get_action(self): + xpub1 = self.master_public_keys.get("x1/") + xpub2 = self.master_public_keys.get("x2/") + xpub3 = self.master_public_keys.get("x3/") + if xpub2 is None and not self.storage.get('use_trustedcoin'): + return 'show_disclaimer' + if xpub2 is None: + return 'create_extended_seed' + if xpub3 is None: + return 'create_remote_key' + if not self.accounts: + return 'create_accounts' + + def make_seed(self): + return Mnemonic('english').make_seed(num_bits=256, prefix=SEED_PREFIX) + + def can_sign_without_server(self): + return self.master_private_keys.get('x2/') is not None + + def extra_fee(self, tx): + if self.can_sign_without_server(): + return 0 + if self.billing_info.get('tx_remaining'): + return 0 + if self.is_billing: + return 0 + # trustedcoin won't charge if the total inputs is lower than their fee + price = int(self.price_per_tx.get(1)) + assert price <= 100000 + if tx.input_value() < price: + self.print_error("not charging for this tx") + return 0 + return price + + def estimated_fee(self, tx, fee_per_kb): + fee = Multisig_Wallet.estimated_fee(self, tx, fee_per_kb) + fee += self.extra_fee(tx) + return fee + + def get_tx_fee(self, tx): + fee = Multisig_Wallet.get_tx_fee(self, tx) + fee += self.extra_fee(tx) + return fee + + def make_unsigned_transaction(self, *args): + tx = BIP32_Wallet.make_unsigned_transaction(self, *args) + fee = self.extra_fee(tx) + if fee: + address = self.billing_info['billing_address'] + tx.outputs.append(('address', address, fee)) + return tx + + def sign_transaction(self, tx, password): + BIP32_Wallet.sign_transaction(self, tx, password) + if tx.is_complete(): + return + if not self.auth_code: + self.print_error("sign_transaction: no auth code") + return + long_user_id, short_id = self.get_user_id() + tx_dict = tx.as_dict() + raw_tx = tx_dict["hex"] + r = server.sign(short_id, raw_tx, self.auth_code) + if r: + raw_tx = r.get('transaction') + tx.update(raw_tx) + self.print_error("twofactor: is complete", tx.is_complete()) + + def get_user_id(self): + def make_long_id(xpub_hot, xpub_cold): + return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold]))) + xpub_hot = self.master_public_keys["x1/"] + xpub_cold = self.master_public_keys["x2/"] + long_id = make_long_id(xpub_hot, xpub_cold) + short_id = hashlib.sha256(long_id).hexdigest() + return long_id, short_id + +# Utility functions + +def make_xpub(xpub, s): + _, _, _, c, cK = deserialize_xkey(xpub) + cK2, c2 = bitcoin._CKD_pub(cK, c, s) + xpub2 = ("0488B21E" + "00" + "00000000" + "00000000").decode("hex") + c2 + cK2 + return EncodeBase58Check(xpub2) + +def restore_third_key(wallet): + long_user_id, short_id = wallet.get_user_id() + xpub3 = make_xpub(signing_xpub, long_user_id) + wallet.add_master_public_key('x3/', xpub3) + +def make_billing_address(wallet, num): + long_id, short_id = wallet.get_user_id() + xpub = make_xpub(billing_xpub, long_id) + _, _, _, c, cK = deserialize_xkey(xpub) + cK, c = bitcoin.CKD_pub(cK, c, num) + address = public_key_to_bc_address( cK ) + return address + +def need_server(wallet, tx): + from electrum.account import BIP32_Account + # Detect if the server is needed + long_id, short_id = wallet.get_user_id() + xpub3 = wallet.master_public_keys['x3/'] + for x in tx.inputs_to_sign(): + if x[0:2] == 'ff': + xpub, sequence = BIP32_Account.parse_xpubkey(x) + if xpub == xpub3: + return True + return False + + +class TrustedCoinPlugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.seed_func = lambda x: bitcoin.is_new_seed(x, SEED_PREFIX) + + def constructor(self, s): + return Wallet_2fa(s) + + def is_available(self): + return True + + def set_enabled(self, wallet, enabled): + wallet.storage.put('use_' + self.name, enabled) + + def is_enabled(self): + return True + + @hook + def on_new_window(self, window): + wallet = window.wallet + if wallet.storage.get('wallet_type') == '2fa': + button = StatusBarButton(QIcon(":icons/trustedcoin.png"), + _("TrustedCoin"), + partial(self.settings_dialog, window)) + window.statusBar().addPermanentWidget(button) + t = Thread(target=self.request_billing_info, args=(wallet,)) + t.setDaemon(True) + t.start() + + def request_billing_info(self, wallet): + billing_info = server.get(wallet.get_user_id()[1]) + billing_address = make_billing_address(wallet, billing_info['billing_index']) + assert billing_address == billing_info['billing_address'] + wallet.billing_info = billing_info + wallet.price_per_tx = dict(billing_info['price_per_tx']) + return True + + def create_extended_seed(self, wallet, window): + seed = wallet.make_seed() + if not window.show_seed(seed, None): + return + + if not window.verify_seed(seed, None, self.seed_func): + return + + password = window.password_dialog() + wallet.storage.put('seed_version', wallet.seed_version, True) + wallet.storage.put('use_encryption', password is not None, True) + + words = seed.split() + n = len(words)/2 + wallet.add_cosigner_seed(' '.join(words[0:n]), 'x1/', password) + wallet.add_cosigner_xpub(' '.join(words[n:]), 'x2/') + + msg = [ + _('Your wallet file is:') + " %s"%os.path.abspath(wallet.storage.path), + _('You need to be online in order to complete the creation of your wallet.'), + _('If you generated your seed on an offline computer, click on "%s" to close this window, move your wallet file to an online computer and reopen it with Electrum.') % _('Close'), + _('If you are online, click on "%s" to continue.') % _('Next') + ] + return window.question('\n\n'.join(msg), no_label=_('Close'), yes_label=_('Next')) + + + def show_disclaimer(self, wallet, window): + msg = [ + _("Two-factor authentication is a service provided by TrustedCoin.") + ' ', + _("It uses a multi-signature wallet, where you own 2 of 3 keys.") + ' ', + _("The third key is stored on a remote server that signs transactions on your behalf.") + ' ', + _("To use this service, you will need a smartphone with Google Authenticator.") + '\n\n', + + _("A small fee will be charged on each transaction that uses the remote server.") + ' ', + _("You may check and modify your billing preferences once the installation is complete.") + '\n\n', + + _("Note that your coins are not locked in this service.") + ' ', + _("You may withdraw your funds at any time and at no cost, without the remote server, by using the 'restore wallet' option with your wallet seed.") + '\n\n', + + _('The next step will generate the seed of your wallet.') + ' ', + _('This seed will NOT be saved in your computer, and it must be stored on paper.') + ' ', + _('To be safe from malware, you may want to do this on an offline computer, and move your wallet later to an online computer.') + ] + icon = QPixmap(':icons/trustedcoin.png') + if not window.question(''.join(msg), icon=icon): + return False + self.set_enabled(wallet, True) + return True + + @hook + def do_clear(self, window): + window.wallet.is_billing = False + + @hook + def get_wizard_action(self, window, wallet, action): + if hasattr(self, action): + return getattr(self, action) + + @hook + def installwizard_restore(self, window, storage): + if storage.get('wallet_type') != '2fa': + return + + seed = window.enter_seed_dialog("Enter your seed", None, func=self.seed_func) + if not seed: + return + wallet = Wallet_2fa(storage) + password = window.password_dialog() + + wallet.add_seed(seed, password) + words = seed.split() + n = len(words)/2 + wallet.add_cosigner_seed(' '.join(words[0:n]), 'x1/', password) + wallet.add_cosigner_seed(' '.join(words[n:]), 'x2/', password) + + restore_third_key(wallet) + wallet.create_main_account(password) + # disable plugin + self.set_enabled(wallet, False) + return wallet + + + def create_remote_key(self, wallet, window): + if wallet.storage.get('wallet_type') != '2fa': + raise + return + + email = self.accept_terms_of_use(window) + if not email: + return + + xpub_hot = wallet.master_public_keys["x1/"] + xpub_cold = wallet.master_public_keys["x2/"] + + # Generate third key deterministically. + long_user_id, short_id = wallet.get_user_id() + xpub3 = make_xpub(signing_xpub, long_user_id) + + # secret must be sent by the server + try: + r = server.create(xpub_hot, xpub_cold, email) + except socket.error: + window.show_message('Server not reachable, aborting') + return + except TrustedCoinException as e: + if e.status_code == 409: + r = None + else: + raise e + + if r is None: + otp_secret = None + else: + otp_secret = r.get('otp_secret') + if not otp_secret: + window.show_message(_('Error')) + return + _xpub3 = r['xpubkey_cosigner'] + _id = r['id'] + try: + assert _id == short_id, ("user id error", _id, short_id) + assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3) + except Exception as e: + window.show_message(str(e)) + return + + if not self.setup_google_auth(window, short_id, otp_secret): + return + + wallet.add_master_public_key('x3/', xpub3) + return True + + diff --git a/plugins/virtualkeyboard.py b/plugins/virtualkeyboard.py @@ -1,60 +0,0 @@ -from PyQt4.QtGui import * -from electrum.plugins import BasePlugin, hook -from electrum.i18n import _ -import random - -class QtPlugin(BasePlugin): - - vkb = None - vkb_index = 0 - - @hook - 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 - self.vkb = None - - 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): - 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/plugins/virtualkeyboard/__init__.py b/plugins/virtualkeyboard/__init__.py @@ -0,0 +1,5 @@ +from electrum.i18n import _ + +fullname = 'Virtual Keyboard' +description = '%s\n%s' % (_("Add an optional virtual keyboard to the password dialog."), _("Warning: do not use this if it makes you pick a weaker password.")) +available_for = ['qt'] diff --git a/plugins/virtualkeyboard/qt.py b/plugins/virtualkeyboard/qt.py @@ -0,0 +1,60 @@ +from PyQt4.QtGui import * +from electrum.plugins import BasePlugin, hook +from electrum.i18n import _ +import random + +class Plugin(BasePlugin): + + vkb = None + vkb_index = 0 + + @hook + 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 + self.vkb = None + + 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): + 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.py b/setup.py @@ -34,12 +34,28 @@ setup( 'protobuf', 'dnspython', ], + packages=[ + 'electrum', + 'electrum_gui', + 'electrum_gui.qt', + 'electrum_plugins.audio_modem', + 'electrum_plugins.cosigner_pool', + 'electrum_plugins.email_requests', + 'electrum_plugins.exchange_rate', + 'electrum_plugins.greenaddress_instant', + 'electrum_plugins.keepkey', + 'electrum_plugins.labels', + 'electrum_plugins.ledger', + 'electrum_plugins.plot', + 'electrum_plugins.trezor', + 'electrum_plugins.trustedcoin', + 'electrum_plugins.virtualkeyboard', + ], package_dir={ 'electrum': 'lib', 'electrum_gui': 'gui', 'electrum_plugins': 'plugins', }, - packages=['electrum','electrum_gui','electrum_gui.qt','electrum_plugins'], package_data={ 'electrum': [ 'www/index.html',