diff --git a/gui/qt/qrtextedit.py b/gui/qt/qrtextedit.py
index 8b7106e38..994733ab9 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 94fd43e3c..e2197dd03 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 000000000..f9a271c55
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 000000000..963db53f9
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 000000000..146cb1355
--- /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