add plugins changes from electrum 3.1.3

This commit is contained in:
zebra-lucky 2018-06-02 19:40:29 +03:00
parent 4cc7dc7188
commit 0c422d5ed9
24 changed files with 1182 additions and 393 deletions

View File

@ -43,9 +43,7 @@ import sys
import traceback
PORT = 12344
HOST = 'cosigner.electrum.org'
server = ServerProxy('http://%s:%d'%(HOST,PORT), allow_none=True)
server = ServerProxy('https://cosigner.electrum.org/', allow_none=True)
class Listener(util.DaemonThread):
@ -175,7 +173,8 @@ class Plugin(BasePlugin):
for window, xpub, K, _hash in self.cosigner_list:
if not self.cosigner_can_sign(tx, xpub):
continue
message = bitcoin.encrypt_message(bfh(tx.raw), bh2u(K)).decode('ascii')
raw_tx_bytes = bfh(str(tx))
message = bitcoin.encrypt_message(raw_tx_bytes, bh2u(K)).decode('ascii')
try:
server.put(_hash, message)
except Exception as e:
@ -194,7 +193,7 @@ class Plugin(BasePlugin):
return
wallet = window.wallet
if wallet.has_password():
if wallet.has_keystore_encryption():
password = window.password_dialog('An encrypted transaction was retrieved from cosigning pool.\nPlease enter your password to decrypt it.')
if not password:
return

View File

@ -9,3 +9,6 @@ class Plugin(DigitalBitboxPlugin):
if not isinstance(keystore, self.keystore_class):
return
keystore.handler = self.handler
def create_handler(self, window):
return self.handler

View File

@ -7,11 +7,13 @@ try:
import electrum_zcash
from electrum_zcash.bitcoin import TYPE_ADDRESS, push_script, var_int, msg_magic, Hash, verify_message, pubkey_from_signature, point_to_ser, public_key_to_p2pkh, EncodeAES, DecodeAES, MyVerifyingKey
from electrum_zcash.bitcoin import serialize_xpub, deserialize_xpub
from electrum_zcash import constants
from electrum_zcash.transaction import Transaction
from electrum_zcash.i18n import _
from electrum_zcash.keystore import Hardware_KeyStore
from ..hw_wallet import HW_PluginBase
from electrum_zcash.util import print_error, to_string, UserCancelled
from electrum_zcash.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
import time
import hid
@ -80,9 +82,16 @@ class DigitalBitbox_Client():
def is_paired(self):
return self.password is not None
def has_usable_connection_with_device(self):
try:
self.dbb_has_password()
except BaseException:
return False
return True
def _get_xpub(self, bip32_path):
if self.check_device_dialog():
return self.hid_send_encrypt(b'{"xpub": "%s"}' % bip32_path.encode('utf8'))
return self.hid_send_encrypt(('{"xpub": "%s"}' % bip32_path).encode('utf8'))
def get_xpub(self, bip32_path, xtype):
@ -91,20 +100,20 @@ class DigitalBitbox_Client():
if reply:
xpub = reply['xpub']
# Change type of xpub to the requested type. The firmware
# only ever returns the standard type, but it is agnostic
# only ever returns the mainnet standard type, but it is agnostic
# to the type when signing.
if xtype != 'standard':
_, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
if xtype != 'standard' or constants.net.TESTNET:
_, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub, net=constants.BitcoinMainnet)
xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
return xpub
else:
raise BaseException('no reply')
raise Exception('no reply')
def dbb_has_password(self):
reply = self.hid_send_plain(b'{"ping":""}')
if 'ping' not in reply:
raise Exception('Device communication error. Please unplug and replug your Digital Bitbox.')
raise Exception(_('Device communication error. Please unplug and replug your Digital Bitbox.'))
if reply['ping'] == 'password':
return True
return False
@ -112,7 +121,7 @@ class DigitalBitbox_Client():
def stretch_key(self, key):
import pbkdf2, hmac
return binascii.hexlify(pbkdf2.PBKDF2(key, b'Digital Bitbox', iterations = 20480, macmodule = hmac, digestmodule = hashlib.sha512).read(64))
return to_hexstr(pbkdf2.PBKDF2(key, b'Digital Bitbox', iterations = 20480, macmodule = hmac, digestmodule = hashlib.sha512).read(64))
def backup_password_dialog(self):
@ -122,9 +131,11 @@ class DigitalBitbox_Client():
if password is None:
return None
if len(password) < 4:
msg = _("Password must have at least 4 characters.\r\n\r\nEnter password:")
msg = _("Password must have at least 4 characters.") \
+ "\n\n" + _("Enter password:")
elif len(password) > 64:
msg = _("Password must have less than 64 characters.\r\n\r\nEnter password:")
msg = _("Password must have less than 64 characters.") \
+ "\n\n" + _("Enter password:")
else:
return password.encode('utf8')
@ -135,9 +146,11 @@ class DigitalBitbox_Client():
if password is None:
return False
if len(password) < 4:
msg = _("Password must have at least 4 characters.\r\n\r\nEnter password:")
msg = _("Password must have at least 4 characters.") + \
"\n\n" + _("Enter password:")
elif len(password) > 64:
msg = _("Password must have less than 64 characters.\r\n\r\nEnter password:")
msg = _("Password must have less than 64 characters.") + \
"\n\n" + _("Enter password:")
else:
self.password = password.encode('utf8')
return True
@ -148,10 +161,11 @@ class DigitalBitbox_Client():
if self.password is None and not self.dbb_has_password():
if not self.setupRunning:
return False # A fresh device cannot connect to an existing wallet
msg = _("An uninitialized Digital Bitbox is detected. " \
"Enter a new password below.\r\n\r\n REMEMBER THE PASSWORD!\r\n\r\n" \
"You cannot access your coins or a backup without the password.\r\n" \
"A backup is saved automatically when generating a new wallet.")
msg = _("An uninitialized Digital Bitbox is detected.") + " " + \
_("Enter a new password below.") + "\n\n" + \
_("REMEMBER THE PASSWORD!") + "\n\n" + \
_("You cannot access your coins or a backup without the password.") + "\n" + \
_("A backup is saved automatically when generating a new wallet.")
if self.password_dialog(msg):
reply = self.hid_send_plain(b'{"password":"' + self.password + b'"}')
else:
@ -161,19 +175,19 @@ class DigitalBitbox_Client():
msg = _("Enter your Digital Bitbox password:")
while self.password is None:
if not self.password_dialog(msg):
return False
raise UserCancelled()
reply = self.hid_send_encrypt(b'{"led":"blink"}')
if 'error' in reply:
self.password = None
if reply['error']['code'] == 109:
msg = _("Incorrect password entered.\r\n\r\n" \
+ reply['error']['message'] + "\r\n\r\n" \
"Enter your Digital Bitbox password:")
msg = _("Incorrect password entered.") + "\n\n" + \
reply['error']['message'] + "\n\n" + \
_("Enter your Digital Bitbox password:")
else:
# Should never occur
msg = _("Unexpected error occurred.\r\n\r\n" \
+ reply['error']['message'] + "\r\n\r\n" \
"Enter your Digital Bitbox password:")
msg = _("Unexpected error occurred.") + "\n\n" + \
reply['error']['message'] + "\n\n" + \
_("Enter your Digital Bitbox password:")
# Initialize device if not yet initialized
if not self.setupRunning:
@ -189,7 +203,7 @@ class DigitalBitbox_Client():
def recover_or_erase_dialog(self):
msg = _("The Digital Bitbox is already seeded. Choose an option:\n")
msg = _("The Digital Bitbox is already seeded. Choose an option:") + "\n"
choices = [
(_("Create a wallet using the current seed")),
(_("Load a wallet from the micro SD card (the current seed is overwritten)")),
@ -206,13 +220,13 @@ class DigitalBitbox_Client():
return
else:
if self.hid_send_encrypt(b'{"device":"info"}')['device']['lock']:
raise Exception("Full 2FA enabled. This is not supported yet.")
raise Exception(_("Full 2FA enabled. This is not supported yet."))
# Use existing seed
self.isInitialized = True
def seed_device_dialog(self):
msg = _("Choose how to initialize your Digital Bitbox:\n")
msg = _("Choose how to initialize your Digital Bitbox:") + "\n"
choices = [
(_("Generate a new random wallet")),
(_("Load a wallet from the micro SD card"))
@ -240,10 +254,15 @@ class DigitalBitbox_Client():
if not dbb_user_dir:
return
try:
# Python 3.5+
jsonDecodeError = json.JSONDecodeError
except AttributeError:
jsonDecodeError = ValueError
try:
with open(os.path.join(dbb_user_dir, "config.dat")) as f:
dbb_config = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
except (FileNotFoundError, jsonDecodeError):
return
if 'encryptionprivkey' not in dbb_config or 'comserverchannelid' not in dbb_config:
@ -251,7 +270,7 @@ class DigitalBitbox_Client():
choices = [
_('Do not pair'),
_('Import pairing from the digital bitbox desktop app'),
_('Import pairing from the Digital Bitbox desktop app'),
]
try:
reply = self.handler.win.query_choice(_('Mobile pairing options'), choices)
@ -270,17 +289,17 @@ class DigitalBitbox_Client():
def dbb_generate_wallet(self):
key = self.stretch_key(self.password)
filename = ("Electrum-Zcash-" + time.strftime("%Y-%m-%d-%H-%M-%S") + ".pdf").encode('utf8')
msg = b'{"seed":{"source": "create", "key": "%s", "filename": "%s", "entropy": "%s"}}' % (key, filename, b'Digital Bitbox Electrum-Zcash Plugin')
filename = ("Electrum-Zcash-" + time.strftime("%Y-%m-%d-%H-%M-%S") + ".pdf")
msg = ('{"seed":{"source": "create", "key": "%s", "filename": "%s", "entropy": "%s"}}' % (key, filename, 'Digital Bitbox Electrum-Zcash Plugin')).encode('utf8')
reply = self.hid_send_encrypt(msg)
if 'error' in reply:
raise Exception(reply['error']['message'])
def dbb_erase(self):
self.handler.show_message(_("Are you sure you want to erase the Digital Bitbox?\r\n\r\n" \
"To continue, touch the Digital Bitbox's light for 3 seconds.\r\n\r\n" \
"To cancel, briefly touch the light or wait for the timeout."))
self.handler.show_message(_("Are you sure you want to erase the Digital Bitbox?") + "\n\n" +
_("To continue, touch the Digital Bitbox's light for 3 seconds.") + "\n\n" +
_("To cancel, briefly touch the light or wait for the timeout."))
hid_reply = self.hid_send_encrypt(b'{"reset":"__ERASE__"}')
self.handler.finished()
if 'error' in hid_reply:
@ -303,10 +322,10 @@ class DigitalBitbox_Client():
raise Exception('Canceled by user')
key = self.stretch_key(key)
if show_msg:
self.handler.show_message(_("Loading backup...\r\n\r\n" \
"To continue, touch the Digital Bitbox's light for 3 seconds.\r\n\r\n" \
"To cancel, briefly touch the light or wait for the timeout."))
msg = b'{"seed":{"source": "backup", "key": "%s", "filename": "%s"}}' % (key, backups['backup'][f].encode('utf8'))
self.handler.show_message(_("Loading backup...") + "\n\n" +
_("To continue, touch the Digital Bitbox's light for 3 seconds.") + "\n\n" +
_("To cancel, briefly touch the light or wait for the timeout."))
msg = ('{"seed":{"source": "backup", "key": "%s", "filename": "%s"}}' % (key, backups['backup'][f])).encode('utf8')
hid_reply = self.hid_send_encrypt(msg)
self.handler.finished()
if 'error' in hid_reply:
@ -420,7 +439,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
def decrypt_message(self, pubkey, message, password):
raise RuntimeError(_('Encryption and decryption are currently not supported for %s') % self.device)
raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device))
def sign_message(self, sequence, message, password):
@ -434,17 +453,17 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
hasharray.append({'hash': inputHash, 'keypath': inputPath})
hasharray = json.dumps(hasharray)
msg = b'{"sign":{"meta":"sign message", "data":%s}}' % hasharray.encode('utf8')
msg = ('{"sign":{"meta":"sign message", "data":%s}}' % hasharray).encode('utf8')
dbb_client = self.plugin.get_client(self)
if not dbb_client.is_paired():
raise Exception("Could not sign message.")
raise Exception(_("Could not sign message."))
reply = dbb_client.hid_send_encrypt(msg)
self.handler.show_message(_("Signing message ...\r\n\r\n" \
"To continue, touch the Digital Bitbox's blinking light for 3 seconds.\r\n\r\n" \
"To cancel, briefly touch the blinking light or wait for the timeout."))
self.handler.show_message(_("Signing message ...") + "\n\n" +
_("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + "\n\n" +
_("To cancel, briefly touch the blinking light or wait for the timeout."))
reply = dbb_client.hid_send_encrypt(msg) # Send twice, first returns an echo for smart verification (not implemented)
self.handler.finished()
@ -452,7 +471,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
raise Exception(reply['error']['message'])
if 'sign' not in reply:
raise Exception("Could not sign message.")
raise Exception(_("Could not sign message."))
if 'recid' in reply['sign'][0]:
# firmware > v2.1.1
@ -461,7 +480,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
pk = point_to_ser(pk.pubkey.point, compressed)
addr = public_key_to_p2pkh(pk)
if verify_message(addr, sig, message) is False:
raise Exception("Could not sign message")
raise Exception(_("Could not sign message"))
elif 'pubkey' in reply['sign'][0]:
# firmware <= v2.1.1
for i in range(4):
@ -473,7 +492,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
except Exception:
continue
else:
raise Exception("Could not sign message")
raise Exception(_("Could not sign message"))
except BaseException as e:
@ -574,14 +593,14 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
self.plugin.comserver_post_notification(reply)
if steps > 1:
self.handler.show_message(_("Signing large transaction. Please be patient ...\r\n\r\n" \
"To continue, touch the Digital Bitbox's blinking light for 3 seconds. " \
"(Touch " + str(step + 1) + " of " + str(int(steps)) + ")\r\n\r\n" \
"To cancel, briefly touch the blinking light or wait for the timeout.\r\n\r\n"))
self.handler.show_message(_("Signing large transaction. Please be patient ...") + "\n\n" +
_("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + " " +
_("(Touch {} of {})").format((step + 1), steps) + "\n\n" +
_("To cancel, briefly touch the blinking light or wait for the timeout.") + "\n\n")
else:
self.handler.show_message(_("Signing transaction ...\r\n\r\n" \
"To continue, touch the Digital Bitbox's blinking light for 3 seconds.\r\n\r\n" \
"To cancel, briefly touch the blinking light or wait for the timeout."))
self.handler.show_message(_("Signing transaction...") + "\n\n" +
_("To continue, touch the Digital Bitbox's blinking light for 3 seconds.") + "\n\n" +
_("To cancel, briefly touch the blinking light or wait for the timeout."))
# Send twice, first returns an echo for smart verification
reply = dbb_client.hid_send_encrypt(msg)
@ -660,7 +679,8 @@ class DigitalBitboxPlugin(HW_PluginBase):
def create_client(self, device, handler):
if device.interface_number == 0 or device.usage_page == 0xffff:
self.handler = handler
if handler:
self.handler = handler
client = self.get_dbb_device(device)
if client is not None:
client = DigitalBitbox_Client(self, client)
@ -669,12 +689,13 @@ class DigitalBitboxPlugin(HW_PluginBase):
return None
def setup_device(self, device_info, wizard):
def setup_device(self, device_info, wizard, purpose):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
client.handler = self.create_handler(wizard)
client.setupRunning = True
if purpose == HWD_SETUP_NEW_WALLET:
client.setupRunning = True
client.get_xpub("m/44'/133'", 'standard')
@ -697,6 +718,8 @@ class DigitalBitboxPlugin(HW_PluginBase):
def get_xpub(self, device_id, derivation, xtype, wizard):
if xtype not in ('standard', 'p2wpkh-p2sh', 'p2wpkh'):
raise ScriptTypeNotSupported(_('This type of script is not supported with the Digital Bitbox.'))
devmgr = self.device_manager()
client = devmgr.client_by_id(device_id)
client.handler = self.create_handler(wizard)
@ -713,3 +736,13 @@ class DigitalBitboxPlugin(HW_PluginBase):
if client is not None:
client.check_device_dialog()
return client
def show_address(self, wallet, keystore, address):
change, index = wallet.get_address_index(address)
keypath = '%s/%d/%d' % (keystore.derivation, change, index)
xpub = self.get_client(keystore)._get_xpub(keypath)
verify_request_payload = {
"type": 'p2pkh',
"echo": xpub['echo'],
}
self.comserver_post_notification(verify_request_payload)

View File

@ -1,3 +1,5 @@
from functools import partial
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from .digitalbitbox import DigitalBitboxPlugin
@ -30,16 +32,9 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase):
if len(addrs) == 1:
def show_address():
change, index = wallet.get_address_index(addrs[0])
keypath = '%s/%d/%d' % (keystore.derivation, change, index)
xpub = self.get_client(keystore)._get_xpub(keypath)
verify_request_payload = {
"type": 'p2pkh',
"echo": xpub['echo'],
}
self.comserver_post_notification(verify_request_payload)
keystore.thread.add(partial(self.show_address, wallet, keystore, addrs[0]))
menu.addAction(_("Show on %s") % self.device, show_address)
menu.addAction(_("Show on {}").format(self.device), show_address)
class DigitalBitbox_Handler(QtHandlerBase):

View File

@ -22,11 +22,13 @@
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import random
import time
import threading
import base64
from functools import partial
import traceback
import sys
import smtplib
import imaplib
@ -37,17 +39,18 @@ from email.encoders import encode_base64
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import PyQt5.QtGui as QtGui
from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QLineEdit)
from PyQt5.QtWidgets import (QVBoxLayout, QLabel, QGridLayout, QLineEdit,
QInputDialog)
from electrum_zcash.plugins import BasePlugin, hook
from electrum_zcash.paymentrequest import PaymentRequest
from electrum_zcash.i18n import _
from electrum_zcash_gui.qt.util import EnterButton, Buttons, CloseButton
from electrum_zcash_gui.qt.util import OkButton, WindowModalDialog
from electrum_zcash.util import PrintError
from electrum_zcash_gui.qt.util import (EnterButton, Buttons, CloseButton, OkButton,
WindowModalDialog, get_parent_main_window)
class Processor(threading.Thread):
class Processor(threading.Thread, PrintError):
polling_interval = 5*60
def __init__(self, imap_server, username, password, callback):
@ -57,6 +60,8 @@ class Processor(threading.Thread):
self.password = password
self.imap_server = imap_server
self.on_receive = callback
self.M = None
self.connect_wait = 100 # ms, between failed connection attempts
def poll(self):
try:
@ -64,9 +69,9 @@ class Processor(threading.Thread):
except:
return
typ, data = self.M.search(None, 'ALL')
for num in data[0].split():
for num in str(data[0], 'utf8').split():
typ, msg_data = self.M.fetch(num, '(RFC822)')
msg = email.message_from_string(msg_data[0][1])
msg = email.message_from_string(str(msg_data[0][1], 'utf8'))
p = msg.get_payload()
if not msg.is_multipart():
p = [p]
@ -78,13 +83,18 @@ class Processor(threading.Thread):
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()
try:
self.M = imaplib.IMAP4_SSL(self.imap_server)
self.M.login(self.username, self.password)
except BaseException as e:
self.print_error(e)
self.connect_wait *= 2
# Reconnect when host changes
while self.M and self.M.host == self.imap_server:
self.poll()
time.sleep(self.polling_interval)
time.sleep(random.randint(0, self.connect_wait))
def send(self, recipient, message, payment_request):
msg = MIMEMultipart()
@ -96,10 +106,13 @@ class Processor(threading.Thread):
encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="payreq.zec"')
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()
try:
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()
except BaseException as e:
self.print_error(e)
class QEmailSignalObject(QObject):
@ -127,19 +140,29 @@ class Plugin(BasePlugin):
self.processor.start()
self.obj = QEmailSignalObject()
self.obj.email_new_invoice_signal.connect(self.new_invoice)
self.wallets = set()
def on_receive(self, pr_str):
self.print_error('received payment request')
self.pr = PaymentRequest(pr_str)
self.obj.email_new_invoice_signal.emit()
@hook
def load_wallet(self, wallet, main_window):
self.wallets |= {wallet}
@hook
def close_wallet(self, wallet):
self.wallets -= {wallet}
def new_invoice(self):
self.parent.invoices.add(self.pr)
#window.update_invoices_list()
for wallet in self.wallets:
wallet.invoices.add(self.pr)
#main_window.invoice_list.update()
@hook
def receive_list_menu(self, menu, addr):
window = menu.parentWidget()
window = get_parent_main_window(menu)
menu.addAction(_("Send via e-mail"), lambda: self.send(window, addr))
def send(self, window, addr):
@ -152,20 +175,20 @@ class Plugin(BasePlugin):
pr = paymentrequest.make_request(self.config, r)
if not pr:
return
recipient, ok = QtGui.QInputDialog.getText(window, 'Send request', 'Email invoice to:')
recipient, ok = 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:
# FIXME this runs in the GUI thread and blocks it...
self.processor.send(recipient, message, payload)
except BaseException as e:
traceback.print_exc(file=sys.stderr)
window.show_message(str(e))
return
window.show_message(_('Request sent.'))
else:
window.show_message(_('Request sent.'))
def requires_settings(self):
return True
@ -178,7 +201,7 @@ class Plugin(BasePlugin):
d.setMinimumSize(500, 200)
vbox = QVBoxLayout(d)
vbox.addWidget(QLabel(_('Server hosting your email acount')))
vbox.addWidget(QLabel(_('Server hosting your email account')))
grid = QGridLayout()
vbox.addLayout(grid)
grid.addWidget(QLabel('Server (IMAP)'), 0, 0)
@ -204,9 +227,36 @@ class Plugin(BasePlugin):
server = str(server_e.text())
self.config.set_key('email_server', server)
self.imap_server = server
username = str(username_e.text())
self.config.set_key('email_username', username)
self.username = username
password = str(password_e.text())
self.config.set_key('email_password', password)
self.password = password
check_connection = CheckConnectionThread(server, username, password)
check_connection.connection_error_signal.connect(lambda e: window.show_message(
_("Unable to connect to mail server:\n {}").format(e) + "\n" +
_("Please check your connection and credentials.")
))
check_connection.start()
class CheckConnectionThread(QThread):
connection_error_signal = pyqtSignal(str)
def __init__(self, server, username, password):
super().__init__()
self.server = server
self.username = username
self.password = password
def run(self):
try:
conn = imaplib.IMAP4_SSL(self.server)
conn.login(self.username, self.password)
except BaseException as e:
self.connection_error_signal.emit(str(e))

View File

@ -32,6 +32,9 @@ class CmdLineHandler:
def show_message(self, msg, on_cancel=None):
print_msg(msg)
def show_error(self, msg):
print_msg(msg)
def update_status(self, b):
print_error('trezor status', b)

View File

@ -51,3 +51,10 @@ class HW_PluginBase(BasePlugin):
for keystore in wallet.get_keystores():
if isinstance(keystore, self.keystore_class):
self.device_manager().unpair_xpub(keystore.xpub)
def setup_device(self, device_info, wizard, purpose):
"""Called when creating a new wallet or when using the device to decrypt
an existing wallet. Select the device to use. If the device is
uninitialized, go through the initialization process.
"""
raise NotImplementedError()

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
# -*- mode: python -*-
#
# Electrum - lightweight Bitcoin client
@ -34,7 +34,7 @@ from electrum_zcash.i18n import _
from electrum_zcash.util import PrintError
# The trickiest thing about this handler was getting windows properly
# parented on MacOSX.
# parented on macOS.
class QtHandlerBase(QObject, PrintError):
'''An interface between the GUI (here, QT) and the device handling
logic for handling I/O.'''
@ -70,9 +70,10 @@ class QtHandlerBase(QObject, PrintError):
self.status_signal.emit(paired)
def _update_status(self, paired):
button = self.button
icon = button.icon_paired if paired else button.icon_unpaired
button.setIcon(QIcon(icon))
if hasattr(self, 'button'):
button = self.button
icon = button.icon_paired if paired else button.icon_unpaired
button.setIcon(QIcon(icon))
def query_choice(self, msg, labels):
self.done.clear()
@ -143,7 +144,7 @@ class QtHandlerBase(QObject, PrintError):
def message_dialog(self, msg, on_cancel):
# Called more than once during signing, to confirm output and fee
self.clear_dialog()
title = _('Please check your %s device') % self.device
title = _('Please check your {} device').format(self.device)
self.dialog = dialog = WindowModalDialog(self.top_level_window(), title)
l = QLabel(msg)
vbox = QVBoxLayout(dialog)
@ -183,10 +184,12 @@ class QtPluginBase(object):
if not isinstance(keystore, self.keystore_class):
continue
if not self.libraries_available:
window.show_error(
_("Cannot find python library for") + " '%s'.\n" % self.name \
+ _("Make sure you install it with python3")
)
if hasattr(self, 'libraries_available_message'):
message = self.libraries_available_message + '\n'
else:
message = _("Cannot find python library for") + " '%s'.\n" % self.name
message += _("Make sure you install it with python3")
window.show_error(message)
return
tooltip = self.device + '\n' + (keystore.label or 'unnamed')
cb = partial(self.show_settings_dialog, window, keystore)

View File

@ -11,15 +11,15 @@ class GuiMixin(object):
# Requires: self.proto, self.device
messages = {
3: _("Confirm the transaction output on your %s device"),
4: _("Confirm internal entropy on your %s device to begin"),
5: _("Write down the seed word shown on your %s"),
6: _("Confirm on your %s that you want to wipe it clean"),
7: _("Confirm on your %s device the message to sign"),
3: _("Confirm the transaction output on your {} device"),
4: _("Confirm internal entropy on your {} device to begin"),
5: _("Write down the seed word shown on your {}"),
6: _("Confirm on your {} that you want to wipe it clean"),
7: _("Confirm on your {} device the message to sign"),
8: _("Confirm the total amount spent and the transaction fee on your "
"%s device"),
10: _("Confirm wallet address on your %s device"),
'default': _("Check your %s device to continue"),
"{} device"),
10: _("Confirm wallet address on your {} device"),
'default': _("Check your {} device to continue"),
}
def callback_Failure(self, msg):
@ -38,18 +38,21 @@ class GuiMixin(object):
message = self.msg
if not message:
message = self.messages.get(msg.code, self.messages['default'])
self.handler.show_message(message % self.device, self.cancel)
self.handler.show_message(message.format(self.device), self.cancel)
return self.proto.ButtonAck()
def callback_PinMatrixRequest(self, msg):
if msg.type == 2:
msg = _("Enter a new PIN for your %s:")
msg = _("Enter a new PIN for your {}:")
elif msg.type == 3:
msg = (_("Re-enter the new PIN for your %s.\n\n"
msg = (_("Re-enter the new PIN for your {}.\n\n"
"NOTE: the positions of the numbers have changed!"))
else:
msg = _("Enter your current %s PIN:")
pin = self.handler.get_pin(msg % self.device)
msg = _("Enter your current {} PIN:")
pin = self.handler.get_pin(msg.format(self.device))
if len(pin) > 9:
self.handler.show_error(_('The PIN cannot be longer than 9 characters.'))
pin = '' # to cancel below
if not pin:
return self.proto.Cancel()
return self.proto.PinMatrixAck(pin=pin)
@ -57,21 +60,27 @@ class GuiMixin(object):
def callback_PassphraseRequest(self, req):
if self.creating_wallet:
msg = _("Enter a passphrase to generate this wallet. Each time "
"you use this wallet your %s will prompt you for the "
"you use this wallet your {} will prompt you for the "
"passphrase. If you forget the passphrase you cannot "
"access the Zcash coins in the wallet.") % self.device
"access the Zcash coins in the wallet.").format(self.device)
else:
msg = _("Enter the passphrase to unlock this wallet:")
passphrase = self.handler.get_passphrase(msg, self.creating_wallet)
if passphrase is None:
return self.proto.Cancel()
passphrase = bip39_normalize_passphrase(passphrase)
return self.proto.PassphraseAck(passphrase=passphrase)
ack = self.proto.PassphraseAck(passphrase=passphrase)
length = len(ack.passphrase)
if length > 50:
self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length))
return self.proto.Cancel()
return ack
def callback_WordRequest(self, msg):
self.step += 1
msg = _("Step %d/24. Enter seed word as explained on "
"your %s:") % (self.step, self.device)
msg = _("Step {}/24. Enter seed word as explained on "
"your {}:").format(self.step, self.device)
word = self.handler.get_word(msg)
# Unfortunately the device can't handle self.proto.Cancel()
return self.proto.WordAck(word=word)
@ -110,6 +119,14 @@ class KeepKeyClientBase(GuiMixin, PrintError):
def is_pairable(self):
return not self.features.bootloader_mode
def has_usable_connection_with_device(self):
try:
res = self.ping("electrum-zcash pinging device")
assert res == "electrum-zcash pinging device"
except BaseException:
return False
return True
def used(self):
self.last_operation = time.time()
@ -126,7 +143,7 @@ class KeepKeyClientBase(GuiMixin, PrintError):
def expand_path(n):
'''Convert bip32 path to list of uint32 integers with prime flags
0/-1/1' -> [0, 0x80000001, 0x80000001]'''
# This code is similar to code in trezorlib where it unforunately
# This code is similar to code in trezorlib where it unfortunately
# is not declared as a staticmethod. Our n has an extra element.
PRIME_DERIVATION_FLAG = 0x80000000
path = []
@ -155,27 +172,27 @@ class KeepKeyClientBase(GuiMixin, PrintError):
def toggle_passphrase(self):
if self.features.passphrase_protection:
self.msg = _("Confirm on your %s device to disable passphrases")
self.msg = _("Confirm on your {} device to disable passphrases")
else:
self.msg = _("Confirm on your %s device to enable passphrases")
self.msg = _("Confirm on your {} device to enable passphrases")
enabled = not self.features.passphrase_protection
self.apply_settings(use_passphrase=enabled)
def change_label(self, label):
self.msg = _("Confirm the new label on your %s device")
self.msg = _("Confirm the new label on your {} device")
self.apply_settings(label=label)
def change_homescreen(self, homescreen):
self.msg = _("Confirm on your %s device to change your home screen")
self.msg = _("Confirm on your {} device to change your home screen")
self.apply_settings(homescreen=homescreen)
def set_pin(self, remove):
if remove:
self.msg = _("Confirm on your %s device to disable PIN protection")
self.msg = _("Confirm on your {} device to disable PIN protection")
elif self.features.pin_protection:
self.msg = _("Confirm on your %s device to change your PIN")
self.msg = _("Confirm on your {} device to change your PIN")
else:
self.msg = _("Confirm on your %s device to set a PIN")
self.msg = _("Confirm on your {} device to set a PIN")
self.change_pin(remove)
def clear_session(self):
@ -188,7 +205,6 @@ class KeepKeyClientBase(GuiMixin, PrintError):
except BaseException as e:
# If the device was removed it has the same effect...
self.print_error("clear_session: ignoring error", str(e))
pass
def get_public_node(self, address_n, creating):
self.creating_wallet = creating

View File

@ -9,3 +9,6 @@ class Plugin(KeepKeyPlugin):
if not isinstance(keystore, self.keystore_class):
return
keystore.handler = self.handler
def create_handler(self, window):
return self.handler

View File

@ -1,14 +1,14 @@
import threading
from binascii import hexlify, unhexlify
from electrum_zcash.util import bfh, bh2u
from electrum_zcash.bitcoin import (b58_address_to_hash160, xpub_from_pubkey,
TYPE_ADDRESS, TYPE_SCRIPT, NetworkConstants)
TYPE_ADDRESS, TYPE_SCRIPT)
from electrum_zcash import constants
from electrum_zcash.i18n import _
from electrum_zcash.plugins import BasePlugin
from electrum_zcash.transaction import deserialize
from electrum_zcash.transaction import deserialize, Transaction
from electrum_zcash.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
from electrum_zcash.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase
@ -25,7 +25,7 @@ class KeepKeyCompatibleKeyStore(Hardware_KeyStore):
return self.plugin.get_client(self, force_pair)
def decrypt_message(self, sequence, message, password):
raise RuntimeError(_('Encryption and decryption are not implemented by %s') % self.device)
raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device))
def sign_message(self, sequence, message, password):
client = self.get_client()
@ -44,6 +44,8 @@ class KeepKeyCompatibleKeyStore(Hardware_KeyStore):
for txin in tx.inputs():
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
tx_hash = txin['prevout_hash']
if txin.get('prev_tx') is None:
raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
prev_tx[tx_hash] = txin['prev_tx']
for x_pubkey in x_pubkeys:
if not is_xpubkey(x_pubkey):
@ -66,8 +68,6 @@ class KeepKeyCompatiblePlugin(HW_PluginBase):
def __init__(self, parent, config, name):
HW_PluginBase.__init__(self, parent, config, name)
self.main_thread = threading.current_thread()
# FIXME: move to base class when Ledger is fixed
if self.libraries_available:
self.device_manager().register_devices(self.DEVICE_IDS)
@ -95,7 +95,7 @@ class KeepKeyCompatiblePlugin(HW_PluginBase):
return None
def create_client(self, device, handler):
# disable bridge because it seems to never returns if keepkey is plugged
# disable bridge because it seems to never returns if KeepKey is plugged
#transport = self._try_bridge(device) or self._try_hid(device)
transport = self._try_hid(device)
if not transport:
@ -114,9 +114,9 @@ class KeepKeyCompatiblePlugin(HW_PluginBase):
return None
if not client.atleast_version(*self.minimum_firmware):
msg = (_('Outdated %s firmware for device labelled %s. Please '
'download the updated firmware from %s') %
(self.device, client.label(), self.firmware_URL))
msg = (_('Outdated {} firmware for device labelled {}. Please '
'download the updated firmware from {}')
.format(self.device, client.label(), self.firmware_URL))
self.print_error(msg)
handler.show_error(msg)
return None
@ -134,18 +134,18 @@ class KeepKeyCompatiblePlugin(HW_PluginBase):
return client
def get_coin_name(self):
return "Zcash Testnet" if NetworkConstants.TESTNET else "Zcash"
return "ZcashTestnet" if constants.net.TESTNET else "Zcash"
def initialize_device(self, device_id, wizard, handler):
# Initialization method
msg = _("Choose how you want to initialize your %s.\n\n"
msg = _("Choose how you want to initialize your {}.\n\n"
"The first two methods are secure as no secret information "
"is entered into your computer.\n\n"
"For the last two methods you input secrets on your keyboard "
"and upload them to your %s, and so you should "
"and upload them to your {}, and so you should "
"only do those on a computer you know to be trustworthy "
"and free of malware."
) % (self.device, self.device)
).format(self.device, self.device)
choices = [
# Must be short as QT doesn't word-wrap radio button text
(TIM_NEW, _("Let the device generate a completely new seed randomly")),
@ -189,10 +189,7 @@ class KeepKeyCompatiblePlugin(HW_PluginBase):
label, language)
wizard.loop.exit(0)
def setup_device(self, device_info, wizard):
'''Called when creating a new wallet. Select the device to use. If
the device is uninitialized, go through the intialization
process.'''
def setup_device(self, device_info, wizard, purpose):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
@ -204,6 +201,8 @@ class KeepKeyCompatiblePlugin(HW_PluginBase):
client.used()
def get_xpub(self, device_id, derivation, xtype, wizard):
if xtype not in ('standard',):
raise ScriptTypeNotSupported(_('This type of script is not supported with KeepKey.'))
devmgr = self.device_manager()
client = devmgr.client_by_id(device_id)
client.handler = wizard
@ -297,53 +296,80 @@ class KeepKeyCompatiblePlugin(HW_PluginBase):
return inputs
def tx_outputs(self, derivation, tx):
def create_output_by_derivation(info):
index, xpubs, m = info
if len(xpubs) == 1:
script_type = self.types.PAYTOADDRESS
address_n = self.client_class.expand_path(derivation + "/%d/%d" % index)
txoutputtype = self.types.TxOutputType(
amount=amount,
script_type=script_type,
address_n=address_n,
)
else:
script_type = self.types.PAYTOMULTISIG
address_n = self.client_class.expand_path("/%d/%d" % index)
nodes = map(self.ckd_public.deserialize, xpubs)
pubkeys = [self.types.HDNodePathType(node=node, address_n=address_n) for node in nodes]
multisig = self.types.MultisigRedeemScriptType(
pubkeys=pubkeys,
signatures=[b''] * len(pubkeys),
m=m)
txoutputtype = self.types.TxOutputType(
multisig=multisig,
amount=amount,
address_n=self.client_class.expand_path(derivation + "/%d/%d" % index),
script_type=script_type)
return txoutputtype
def create_output_by_address():
txoutputtype = self.types.TxOutputType()
txoutputtype.amount = amount
if _type == TYPE_SCRIPT:
txoutputtype.script_type = self.types.PAYTOOPRETURN
txoutputtype.op_return_data = address[2:]
elif _type == TYPE_ADDRESS:
addrtype, hash_160 = b58_address_to_hash160(address)
if addrtype == constants.net.ADDRTYPE_P2PKH:
txoutputtype.script_type = self.types.PAYTOADDRESS
elif addrtype == constants.net.ADDRTYPE_P2SH:
txoutputtype.script_type = self.types.PAYTOSCRIPTHASH
else:
raise Exception('addrtype: ' + str(addrtype))
txoutputtype.address = address
return txoutputtype
def is_any_output_on_change_branch():
for _type, address, amount in tx.outputs():
info = tx.output_info.get(address)
if info is not None:
index, xpubs, m = info
if index[0] == 1:
return True
return False
outputs = []
has_change = False
any_output_on_change_branch = is_any_output_on_change_branch()
for _type, address, amount in tx.outputs():
use_create_by_derivation = False
info = tx.output_info.get(address)
if info is not None and not has_change:
has_change = True # no more than one change address
addrtype, hash_160 = b58_address_to_hash160(address)
index, xpubs, m = info
if len(xpubs) == 1:
script_type = self.types.PAYTOADDRESS
address_n = self.client_class.expand_path(derivation + "/%d/%d"%index)
txoutputtype = self.types.TxOutputType(
amount = amount,
script_type = script_type,
address_n = address_n,
)
else:
script_type = self.types.PAYTOMULTISIG
address_n = self.client_class.expand_path("/%d/%d"%index)
nodes = map(self.ckd_public.deserialize, xpubs)
pubkeys = [ self.types.HDNodePathType(node=node, address_n=address_n) for node in nodes]
multisig = self.types.MultisigRedeemScriptType(
pubkeys = pubkeys,
signatures = [b''] * len(pubkeys),
m = m)
txoutputtype = self.types.TxOutputType(
multisig = multisig,
amount = amount,
address_n = self.client_class.expand_path(derivation + "/%d/%d"%index),
script_type = script_type)
else:
txoutputtype = self.types.TxOutputType()
txoutputtype.amount = amount
if _type == TYPE_SCRIPT:
txoutputtype.script_type = self.types.PAYTOOPRETURN
txoutputtype.op_return_data = address[2:]
elif _type == TYPE_ADDRESS:
addrtype, hash_160 = b58_address_to_hash160(address)
if addrtype == NetworkConstants.ADDRTYPE_P2PKH:
txoutputtype.script_type = self.types.PAYTOADDRESS
elif addrtype == NetworkConstants.ADDRTYPE_P2SH:
txoutputtype.script_type = self.types.PAYTOSCRIPTHASH
else:
raise BaseException('addrtype: ' + str(addrtype))
txoutputtype.address = address
on_change_branch = index[0] == 1
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
if on_change_branch == any_output_on_change_branch:
use_create_by_derivation = True
has_change = True
if use_create_by_derivation:
txoutputtype = create_output_by_derivation(info)
else:
txoutputtype = create_output_by_address()
outputs.append(txoutputtype)
return outputs
@ -361,7 +387,7 @@ class KeepKeyCompatiblePlugin(HW_PluginBase):
o.script_pubkey = bfh(vout['scriptPubKey'])
return t
# This function is called from the trezor libraries (via tx_api)
# This function is called from the TREZOR libraries (via tx_api)
def get_tx(self, tx_hash):
tx = self.prev_tx[tx_hash]
return self.electrum_tx_to_txtype(tx)

View File

@ -194,7 +194,7 @@ class QtPlugin(QtPluginBase):
if type(keystore) == self.keystore_class and len(addrs) == 1:
def show_address():
keystore.thread.add(partial(self.show_address, wallet, addrs[0]))
menu.addAction(_("Show on %s") % self.device, show_address)
menu.addAction(_("Show on {}").format(self.device), show_address)
def show_settings_dialog(self, window, keystore):
device_id = self.choose_device(window, keystore)
@ -227,7 +227,7 @@ class QtPlugin(QtPluginBase):
bg = QButtonGroup()
for i, count in enumerate([12, 18, 24]):
rb = QRadioButton(gb)
rb.setText(_("%d words") % count)
rb.setText(_("{} words").format(count))
bg.addButton(rb)
bg.setId(rb, i)
hbox1.addWidget(rb)
@ -250,7 +250,7 @@ class QtPlugin(QtPluginBase):
vbox.addWidget(QLabel(msg))
vbox.addWidget(text)
pin = QLineEdit()
pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,10}')))
pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}')))
pin.setMaximumWidth(100)
hbox_pin = QHBoxLayout()
hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):")))
@ -292,7 +292,7 @@ class SettingsDialog(WindowModalDialog):
their PIN.'''
def __init__(self, window, plugin, keystore, device_id):
title = _("%s Settings") % plugin.device
title = _("{} Settings").format(plugin.device)
super(SettingsDialog, self).__init__(window, title)
self.setMaximumWidth(540)
@ -457,9 +457,9 @@ class SettingsDialog(WindowModalDialog):
settings_glayout = QGridLayout()
# Settings tab - Label
label_msg = QLabel(_("Name this %s. If you have mutiple devices "
label_msg = QLabel(_("Name this {}. If you have multiple devices "
"their labels help distinguish them.")
% plugin.device)
.format(plugin.device))
label_msg.setWordWrap(True)
label_label = QLabel(_("Device Label"))
label_edit = QLineEdit()
@ -482,7 +482,7 @@ class SettingsDialog(WindowModalDialog):
pin_msg = QLabel(_("PIN protection is strongly recommended. "
"A PIN is your only protection against someone "
"stealing your Zcash coins if they obtain physical "
"access to your %s.") % plugin.device)
"access to your {}.").format(plugin.device))
pin_msg.setWordWrap(True)
pin_msg.setStyleSheet("color: red")
settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
@ -497,8 +497,8 @@ class SettingsDialog(WindowModalDialog):
homescreen_clear_button.clicked.connect(clear_homescreen)
homescreen_msg = QLabel(_("You can set the homescreen on your "
"device to personalize it. You must "
"choose a %d x %d monochrome black and "
"white image.") % (hs_rows, hs_cols))
"choose a {} x {} monochrome black and "
"white image.").format(hs_rows, hs_cols))
homescreen_msg.setWordWrap(True)
settings_glayout.addWidget(homescreen_label, 4, 0)
settings_glayout.addWidget(homescreen_change_button, 4, 1)
@ -541,7 +541,7 @@ class SettingsDialog(WindowModalDialog):
clear_pin_button.clicked.connect(clear_pin)
clear_pin_warning = QLabel(
_("If you disable your PIN, anyone with physical access to your "
"%s device can spend your Zcash coins.") % plugin.device)
"{} device can spend your Zcash coins.").format(plugin.device))
clear_pin_warning.setWordWrap(True)
clear_pin_warning.setStyleSheet("color: red")
advanced_glayout.addWidget(clear_pin_button, 0, 2)

View File

@ -16,7 +16,7 @@ class LabelsPlugin(BasePlugin):
def __init__(self, parent, config, name):
BasePlugin.__init__(self, parent, config, name)
self.target_host = 'labels.bauerj.eu'
self.target_host = 'labels.electrum.org'
self.wallets = {}
def encode(self, wallet, msg):
@ -45,7 +45,7 @@ class LabelsPlugin(BasePlugin):
@hook
def set_label(self, wallet, item, label):
if not wallet in self.wallets:
if wallet not in self.wallets:
return
if not item:
return
@ -55,7 +55,7 @@ class LabelsPlugin(BasePlugin):
"walletNonce": nonce,
"externalId": self.encode(wallet, item),
"encryptedLabel": self.encode(wallet, label)}
t = threading.Thread(target=self.do_request,
t = threading.Thread(target=self.do_request_safe,
args=["POST", "/label", False, bundle])
t.setDaemon(True)
t.start()
@ -72,14 +72,24 @@ class LabelsPlugin(BasePlugin):
kwargs['headers']['Content-Type'] = 'application/json'
response = requests.request(method, url, **kwargs)
if response.status_code != 200:
raise BaseException(response.status_code, response.text)
raise Exception(response.status_code, response.text)
response = response.json()
if "error" in response:
raise BaseException(response["error"])
raise Exception(response["error"])
return response
def do_request_safe(self, *args, **kwargs):
try:
self.do_request(*args, **kwargs)
except BaseException as e:
#traceback.print_exc(file=sys.stderr)
self.print_error('error doing request')
def push_thread(self, wallet):
wallet_id = self.wallets[wallet][2]
wallet_data = self.wallets.get(wallet, None)
if not wallet_data:
raise Exception('Wallet {} not loaded'.format(wallet))
wallet_id = wallet_data[2]
bundle = {"labels": [],
"walletId": wallet_id,
"walletNonce": self.get_nonce(wallet)}
@ -95,42 +105,47 @@ class LabelsPlugin(BasePlugin):
self.do_request("POST", "/labels", True, bundle)
def pull_thread(self, wallet, force):
wallet_id = self.wallets[wallet][2]
wallet_data = self.wallets.get(wallet, None)
if not wallet_data:
raise Exception('Wallet {} not loaded'.format(wallet))
wallet_id = wallet_data[2]
nonce = 1 if force else self.get_nonce(wallet) - 1
self.print_error("asking for labels since nonce", nonce)
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)
self.set_nonce(wallet, response["nonce"] + 1)
self.on_pulled(wallet)
def pull_thread_safe(self, wallet, force):
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)
self.set_nonce(wallet, response["nonce"] + 1)
self.on_pulled(wallet)
except Exception as e:
traceback.print_exc(file=sys.stderr)
self.print_error("could not retrieve labels")
self.pull_thread(wallet, force)
except BaseException as e:
# traceback.print_exc(file=sys.stderr)
self.print_error('could not retrieve labels')
def start_wallet(self, wallet):
nonce = self.get_nonce(wallet)
@ -144,7 +159,7 @@ class LabelsPlugin(BasePlugin):
wallet_id = hashlib.sha256(mpk).hexdigest()
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 = threading.Thread(target=self.pull_thread_safe, args=(wallet, False))
t.setDaemon(True)
t.start()

View File

@ -1,4 +1,6 @@
from functools import partial
import traceback
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
@ -37,10 +39,12 @@ class Plugin(LabelsPlugin):
hbox.addWidget(QLabel("Label sync options:"))
upload = ThreadedButton("Force upload",
partial(self.push_thread, wallet),
partial(self.done_processing, d))
partial(self.done_processing_success, d),
partial(self.done_processing_error, d))
download = ThreadedButton("Force download",
partial(self.pull_thread, wallet, True),
partial(self.done_processing, d))
partial(self.done_processing_success, d),
partial(self.done_processing_error, d))
vbox = QVBoxLayout()
vbox.addWidget(upload)
vbox.addWidget(download)
@ -54,13 +58,20 @@ class Plugin(LabelsPlugin):
def on_pulled(self, wallet):
self.obj.labels_changed_signal.emit(wallet)
def done_processing(self, dialog, result):
def done_processing_success(self, dialog, result):
dialog.show_message(_("Your labels have been synchronised."))
def done_processing_error(self, dialog, result):
traceback.print_exception(*result, file=sys.stderr)
dialog.show_error(_("Error synchronising labels") + ':\n' + str(result[:2]))
@hook
def on_new_window(self, window):
def load_wallet(self, wallet, window):
# FIXME if the user just enabled the plugin, this hook won't be called
# as the wallet is already loaded, and hence the plugin will be in
# a non-functional state for that window
self.obj.labels_changed_signal.connect(window.update_tabs)
self.start_wallet(window.wallet)
self.start_wallet(wallet)
@hook
def on_close_window(self, window):

View File

@ -1,16 +1,24 @@
import os
import hashlib
import logging
import json
import copy
from binascii import hexlify, unhexlify
import websocket
from PyQt5.Qt import QDialog, QLineEdit, QTextEdit, QVBoxLayout, QLabel
import PyQt5.QtCore as QtCore
from PyQt5.QtWidgets import *
from btchip.btchip import *
from electrum_zcash.i18n import _
from electrum_zcash_gui.qt.util import *
from electrum_zcash.util import print_msg
import os, hashlib, websocket, logging, json, copy
from electrum_zcash import constants, bitcoin
from electrum_zcash_gui.qt.qrcodewidget import QRCodeWidget
from btchip.btchip import *
DEBUG = False
@ -24,7 +32,7 @@ helpTxt = [_("Your Ledger Wallet wants to tell you a one-time PIN code.<br><br>"
_("Waiting for authentication on your mobile phone"),
_("Transaction accepted by mobile phone. Waiting for confirmation."),
_("Click Pair button to begin pairing a mobile phone."),
_("Scan this QR code with your LedgerWallet phone app to pair it with this Ledger device.<br>"
_("Scan this QR code with your Ledger Wallet phone app to pair it with this Ledger device.<br>"
"To complete pairing you will need your security card to answer a challenge." )
]
@ -37,7 +45,7 @@ class LedgerAuthDialog(QDialog):
self.handler = handler
self.txdata = data
self.idxs = self.txdata['keycardData'] if self.txdata['confirmationType'] > 1 else ''
self.setMinimumWidth(600)
self.setMinimumWidth(650)
self.setWindowTitle(_("Ledger Wallet Authentication"))
self.cfg = copy.deepcopy(self.handler.win.wallet.get_keystore().cfg)
self.dongle = self.handler.win.wallet.get_keystore().get_client().dongle
@ -110,17 +118,23 @@ class LedgerAuthDialog(QDialog):
card = QVBoxLayout()
self.cardbox.setLayout(card)
self.addrtext = QTextEdit()
self.addrtext.setStyleSheet("QTextEdit { color:blue; background-color:lightgray; padding:15px 10px; border:none; font-size:20pt; }")
self.addrtext.setStyleSheet("QTextEdit { color:blue; background-color:lightgray; padding:15px 10px; border:none; font-size:20pt; font-family:monospace; }")
self.addrtext.setReadOnly(True)
self.addrtext.setMaximumHeight(120)
self.addrtext.setMaximumHeight(130)
card.addWidget(self.addrtext)
def pin_changed(s):
if len(s) < len(self.idxs):
i = self.idxs[len(s)]
addr = self.txdata['address']
addr = addr[:i] + '<u><b>' + addr[i:i+1] + '</u></b>' + addr[i+1:]
self.addrtext.setHtml(str(addr))
if not constants.net.TESTNET:
text = addr[:i] + '<u><b>' + addr[i:i+1] + '</u></b>' + addr[i+1:]
else:
# pin needs to be created from mainnet address
addr_mainnet = bitcoin.script_to_address(bitcoin.address_to_script(addr), net=constants.BitcoinMainnet)
addr_mainnet = addr_mainnet[:i] + '<u><b>' + addr_mainnet[i:i+1] + '</u></b>' + addr_mainnet[i+1:]
text = str(addr) + '\n' + str(addr_mainnet)
self.addrtext.setHtml(str(text))
else:
self.addrtext.setHtml(_("Press Enter"))
@ -164,7 +178,7 @@ class LedgerAuthDialog(QDialog):
if not self.cfg['pair']:
self.modes.addItem(_("Mobile - Not paired"))
else:
self.modes.addItem(_("Mobile - %s") % self.cfg['pair'][1])
self.modes.addItem(_("Mobile - {}").format(self.cfg['pair'][1]))
self.modes.blockSignals(False)
def update_dlg(self):
@ -179,8 +193,8 @@ class LedgerAuthDialog(QDialog):
self.pinbox.setVisible(self.cfg['mode'] == 0)
self.cardbox.setVisible(self.cfg['mode'] == 1)
self.pintxt.setFocus(True) if self.cfg['mode'] == 0 else self.cardtxt.setFocus(True)
self.setMaximumHeight(200)
self.setMaximumHeight(400)
def do_pairing(self):
rng = os.urandom(16)
pairID = (hexlify(rng) + hexlify(hashlib.sha256(rng).digest()[0:1])).decode('utf-8')
@ -338,11 +352,7 @@ class LedgerWebSocket(QThread):
ws.send( self.txreq )
debug_msg("Req Sent", self.txreq)
def debug_msg(*args):
if DEBUG:
print_msg(*args)

View File

@ -9,3 +9,6 @@ class Plugin(LedgerPlugin):
if not isinstance(keystore, self.keystore_class):
return
keystore.handler = self.handler
def create_handler(self, window):
return self.handler

View File

@ -4,15 +4,16 @@ import sys
import traceback
from electrum_zcash import bitcoin
from electrum_zcash import constants
from electrum_zcash.bitcoin import (TYPE_ADDRESS, int_to_hex, var_int,
b58_address_to_hash160,
hash160_to_b58_address, NetworkConstants)
hash160_to_b58_address)
from electrum_zcash.i18n import _
from electrum_zcash.plugins import BasePlugin
from electrum_zcash.keystore import Hardware_KeyStore
from electrum_zcash.transaction import Transaction
from ..hw_wallet import HW_PluginBase
from electrum_zcash.util import print_error, is_verbose, bfh, bh2u
from electrum_zcash.util import print_error, is_verbose, bfh, bh2u, versiontuple
def setAlternateCoinVersions(self, regular, p2sh):
@ -35,6 +36,8 @@ except ImportError:
MSG_NEEDS_FW_UPDATE_GENERIC = _('Firmware version too old. Please update at') + \
' https://www.ledgerwallet.com'
MULTI_OUTPUT_SUPPORT = '1.1.4'
ALTERNATIVE_COIN_VERSION = '1.0.1'
class Ledger_Client():
@ -60,6 +63,13 @@ class Ledger_Client():
def i4b(self, x):
return pack('>I', x)
def has_usable_connection_with_device(self):
try:
self.dongleObject.getFirmwareVersion()
except BaseException:
return False
return True
def test_pin_unlocked(func):
"""Function decorator to test the Ledger for being unlocked, and if not,
raise a human-readable exception.
@ -129,12 +139,12 @@ class Ledger_Client():
def perform_hw1_preflight(self):
try:
firmwareInfo = self.dongleObject.getFirmwareVersion()
firmware = firmwareInfo['version'].split(".")
self.canAlternateCoinVersions = (firmwareInfo['specialVersion'] >= 0x20 and
map(int, firmware) >= [1, 0, 1])
self.multiOutputSupported = int(firmware[0]) >= 1 and int(firmware[1]) >= 1 and int(firmware[2]) >= 4
firmware = firmwareInfo['version']
self.multiOutputSupported = versiontuple(firmware) >= versiontuple(MULTI_OUTPUT_SUPPORT)
self.canAlternateCoinVersions = (versiontuple(firmware) >= versiontuple(ALTERNATIVE_COIN_VERSION)
and firmwareInfo['specialVersion'] >= 0x20)
if not checkFirmware(firmware):
if not checkFirmware(firmwareInfo):
self.dongleObject.dongle.close()
raise Exception(MSG_NEEDS_FW_UPDATE_GENERIC)
try:
@ -158,13 +168,17 @@ class Ledger_Client():
pin = pin.encode()
self.dongleObject.verifyPin(pin)
if self.canAlternateCoinVersions:
self.dongleObject.setAlternateCoinVersions(NetworkConstants.ADDRTYPE_P2PKH,
NetworkConstants.ADDRTYPE_P2SH)
self.dongleObject.setAlternateCoinVersions(constants.net.ADDRTYPE_P2PKH,
constants.net.ADDRTYPE_P2SH)
except BTChipException as e:
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")
if e.sw == 0x6f00 and e.message == 'Invalid channel':
# based on docs 0x6f00 might be a more general error, hence we also compare message to be sure
raise Exception("Invalid channel.\n"
"Please make sure that 'Browser support' is disabled on your device.")
raise e
def checkDevice(self):
@ -172,8 +186,8 @@ class Ledger_Client():
try:
self.perform_hw1_preflight()
except BTChipException as e:
if (e.sw == 0x6d00):
raise BaseException("Device not in Zcash mode")
if (e.sw == 0x6d00 or e.sw == 0x6700):
raise Exception(_("Device not in Zcash mode")) from e
raise e
self.preflightDone = True
@ -221,6 +235,16 @@ class Ledger_KeyStore(Hardware_KeyStore):
self.client = None
raise Exception(message)
def set_and_unset_signing(func):
"""Function decorator to set and unset self.signing."""
def wrapper(self, *args, **kwargs):
try:
self.signing = True
return func(self, *args, **kwargs)
finally:
self.signing = False
return wrapper
def address_id_stripped(self, address):
# Strip the leading "m/"
change, index = self.get_address_index(address)
@ -229,15 +253,16 @@ class Ledger_KeyStore(Hardware_KeyStore):
return address_path[2:]
def decrypt_message(self, pubkey, message, password):
raise RuntimeError(_('Encryption and decryption are currently not supported for %s') % self.device)
raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device))
@set_and_unset_signing
def sign_message(self, sequence, message, password):
self.signing = True
message = message.encode('utf8')
message_hash = hashlib.sha256(message).hexdigest().upper()
# prompt for the PIN before displaying the dialog if necessary
client = self.get_client()
address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
self.handler.show_message("Signing message ...")
self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash)
try:
info = self.get_client().signMessagePrepare(address_path, message)
pin = ""
@ -250,16 +275,17 @@ class Ledger_KeyStore(Hardware_KeyStore):
except BTChipException as 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.")
elif e.sw == 0x6985: # cancelled by user
return b''
else:
self.give_error(e, True)
except UserWarning:
self.handler.show_error(_('Cancelled by user'))
return ''
return b''
except Exception as e:
self.give_error(e, True)
finally:
self.handler.finished()
self.signing = False
# Parse the ASN.1 signature
rLength = signature[3]
r = signature[4 : 4 + rLength]
@ -272,12 +298,11 @@ class Ledger_KeyStore(Hardware_KeyStore):
# And convert it
return bytes([27 + 4 + (signature[0] & 0x01)]) + r + s
@set_and_unset_signing
def sign_transaction(self, tx, password):
if tx.is_complete():
return
client = self.get_client()
self.signing = True
inputs = []
inputsPaths = []
pubKeys = []
@ -313,6 +338,9 @@ class Ledger_KeyStore(Hardware_KeyStore):
self.give_error("No matching x_key for sign_transaction") # should never happen
redeemScript = Transaction.get_preimage_script(txin)
if txin.get('prev_tx') is None: # and not Transaction.is_segwit_input(txin):
# note: offline signing does not work atm even with segwit inputs for ledger
raise Exception(_('Offline signing with {} is not supported.').format(self.device))
inputs.append([txin['prev_tx'].raw, txin['prevout_n'], redeemScript, txin['prevout_hash'], signingPos, txin.get('sequence', 0xffffffff - 1) ])
inputsPaths.append(hwAddress)
pubKeys.append(pubkeys)
@ -340,7 +368,8 @@ class Ledger_KeyStore(Hardware_KeyStore):
for _type, address, amount in tx.outputs():
assert _type == TYPE_ADDRESS
info = tx.output_info.get(address)
if (info is not None) and (len(tx.outputs()) != 1):
if (info is not None) and len(tx.outputs()) > 1 \
and info[0][0] == 1: # "is on 'change' branch"
index, xpubs, m = info
changePath = self.get_derivation()[2:] + "/%d/%d"%index
changeAmount = amount
@ -348,7 +377,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
output = address
if not self.get_client_electrum().canAlternateCoinVersions:
v, h = b58_address_to_hash160(address)
if v == NetworkConstants.ADDRTYPE_P2PKH:
if v == constants.net.ADDRTYPE_P2PKH:
output = hash160_to_b58_address(h, 0)
outputAmount = amount
@ -377,7 +406,12 @@ class Ledger_KeyStore(Hardware_KeyStore):
while inputIndex < len(inputs):
self.get_client().startUntrustedTransaction(firstTransaction, inputIndex,
chipInputs, redeemScripts[inputIndex])
outputData = self.get_client().finalizeInputFull(txOutput)
if changePath:
# we don't set meaningful outputAddress, amount and fees
# as we only care about the alternateEncoding==True branch
outputData = self.get_client().finalizeInput(b'', 0, 0, changePath, bfh(rawTx))
else:
outputData = self.get_client().finalizeInputFull(txOutput)
outputData['outputData'] = txOutput
if firstTransaction:
transactionOutput = outputData['outputData']
@ -400,6 +434,12 @@ class Ledger_KeyStore(Hardware_KeyStore):
except UserWarning:
self.handler.show_error(_('Cancelled by user'))
return
except BTChipException as e:
if e.sw == 0x6985: # cancelled by user
return
else:
traceback.print_exc(file=sys.stderr)
self.give_error(e, True)
except BaseException as e:
traceback.print_exc(file=sys.stdout)
self.give_error(e, True)
@ -410,8 +450,25 @@ class Ledger_KeyStore(Hardware_KeyStore):
signingPos = inputs[i][4]
txin['signatures'][signingPos] = bh2u(signatures[i])
tx.raw = tx.serialize()
self.signing = False
@set_and_unset_signing
def show_address(self, sequence, txin_type):
client = self.get_client()
address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
self.handler.show_message(_("Showing address ..."))
try:
client.getWalletPublicKey(address_path, showOnScreen=True)
except BTChipException as e:
if e.sw == 0x6985: # cancelled by user
pass
else:
traceback.print_exc(file=sys.stderr)
self.handler.show_error(e)
except BaseException as e:
traceback.print_exc(file=sys.stderr)
self.handler.show_error(e)
finally:
self.handler.finished()
class LedgerPlugin(HW_PluginBase):
libraries_available = BTCHIP
@ -431,31 +488,32 @@ class LedgerPlugin(HW_PluginBase):
if self.libraries_available:
self.device_manager().register_devices(self.DEVICE_IDS)
def btchip_is_connected(self, keystore):
try:
self.get_client(keystore).getFirmwareVersion()
except Exception as e:
return False
return True
def get_btchip_device(self, device):
ledger = False
if (device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c) or (device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c) or (device.product_key[0] == 0x2c97):
ledger = True
if device.product_key[0] == 0x2581 and device.product_key[1] == 0x3b7c:
ledger = True
if device.product_key[0] == 0x2581 and device.product_key[1] == 0x4b7c:
ledger = True
if device.product_key[0] == 0x2c97:
if device.interface_number == 0 or device.usage_page == 0xffa0:
ledger = True
else:
return None # non-compatible interface of a Nano S or Blue
dev = hid.device()
dev.open_path(device.path)
dev.set_nonblocking(True)
return HIDDongleHIDAPI(dev, ledger, BTCHIP_DEBUG)
def create_client(self, device, handler):
self.handler = handler
if handler:
self.handler = handler
client = self.get_btchip_device(device)
if client is not None:
client = Ledger_Client(client)
return client
def setup_device(self, device_info, wizard):
def setup_device(self, device_info, wizard, purpose):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
@ -472,7 +530,6 @@ class LedgerPlugin(HW_PluginBase):
def get_client(self, keystore, force_pair=True):
# All client interaction should not be in the main GUI thread
#assert self.main_thread != threading.current_thread()
devmgr = self.device_manager()
handler = keystore.handler
with devmgr.hid_lock:
@ -483,3 +540,8 @@ class LedgerPlugin(HW_PluginBase):
if client is not None:
client.checkDevice()
return client
def show_address(self, wallet, address):
sequence = wallet.get_address_index(address)
txin_type = wallet.get_txin_type(address)
wallet.get_keystore().show_address(sequence, txin_type)

View File

@ -1,13 +1,13 @@
import threading
from PyQt5.Qt import QInputDialog, QLineEdit, QVBoxLayout, QLabel
#from btchip.btchipPersoWizard import StartBTChipPersoDialog
from electrum_zcash.i18n import _
from .ledger import LedgerPlugin
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from electrum_zcash.plugins import hook
from electrum_zcash.wallet import Standard_Wallet
from electrum_zcash_gui.qt.util import *
#from btchip.btchipPersoWizard import StartBTChipPersoDialog
from .ledger import LedgerPlugin
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
class Plugin(LedgerPlugin, QtPluginBase):
icon_unpaired = ":icons/ledger_unpaired.png"
@ -16,6 +16,16 @@ class Plugin(LedgerPlugin, QtPluginBase):
def create_handler(self, window):
return Ledger_Handler(window)
@hook
def receive_menu(self, menu, addrs, wallet):
if type(wallet) is not Standard_Wallet:
return
keystore = wallet.get_keystore()
if type(keystore) == self.keystore_class and len(addrs) == 1:
def show_address():
keystore.thread.add(partial(self.show_address, wallet, addrs[0]))
menu.addAction(_("Show on Ledger"), show_address)
class Ledger_Handler(QtHandlerBase):
setup_signal = pyqtSignal()
auth_signal = pyqtSignal(object)
@ -65,11 +75,7 @@ class Ledger_Handler(QtHandlerBase):
return
def setup_dialog(self):
self.show_error(_('Initialization of Ledger HW devices is currently disabled.'))
return
dialog = StartBTChipPersoDialog()
dialog.exec_()

View File

@ -3,8 +3,8 @@ from .clientbase import TrezorClientBase
class TrezorClient(TrezorClientBase, ProtocolMixin, BaseClient):
def __init__(self, transport, handler, plugin):
BaseClient.__init__(self, transport)
ProtocolMixin.__init__(self, transport)
BaseClient.__init__(self, transport=transport)
ProtocolMixin.__init__(self, transport=transport)
TrezorClientBase.__init__(self, handler, plugin, proto)

View File

@ -11,15 +11,15 @@ class GuiMixin(object):
# Requires: self.proto, self.device
messages = {
3: _("Confirm the transaction output on your %s device"),
4: _("Confirm internal entropy on your %s device to begin"),
5: _("Write down the seed word shown on your %s"),
6: _("Confirm on your %s that you want to wipe it clean"),
7: _("Confirm on your %s device the message to sign"),
3: _("Confirm the transaction output on your {} device"),
4: _("Confirm internal entropy on your {} device to begin"),
5: _("Write down the seed word shown on your {}"),
6: _("Confirm on your {} that you want to wipe it clean"),
7: _("Confirm on your {} device the message to sign"),
8: _("Confirm the total amount spent and the transaction fee on your "
"%s device"),
10: _("Confirm wallet address on your %s device"),
'default': _("Check your %s device to continue"),
"{} device"),
10: _("Confirm wallet address on your {} device"),
'default': _("Check your {} device to continue"),
}
def callback_Failure(self, msg):
@ -28,9 +28,9 @@ class GuiMixin(object):
# However, making the user acknowledge they cancelled
# gets old very quickly, so we suppress those. The NotInitialized
# one is misnamed and indicates a passphrase request was cancelled.
if msg.code in (self.types.Failure_PinCancelled,
self.types.Failure_ActionCancelled,
self.types.Failure_NotInitialized):
if msg.code in (self.types.FailureType.PinCancelled,
self.types.FailureType.ActionCancelled,
self.types.FailureType.NotInitialized):
raise UserCancelled()
raise RuntimeError(msg.message)
@ -38,40 +38,55 @@ class GuiMixin(object):
message = self.msg
if not message:
message = self.messages.get(msg.code, self.messages['default'])
self.handler.show_message(message % self.device, self.cancel)
self.handler.show_message(message.format(self.device), self.cancel)
return self.proto.ButtonAck()
def callback_PinMatrixRequest(self, msg):
if msg.type == 2:
msg = _("Enter a new PIN for your %s:")
msg = _("Enter a new PIN for your {}:")
elif msg.type == 3:
msg = (_("Re-enter the new PIN for your %s.\n\n"
msg = (_("Re-enter the new PIN for your {}.\n\n"
"NOTE: the positions of the numbers have changed!"))
else:
msg = _("Enter your current %s PIN:")
pin = self.handler.get_pin(msg % self.device)
msg = _("Enter your current {} PIN:")
pin = self.handler.get_pin(msg.format(self.device))
if len(pin) > 9:
self.handler.show_error(_('The PIN cannot be longer than 9 characters.'))
pin = '' # to cancel below
if not pin:
return self.proto.Cancel()
return self.proto.PinMatrixAck(pin=pin)
def callback_PassphraseRequest(self, req):
if req and hasattr(req, 'on_device') and req.on_device is True:
return self.proto.PassphraseAck()
if self.creating_wallet:
msg = _("Enter a passphrase to generate this wallet. Each time "
"you use this wallet your %s will prompt you for the "
"you use this wallet your {} will prompt you for the "
"passphrase. If you forget the passphrase you cannot "
"access the Zcash coins in the wallet.") % self.device
"access the Zcash coins in the wallet.").format(self.device)
else:
msg = _("Enter the passphrase to unlock this wallet:")
passphrase = self.handler.get_passphrase(msg, self.creating_wallet)
if passphrase is None:
return self.proto.Cancel()
passphrase = bip39_normalize_passphrase(passphrase)
return self.proto.PassphraseAck(passphrase=passphrase)
ack = self.proto.PassphraseAck(passphrase=passphrase)
length = len(ack.passphrase)
if length > 50:
self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length))
return self.proto.Cancel()
return ack
def callback_PassphraseStateRequest(self, msg):
return self.proto.PassphraseStateAck()
def callback_WordRequest(self, msg):
self.step += 1
msg = _("Step %d/24. Enter seed word as explained on "
"your %s:") % (self.step, self.device)
msg = _("Step {}/24. Enter seed word as explained on "
"your {}:").format(self.step, self.device)
word = self.handler.get_word(msg)
# Unfortunately the device can't handle self.proto.Cancel()
return self.proto.WordAck(word=word)
@ -110,6 +125,14 @@ class TrezorClientBase(GuiMixin, PrintError):
def is_pairable(self):
return not self.features.bootloader_mode
def has_usable_connection_with_device(self):
try:
res = self.ping("electrum-zcash pinging device")
assert res == "electrum-zcash pinging device"
except BaseException:
return False
return True
def used(self):
self.last_operation = time.time()
@ -126,7 +149,7 @@ class TrezorClientBase(GuiMixin, PrintError):
def expand_path(n):
'''Convert bip32 path to list of uint32 integers with prime flags
0/-1/1' -> [0, 0x80000001, 0x80000001]'''
# This code is similar to code in trezorlib where it unforunately
# This code is similar to code in trezorlib where it unfortunately
# is not declared as a staticmethod. Our n has an extra element.
PRIME_DERIVATION_FLAG = 0x80000000
path = []
@ -155,27 +178,27 @@ class TrezorClientBase(GuiMixin, PrintError):
def toggle_passphrase(self):
if self.features.passphrase_protection:
self.msg = _("Confirm on your %s device to disable passphrases")
self.msg = _("Confirm on your {} device to disable passphrases")
else:
self.msg = _("Confirm on your %s device to enable passphrases")
self.msg = _("Confirm on your {} device to enable passphrases")
enabled = not self.features.passphrase_protection
self.apply_settings(use_passphrase=enabled)
def change_label(self, label):
self.msg = _("Confirm the new label on your %s device")
self.msg = _("Confirm the new label on your {} device")
self.apply_settings(label=label)
def change_homescreen(self, homescreen):
self.msg = _("Confirm on your %s device to change your home screen")
self.msg = _("Confirm on your {} device to change your home screen")
self.apply_settings(homescreen=homescreen)
def set_pin(self, remove):
if remove:
self.msg = _("Confirm on your %s device to disable PIN protection")
self.msg = _("Confirm on your {} device to disable PIN protection")
elif self.features.pin_protection:
self.msg = _("Confirm on your %s device to change your PIN")
self.msg = _("Confirm on your {} device to change your PIN")
else:
self.msg = _("Confirm on your %s device to set a PIN")
self.msg = _("Confirm on your {} device to set a PIN")
self.change_pin(remove)
def clear_session(self):
@ -188,7 +211,6 @@ class TrezorClientBase(GuiMixin, PrintError):
except BaseException as e:
# If the device was removed it has the same effect...
self.print_error("clear_session: ignoring error", str(e))
pass
def get_public_node(self, address_n, creating):
self.creating_wallet = creating

View File

@ -9,3 +9,6 @@ class Plugin(TrezorPlugin):
if not isinstance(keystore, self.keystore_class):
return
keystore.handler = self.handler
def create_handler(self, window):
return self.handler

View File

@ -5,7 +5,7 @@ from PyQt5.Qt import Qt
from PyQt5.Qt import QGridLayout, QInputDialog, QPushButton
from PyQt5.Qt import QVBoxLayout, QLabel
from electrum_zcash_gui.qt.util import *
from .plugin import TIM_NEW, TIM_RECOVER, TIM_MNEMONIC
from .trezor import TIM_NEW, TIM_RECOVER, TIM_MNEMONIC
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from electrum_zcash.i18n import _
@ -188,13 +188,14 @@ class QtPlugin(QtPluginBase):
@hook
def receive_menu(self, menu, addrs, wallet):
if type(wallet) is not Standard_Wallet:
if len(addrs) != 1:
return
keystore = wallet.get_keystore()
if type(keystore) == self.keystore_class and len(addrs) == 1:
def show_address():
keystore.thread.add(partial(self.show_address, wallet, addrs[0]))
menu.addAction(_("Show on %s") % self.device, show_address)
for keystore in wallet.get_keystores():
if type(keystore) == self.keystore_class:
def show_address():
keystore.thread.add(partial(self.show_address, wallet, keystore, addrs[0]))
menu.addAction(_("Show on {}").format(self.device), show_address)
break
def show_settings_dialog(self, window, keystore):
device_id = self.choose_device(window, keystore)
@ -250,7 +251,7 @@ class QtPlugin(QtPluginBase):
vbox.addWidget(QLabel(msg))
vbox.addWidget(text)
pin = QLineEdit()
pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,10}')))
pin.setValidator(QRegExpValidator(QRegExp('[1-9]{0,9}')))
pin.setMaximumWidth(100)
hbox_pin = QHBoxLayout()
hbox_pin.addWidget(QLabel(_("Enter your PIN (digits 1-9):")))
@ -292,7 +293,7 @@ class SettingsDialog(WindowModalDialog):
their PIN.'''
def __init__(self, window, plugin, keystore, device_id):
title = _("%s Settings") % plugin.device
title = _("{} Settings").format(plugin.device)
super(SettingsDialog, self).__init__(window, title)
self.setMaximumWidth(540)
@ -320,8 +321,11 @@ class SettingsDialog(WindowModalDialog):
def update(features):
self.features = features
set_label_enabled()
bl_hash = bh2u(features.bootloader_hash)
bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
if features.bootloader_hash:
bl_hash = bh2u(features.bootloader_hash)
bl_hash = "\n".join([bl_hash[:32], bl_hash[32:]])
else:
bl_hash = "N/A"
noyes = [_("No"), _("Yes")]
endis = [_("Enable Passphrases"), _("Disable Passphrases")]
disen = [_("Disabled"), _("Enabled")]
@ -375,25 +379,31 @@ class SettingsDialog(WindowModalDialog):
invoke_client('toggle_passphrase', unpair_after=currently_enabled)
def change_homescreen():
from PIL import Image # FIXME
dialog = QFileDialog(self, _("Choose Homescreen"))
filename, __ = dialog.getOpenFileName()
if filename:
im = Image.open(str(filename))
if im.size != (hs_cols, hs_rows):
raise Exception('Image must be 64 x 128 pixels')
if filename.endswith('.toif'):
img = open(filename, 'rb').read()
if img[:8] != b'TOIf\x90\x00\x90\x00':
raise Exception('File is not a TOIF file with size of 144x144')
else:
from PIL import Image # FIXME
im = Image.open(filename)
if im.size != (128, 64):
raise Exception('Image must be 128 x 64 pixels')
im = im.convert('1')
pix = im.load()
img = ''
for j in range(hs_rows):
for i in range(hs_cols):
img += '1' if pix[i, j] else '0'
img = ''.join(chr(int(img[i:i + 8], 2))
for i in range(0, len(img), 8))
img = bytearray(1024)
for j in range(64):
for i in range(128):
if pix[i, j]:
o = (i + j * 128)
img[o // 8] |= (1 << (7 - o % 8))
img = bytes(img)
invoke_client('change_homescreen', img)
def clear_homescreen():
invoke_client('change_homescreen', '\x00')
invoke_client('change_homescreen', b'\x00')
def set_pin():
invoke_client('set_pin', remove=False)
@ -457,9 +467,9 @@ class SettingsDialog(WindowModalDialog):
settings_glayout = QGridLayout()
# Settings tab - Label
label_msg = QLabel(_("Name this %s. If you have mutiple devices "
label_msg = QLabel(_("Name this {}. If you have multiple devices "
"their labels help distinguish them.")
% plugin.device)
.format(plugin.device))
label_msg.setWordWrap(True)
label_label = QLabel(_("Device Label"))
label_edit = QLineEdit()
@ -482,7 +492,7 @@ class SettingsDialog(WindowModalDialog):
pin_msg = QLabel(_("PIN protection is strongly recommended. "
"A PIN is your only protection against someone "
"stealing your Zcash coins if they obtain physical "
"access to your %s.") % plugin.device)
"access to your {}.").format(plugin.device))
pin_msg.setWordWrap(True)
pin_msg.setStyleSheet("color: red")
settings_glayout.addWidget(pin_msg, 3, 1, 1, -1)
@ -497,8 +507,8 @@ class SettingsDialog(WindowModalDialog):
homescreen_clear_button.clicked.connect(clear_homescreen)
homescreen_msg = QLabel(_("You can set the homescreen on your "
"device to personalize it. You must "
"choose a %d x %d monochrome black and "
"white image.") % (hs_rows, hs_cols))
"choose a {} x {} monochrome black and "
"white image.").format(hs_rows, hs_cols))
homescreen_msg.setWordWrap(True)
settings_glayout.addWidget(homescreen_label, 4, 0)
settings_glayout.addWidget(homescreen_change_button, 4, 1)
@ -541,7 +551,7 @@ class SettingsDialog(WindowModalDialog):
clear_pin_button.clicked.connect(clear_pin)
clear_pin_warning = QLabel(
_("If you disable your PIN, anyone with physical access to your "
"%s device can spend your Zcash coins.") % plugin.device)
"{} device can spend your Zcash coins.").format(plugin.device))
clear_pin_warning.setWordWrap(True)
clear_pin_warning.setStyleSheet("color: red")
advanced_glayout.addWidget(clear_pin_button, 0, 2)

View File

@ -0,0 +1,95 @@
from electrum_zcash.util import PrintError
class TrezorTransport(PrintError):
@staticmethod
def all_transports():
"""Reimplemented trezorlib.transport.all_transports so that we can
enable/disable specific transports.
"""
try:
# only to detect trezorlib version
from trezorlib.transport import all_transports
except ImportError:
# old trezorlib. compat for trezorlib < 0.9.2
transports = []
#try:
# from trezorlib.transport_bridge import BridgeTransport
# transports.append(BridgeTransport)
#except BaseException:
# pass
try:
from trezorlib.transport_hid import HidTransport
transports.append(HidTransport)
except BaseException:
pass
try:
from trezorlib.transport_udp import UdpTransport
transports.append(UdpTransport)
except BaseException:
pass
try:
from trezorlib.transport_webusb import WebUsbTransport
transports.append(WebUsbTransport)
except BaseException:
pass
else:
# new trezorlib.
transports = []
#try:
# from trezorlib.transport.bridge import BridgeTransport
# transports.append(BridgeTransport)
#except BaseException:
# pass
try:
from trezorlib.transport.hid import HidTransport
transports.append(HidTransport)
except BaseException:
pass
try:
from trezorlib.transport.udp import UdpTransport
transports.append(UdpTransport)
except BaseException:
pass
try:
from trezorlib.transport.webusb import WebUsbTransport
transports.append(WebUsbTransport)
except BaseException:
pass
return transports
return transports
def enumerate_devices(self):
"""Just like trezorlib.transport.enumerate_devices,
but with exception catching, so that transports can fail separately.
"""
devices = []
for transport in self.all_transports():
try:
new_devices = transport.enumerate()
except BaseException as e:
self.print_error('enumerate failed for {}. error {}'
.format(transport.__name__, str(e)))
else:
devices.extend(new_devices)
return devices
def get_transport(self, path=None):
"""Reimplemented trezorlib.transport.get_transport,
(1) for old trezorlib
(2) to be able to disable specific transports
(3) to call our own enumerate_devices that catches exceptions
"""
if path is None:
try:
return self.enumerate_devices()[0]
except IndexError:
raise Exception("No TREZOR device found") from None
def match_prefix(a, b):
return a.startswith(b) or b.startswith(a)
transports = [t for t in self.all_transports() if match_prefix(path, t.PATH_PREFIX)]
if transports:
return transports[0].find_by_path(path)
raise Exception("Unknown path prefix '%s'" % path)

View File

@ -1,35 +1,449 @@
from .plugin import TrezorCompatiblePlugin, TrezorCompatibleKeyStore
from binascii import hexlify, unhexlify
from electrum_zcash.util import bfh, bh2u, versiontuple
from electrum_zcash.bitcoin import (b58_address_to_hash160, xpub_from_pubkey,
TYPE_ADDRESS, TYPE_SCRIPT)
from electrum_zcash import constants
from electrum_zcash.i18n import _
from electrum_zcash.plugins import BasePlugin, Device
from electrum_zcash.transaction import deserialize, Transaction
from electrum_zcash.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
from ..hw_wallet import HW_PluginBase
class TrezorKeyStore(TrezorCompatibleKeyStore):
# TREZOR initialization methods
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
# script "generation"
SCRIPT_GEN_LEGACY, SCRIPT_GEN_P2SH_SEGWIT, SCRIPT_GEN_NATIVE_SEGWIT = range(0, 3)
class TrezorKeyStore(Hardware_KeyStore):
hw_type = 'trezor'
device = 'TREZOR'
class TrezorPlugin(TrezorCompatiblePlugin):
firmware_URL = 'https://www.mytrezor.com'
libraries_URL = 'https://github.com/trezor/python-trezor'
minimum_firmware = (1, 3, 3)
keystore_class = TrezorKeyStore
def get_derivation(self):
return self.derivation
def get_script_gen(self):
return SCRIPT_GEN_LEGACY
def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair)
def decrypt_message(self, sequence, message, password):
raise RuntimeError(_('Encryption and decryption are not implemented by {}').format(self.device))
def sign_message(self, sequence, message, password):
client = self.get_client()
address_path = self.get_derivation() + "/%d/%d"%sequence
address_n = client.expand_path(address_path)
msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
return msg_sig.signature
def sign_transaction(self, tx, password):
if tx.is_complete():
return
# previous transactions used as inputs
prev_tx = {}
# path of the xpubs that are involved
xpub_path = {}
for txin in tx.inputs():
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
tx_hash = txin['prevout_hash']
if txin.get('prev_tx') is None:
raise Exception(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
prev_tx[tx_hash] = txin['prev_tx']
for x_pubkey in x_pubkeys:
if not is_xpubkey(x_pubkey):
continue
xpub, s = parse_xpubkey(x_pubkey)
if xpub == self.get_master_public_key():
xpub_path[xpub] = self.get_derivation()
self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
class TrezorPlugin(HW_PluginBase):
# Derived classes provide:
#
# class-static variables: client_class, firmware_URL, handler_class,
# libraries_available, libraries_URL, minimum_firmware,
# wallet_class, ckd_public, types
firmware_URL = 'https://wallet.trezor.io'
libraries_URL = 'https://github.com/trezor/python-trezor'
minimum_firmware = (1, 5, 2)
keystore_class = TrezorKeyStore
minimum_library = (0, 9, 0)
MAX_LABEL_LEN = 32
def __init__(self, parent, config, name):
HW_PluginBase.__init__(self, parent, config, name)
def __init__(self, *args):
try:
from . import client
# Minimal test if python-trezor is installed
import trezorlib
import trezorlib.ckd_public
import trezorlib.transport_hid
self.client_class = client.TrezorClient
self.ckd_public = trezorlib.ckd_public
self.types = trezorlib.client.types
self.DEVICE_IDS = trezorlib.transport_hid.DEVICE_IDS
try:
library_version = trezorlib.__version__
except AttributeError:
# python-trezor only introduced __version__ in 0.9.0
library_version = 'unknown'
if library_version == 'unknown' or \
versiontuple(library_version) < self.minimum_library:
self.libraries_available_message = (
_("Library version for '{}' is too old.").format(name)
+ '\nInstalled: {}, Needed: {}'
.format(library_version, self.minimum_library))
self.print_stderr(self.libraries_available_message)
raise ImportError()
self.libraries_available = True
except ImportError:
self.libraries_available = False
TrezorCompatiblePlugin.__init__(self, *args)
return
def hid_transport(self, pair):
from trezorlib.transport_hid import HidTransport
return HidTransport(pair)
from . import client
from . import transport
import trezorlib.ckd_public
import trezorlib.messages
self.client_class = client.TrezorClient
self.ckd_public = trezorlib.ckd_public
self.types = trezorlib.messages
self.DEVICE_IDS = ('TREZOR',)
def bridge_transport(self, d):
from trezorlib.transport_bridge import BridgeTransport
return BridgeTransport(d)
self.transport_handler = transport.TrezorTransport()
self.device_manager().register_enumerate_func(self.enumerate)
def enumerate(self):
devices = self.transport_handler.enumerate_devices()
return [Device(d.get_path(), -1, d.get_path(), 'TREZOR', 0) for d in devices]
def create_client(self, device, handler):
try:
self.print_error("connecting to device at", device.path)
transport = self.transport_handler.get_transport(device.path)
except BaseException as e:
self.print_error("cannot connect at", device.path, str(e))
return None
if not transport:
self.print_error("cannot connect at", device.path)
return
self.print_error("connected to device at", device.path)
client = self.client_class(transport, handler, self)
# Try a ping for device sanity
try:
client.ping('t')
except BaseException as e:
self.print_error("ping failed", str(e))
return None
if not client.atleast_version(*self.minimum_firmware):
msg = (_('Outdated {} firmware for device labelled {}. Please '
'download the updated firmware from {}')
.format(self.device, client.label(), self.firmware_URL))
self.print_error(msg)
handler.show_error(msg)
return None
return client
def get_client(self, keystore, force_pair=True):
devmgr = self.device_manager()
handler = keystore.handler
with devmgr.hid_lock:
client = devmgr.client_for_keystore(self, handler, keystore, force_pair)
# returns the client for a given keystore. can use xpub
if client:
client.used()
return client
def get_coin_name(self):
return "ZcashTestnet" if constants.net.TESTNET else "Zcash"
def initialize_device(self, device_id, wizard, handler):
# Initialization method
msg = _("Choose how you want to initialize your {}.\n\n"
"The first two methods are secure as no secret information "
"is entered into your computer.\n\n"
"For the last two methods you input secrets on your keyboard "
"and upload them to your {}, and so you should "
"only do those on a computer you know to be trustworthy "
"and free of malware."
).format(self.device, self.device)
choices = [
# Must be short as QT doesn't word-wrap radio button text
(TIM_NEW, _("Let the device generate a completely new seed randomly")),
(TIM_RECOVER, _("Recover from a seed you have previously written down")),
(TIM_MNEMONIC, _("Upload a BIP39 mnemonic to generate the seed")),
(TIM_PRIVKEY, _("Upload a master private key"))
]
def f(method):
import threading
settings = self.request_trezor_init_settings(wizard, method, self.device)
t = threading.Thread(target = self._initialize_device, args=(settings, method, device_id, wizard, handler))
t.setDaemon(True)
t.start()
wizard.loop.exec_()
wizard.choice_dialog(title=_('Initialize Device'), message=msg, choices=choices, run_next=f)
def _initialize_device(self, settings, method, device_id, wizard, handler):
item, label, pin_protection, passphrase_protection = settings
if method == TIM_RECOVER:
# FIXME the PIN prompt will appear over this message
# which makes this unreadable
handler.show_error(_(
"You will be asked to enter 24 words regardless of your "
"seed's actual length. If you enter a word incorrectly or "
"misspell it, you cannot change it or go back - you will need "
"to start again from the beginning.\n\nSo please enter "
"the words carefully!"))
language = 'english'
devmgr = self.device_manager()
client = devmgr.client_by_id(device_id)
if method == TIM_NEW:
strength = 64 * (item + 2) # 128, 192 or 256
u2f_counter = 0
skip_backup = False
client.reset_device(True, strength, passphrase_protection,
pin_protection, label, language,
u2f_counter, skip_backup)
elif method == TIM_RECOVER:
word_count = 6 * (item + 2) # 12, 18 or 24
client.step = 0
client.recovery_device(word_count, passphrase_protection,
pin_protection, label, language)
elif method == TIM_MNEMONIC:
pin = pin_protection # It's the pin, not a boolean
client.load_device_by_mnemonic(str(item), pin,
passphrase_protection,
label, language)
else:
pin = pin_protection # It's the pin, not a boolean
client.load_device_by_xprv(item, pin, passphrase_protection,
label, language)
wizard.loop.exit(0)
def setup_device(self, device_info, wizard, purpose):
devmgr = self.device_manager()
device_id = device_info.device.id_
client = devmgr.client_by_id(device_id)
# fixme: we should use: client.handler = wizard
client.handler = self.create_handler(wizard)
if not device_info.initialized:
self.initialize_device(device_id, wizard, client.handler)
client.get_xpub('m', 'standard')
client.used()
def get_xpub(self, device_id, derivation, xtype, wizard):
devmgr = self.device_manager()
client = devmgr.client_by_id(device_id)
client.handler = wizard
xpub = client.get_xpub(derivation, xtype)
client.used()
return xpub
def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
self.prev_tx = prev_tx
self.xpub_path = xpub_path
client = self.get_client(keystore)
inputs = self.tx_inputs(tx, True, keystore.get_script_gen())
outputs = self.tx_outputs(keystore.get_derivation(), tx, keystore.get_script_gen())
signed_tx = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime)[1]
raw = bh2u(signed_tx)
tx.update_signatures(raw)
def show_address(self, wallet, keystore, address):
client = self.get_client(keystore)
if not client.atleast_version(1, 3):
keystore.handler.show_error(_("Your device firmware is too old"))
return
change, index = wallet.get_address_index(address)
derivation = keystore.derivation
address_path = "%s/%d/%d"%(derivation, change, index)
address_n = client.expand_path(address_path)
xpubs = wallet.get_master_public_keys()
if len(xpubs) == 1:
script_gen = keystore.get_script_gen()
script_type = self.types.InputScriptType.SPENDADDRESS
client.get_address(self.get_coin_name(), address_n, True, script_type=script_type)
else:
def f(xpub):
node = self.ckd_public.deserialize(xpub)
return self.types.HDNodePathType(node=node, address_n=[change, index])
pubkeys = wallet.get_public_keys(address)
# sort xpubs using the order of pubkeys
sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs)))
pubkeys = list(map(f, sorted_xpubs))
multisig = self.types.MultisigRedeemScriptType(
pubkeys=pubkeys,
signatures=[b''] * wallet.n,
m=wallet.m,
)
client.get_address(self.get_coin_name(), address_n, True, multisig=multisig)
def tx_inputs(self, tx, for_sig=False, script_gen=SCRIPT_GEN_LEGACY):
inputs = []
for txin in tx.inputs():
txinputtype = self.types.TxInputType()
if txin['type'] == '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 = parse_xpubkey(x_pubkey)
xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
txinputtype._extend_address_n(xpub_n + s)
txinputtype.script_type = self.types.InputScriptType.SPENDADDRESS
else:
def f(x_pubkey):
if is_xpubkey(x_pubkey):
xpub, s = parse_xpubkey(x_pubkey)
else:
xpub = xpub_from_pubkey(0, bfh(x_pubkey))
s = []
node = self.ckd_public.deserialize(xpub)
return self.types.HDNodePathType(node=node, address_n=s)
pubkeys = list(map(f, x_pubkeys))
multisig = self.types.MultisigRedeemScriptType(
pubkeys=pubkeys,
signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))),
m=txin.get('num_sig'),
)
script_type = self.types.InputScriptType.SPENDMULTISIG
txinputtype = self.types.TxInputType(
script_type=script_type,
multisig=multisig
)
# find which key is mine
for x_pubkey in x_pubkeys:
if is_xpubkey(x_pubkey):
xpub, s = parse_xpubkey(x_pubkey)
if xpub in self.xpub_path:
xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
txinputtype._extend_address_n(xpub_n + s)
break
prev_hash = unhexlify(txin['prevout_hash'])
prev_index = txin['prevout_n']
if 'value' in txin:
txinputtype.amount = txin['value']
txinputtype.prev_hash = prev_hash
txinputtype.prev_index = prev_index
if 'scriptSig' in txin:
script_sig = bfh(txin['scriptSig'])
txinputtype.script_sig = script_sig
txinputtype.sequence = txin.get('sequence', 0xffffffff - 1)
inputs.append(txinputtype)
return inputs
def tx_outputs(self, derivation, tx, script_gen=SCRIPT_GEN_LEGACY):
def create_output_by_derivation(info):
index, xpubs, m = info
if len(xpubs) == 1:
script_type = self.types.OutputScriptType.PAYTOADDRESS
address_n = self.client_class.expand_path(derivation + "/%d/%d" % index)
txoutputtype = self.types.TxOutputType(
amount=amount,
script_type=script_type,
address_n=address_n,
)
else:
script_type = self.types.OutputScriptType.PAYTOMULTISIG
address_n = self.client_class.expand_path("/%d/%d" % index)
nodes = map(self.ckd_public.deserialize, xpubs)
pubkeys = [self.types.HDNodePathType(node=node, address_n=address_n) for node in nodes]
multisig = self.types.MultisigRedeemScriptType(
pubkeys=pubkeys,
signatures=[b''] * len(pubkeys),
m=m)
txoutputtype = self.types.TxOutputType(
multisig=multisig,
amount=amount,
address_n=self.client_class.expand_path(derivation + "/%d/%d" % index),
script_type=script_type)
return txoutputtype
def create_output_by_address():
txoutputtype = self.types.TxOutputType()
txoutputtype.amount = amount
if _type == TYPE_SCRIPT:
txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN
txoutputtype.op_return_data = address[2:]
elif _type == TYPE_ADDRESS:
txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS
txoutputtype.address = address
return txoutputtype
def is_any_output_on_change_branch():
for _type, address, amount in tx.outputs():
info = tx.output_info.get(address)
if info is not None:
index, xpubs, m = info
if index[0] == 1:
return True
return False
outputs = []
has_change = False
any_output_on_change_branch = is_any_output_on_change_branch()
for _type, address, amount in tx.outputs():
use_create_by_derivation = False
info = tx.output_info.get(address)
if info is not None and not has_change:
index, xpubs, m = info
on_change_branch = index[0] == 1
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
# note: ^ restriction can be removed once we require fw
# that has https://github.com/trezor/trezor-mcu/pull/306
if on_change_branch == any_output_on_change_branch:
use_create_by_derivation = True
has_change = True
if use_create_by_derivation:
txoutputtype = create_output_by_derivation(info)
else:
txoutputtype = create_output_by_address()
outputs.append(txoutputtype)
return outputs
def electrum_tx_to_txtype(self, tx):
t = self.types.TransactionType()
if tx is None:
# probably for segwit input and we don't need this prev txn
return t
d = deserialize(tx.raw)
t.version = d['version']
t.lock_time = d['lockTime']
inputs = self.tx_inputs(tx)
t._extend_inputs(inputs)
for vout in d['outputs']:
o = t._add_bin_outputs()
o.amount = vout['value']
o.script_pubkey = bfh(vout['scriptPubKey'])
return t
# This function is called from the TREZOR libraries (via tx_api)
def get_tx(self, tx_hash):
tx = self.prev_tx[tx_hash]
return self.electrum_tx_to_txtype(tx)