diff --git a/gui/qt/qrtextedit.py b/gui/qt/qrtextedit.py index 8b7106e3..994733ab 100644 --- a/gui/qt/qrtextedit.py +++ b/gui/qt/qrtextedit.py @@ -1,4 +1,5 @@ from electrum.i18n import _ +from electrum.plugins import run_hook from PyQt4.QtGui import * from PyQt4.QtCore import * @@ -25,6 +26,7 @@ class ShowQRTextEdit(QRTextEdit): super(ShowQRTextEdit, self).__init__(text) self.setReadOnly(1) self.button.clicked.connect(self.qr_show) + run_hook('show_text_edit', self) def qr_show(self): from qrcodewidget import QRDialog @@ -49,6 +51,7 @@ class ScanQRTextEdit(QRTextEdit): if win: assert hasattr(win,"config"), "You must pass a window with access to the config to ScanQRTextEdit constructor." self.button.clicked.connect(self.qr_input) + run_hook('scan_text_edit', self) def qr_input(self): diff --git a/icons.qrc b/icons.qrc index 94fd43e3..e2197dd0 100644 --- a/icons.qrc +++ b/icons.qrc @@ -28,6 +28,8 @@ icons/network.png icons/dark_background.png icons/qrcode.png + icons/microphone.png + icons/speaker.png icons/trustedcoin.png diff --git a/icons/microphone.png b/icons/microphone.png new file mode 100644 index 00000000..f9a271c5 Binary files /dev/null and b/icons/microphone.png differ diff --git a/icons/speaker.png b/icons/speaker.png new file mode 100644 index 00000000..963db53f Binary files /dev/null and b/icons/speaker.png differ diff --git a/plugins/audio_modem.py b/plugins/audio_modem.py new file mode 100644 index 00000000..146cb135 --- /dev/null +++ b/plugins/audio_modem.py @@ -0,0 +1,123 @@ +from electrum.plugins import BasePlugin, hook +from electrum_gui.qt.util import WaitingDialog +from electrum.util import print_msg + +from PyQt4.QtGui import * +from PyQt4.QtCore import * + +import subprocess +import traceback +import zlib +import json + +try: + subprocess.check_call(['amodem-cli', '--help']) + print_msg('Audio MODEM is enabled.') + amodem_available = True +except subprocess.CalledProcessError: + print_msg('Audio MODEM is not found.') + amodem_available = False + + +def send(parent, blob): + print_msg('Sending:', repr(blob)) + blob = zlib.compress(blob) + def sender_thread(): + try: + args = ['amodem-cli', 'send', '-vv'] + p = subprocess.Popen(args, stdin=subprocess.PIPE) + p.stdin.write(blob) + p.stdin.close() + p.wait() + except Exception: + traceback.print_exc() + p.kill() + + return WaitingDialog( + parent=parent, message='Sending transaction to Audio MODEM...', + run_task=sender_thread) + + +def recv(parent): + def receiver_thread(): + import subprocess + try: + args = ['amodem-cli', 'recv', '-vv'] + p = subprocess.Popen(args, stdout=subprocess.PIPE) + return p.stdout.read() + except Exception: + traceback.print_exc() + p.kill() + + def on_success(blob): + blob = zlib.decompress(blob) + print_msg('Received:', repr(blob)) + parent.setText(blob) + + return WaitingDialog( + parent=parent, message='Receiving transaction from Audio MODEM...', + run_task=receiver_thread, on_success=on_success) + + +class Plugin(BasePlugin): + + def fullname(self): + return 'Audio MODEM' + + def description(self): + return ('Provides support for air-gapped transaction signing.\n\n' + 'Requires http://github.com/romanz/amodem/') + + def is_available(self): + return amodem_available + + is_enabled = is_available + + @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 = send(parent=dialog, blob=blob) + self.sender.start() + b.clicked.connect(handler) + dialog.buttons.insertWidget(1, b) + + @hook + def scan_text_edit(self, parent): + def handler(): + self.receiver = recv(parent=parent) + self.receiver.start() + button = add_button(parent=parent, icon_name=':icons/microphone.png') + button.clicked.connect(handler) + + @hook + def show_text_edit(self, parent): + def handler(): + blob = str(parent.toPlainText()) + self.sender = send(parent=parent, blob=blob) + self.sender.start() + button = add_button(parent=parent, icon_name=':icons/speaker.png') + button.clicked.connect(handler) + + +def add_button(parent, icon_name): + audio_button = QToolButton(parent) + audio_button.setIcon(QIcon(icon_name)) + audio_button.setStyleSheet("QToolButton { border: none; padding: 0px; }") + audio_button.setVisible(True) + + parent_resizeEvent = parent.resizeEvent + + def resizeEvent(e): + result = parent_resizeEvent(e) + qr_button = parent.button + left = qr_button.geometry().left() - audio_button.sizeHint().width() + top = qr_button.geometry().top() + audio_button.move(left, top) + return result + + parent.resizeEvent = resizeEvent + return audio_button