add plugins changes from electrum 3.1.3
This commit is contained in:
parent
4cc7dc7188
commit
0c422d5ed9
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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_()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue