diff --git a/controller.py b/controller.py index 6c8cb6fd..eeff453a 100644 --- a/controller.py +++ b/controller.py @@ -16,6 +16,8 @@ from path import Path from seed_storage import SeedStorage from specter_desktop_multisig_wallet import SpecterDesktopMultisigWallet from blue_vault_wallet import BlueVaultWallet +from sparrow_multisig_wallet import SparrowMultiSigWallet +from generic_ur2_wallet import GenericUR2Wallet class Controller: @@ -278,6 +280,7 @@ class Controller: if r == True: break + self.signing_tools_view.draw_modal(["Generating QR ..."]) self.wallet.set_seed_phrase(seed_phrase) self.signing_tools_view.display_xpub_qr(self.wallet) return Path.MAIN_MENU @@ -330,7 +333,7 @@ class Controller: self.menu_view.draw_modal(["Loading..."]) self.wallet.set_seed_phrase(seed_phrase) raw_pbst = self.wallet.scan_animated_qr_pbst(self) - print("raw_pbst: " + raw_pbst) + if raw_pbst == "nodata": return Path.SIGNING_TOOLS_SUB_MENU if raw_pbst == "invalid": @@ -395,6 +398,12 @@ class Controller: elif r == "Blue Wallet Vault": self.wallet_klass = globals()["BlueVaultWallet"] self.wallet = self.wallet_klass(self.wallet.get_network()) + elif r == "Sparrow Multisig": + self.wallet_klass = globals()["SparrowMultiSigWallet"] + self.wallet = self.wallet_klass(self.wallet.get_network()) + elif r == "UR 2.0 Multisig": + self.wallet_klass = globals()["GenericUR2Wallet"] + self.wallet = self.wallet_klass(self.wallet.get_network()) return Path.SETTINGS_SUB_MENU diff --git a/helpers/qr.py b/helpers/qr.py index d6319407..92626498 100644 --- a/helpers/qr.py +++ b/helpers/qr.py @@ -7,7 +7,7 @@ class QR: def qrimage(self, data): - qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=1, border=2 ) + qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=3 ) qr.add_data(data) qr.make(fit=True) return(qr.make_image(fill_color="black", back_color="white").resize((240,240)).convert('RGB')) \ No newline at end of file diff --git a/models/blue_vault_wallet.py b/models/blue_vault_wallet.py index aaee1b55..09c6ac71 100644 --- a/models/blue_vault_wallet.py +++ b/models/blue_vault_wallet.py @@ -13,7 +13,6 @@ from embit import ec from io import BytesIO from binascii import unhexlify, hexlify, a2b_base64, b2a_base64 from bcur import bcur_decode, cbor_decode, bc32decode, bc32encode, cbor_encode, bcur_encode -from ur2.ur_decoder import URDecoder import re class BlueVaultWallet(Wallet): @@ -45,8 +44,8 @@ class BlueVaultWallet(Wallet): base64_psbt = a2b_base64(raw_psbt) self.tx = psbt.PSBT.parse(base64_psbt) - (self.inp_amount, policy) = BlueVaultWallet.input_amount(self.tx) - (self.change, self.fee, self.spend, self.destinationaddress) = BlueVaultWallet.change_fee_spend_amounts(self.tx, self.inp_amount, policy, self.current_network) + (self.inp_amount, policy) = self.input_amount(self.tx) + (self.change, self.fee, self.spend, self.destinationaddress) = self.change_fee_spend_amounts(self.tx, self.inp_amount, policy, self.current_network) return True @@ -81,8 +80,8 @@ class BlueVaultWallet(Wallet): def data_parse(data) -> str: return data.split("/")[-1].strip() - def capture_complete(qr_data = []) -> bool: - if "empty" not in qr_data: + def capture_complete(self) -> bool: + if "empty" not in self.qr_data: return True else: return False @@ -139,126 +138,10 @@ class BlueVaultWallet(Wallet): self.qrsize = 140 ### - ### Internal Wallet Transactions + ### Internal Wallet Transactions OVERRIDE ### - def input_amount(tx) -> (float, str): - # Check inputs of the transaction and check that they use the same script type - # For multisig parsed policy will look like this: - # { script_type: p2wsh, cosigners: [xpubs strings], m: 2, n: 3} - policy = None - inp_amount = 0.0 - for inp in tx.inputs: - inp_amount += inp.witness_utxo.value - # get policy of the input - inp_policy = BlueVaultWallet.get_policy(inp, inp.witness_utxo.script_pubkey, tx.xpubs) - # if policy is None - assign current - if policy is None: - policy = inp_policy - # otherwise check that everything in the policy is the same - else: - # check policy is the same - if policy != inp_policy: - raise RuntimeError("Mixed inputs in the transaction") - - return (inp_amount, policy) - - def change_fee_spend_amounts(tx, inp_amount, policy, currentnetwork) -> (float, float, float): - spend = 0 - change = 0 - destinationaddress = "" - for i, out in enumerate(tx.outputs): - out_policy = BlueVaultWallet.get_policy(out, tx.tx.vout[i].script_pubkey, tx.xpubs) - is_change = False - # if policy is the same - probably change - if out_policy == policy: - # double-check that it's change - # we already checked in get_cosigners and parse_multisig - # that pubkeys are generated from cosigners, - # and witness script is corresponding multisig - # so we only need to check that scriptpubkey is generated from - # witness script - - # empty script by default - sc = script.Script(b"") - # multisig, we know witness script - if policy["type"] == "p2wsh": - sc = script.p2wsh(out.witness_script) - elif policy["type"] == "p2sh-p2wsh": - sc = script.p2sh(script.p2wsh(out.witness_script)) - # single-sig - elif "pkh" in policy["type"]: - if len(out.bip32_derivations.values()) > 0: - der = list(out.bip32_derivations.values())[0].derivation - my_pubkey = root.derive(der) - if policy["type"] == "p2wpkh": - sc = script.p2wpkh(my_pubkey) - elif policy["type"] == "p2sh-p2wpkh": - sc = script.p2sh(script.p2wpkh(my_pubkey)) - if sc.data == tx.tx.vout[i].script_pubkey.data: - is_change = True - if is_change: - change += tx.tx.vout[i].value - print("Change %d sats" % tx.tx.vout[i].value) - else: - spend += tx.tx.vout[i].value - print("Spending %d sats to %s" % (tx.tx.vout[i].value, tx.tx.vout[i].script_pubkey.address(NETWORKS[currentnetwork]))) - destinationaddress = tx.tx.vout[i].script_pubkey.address(NETWORKS[currentnetwork]) - - fee = inp_amount - change - spend - - return (change, fee, spend, destinationaddress) - - def parse_multisig(sc): - """Takes a script and extracts m,n and pubkeys from it""" - # OP_m ... OP_n OP_CHECKMULTISIG - # check min size - if len(sc.data) < 37 or sc.data[-1] != 0xae: - raise ValueError("Not a multisig script") - m = sc.data[0] - 0x50 - if m < 1 or m > 16: - raise ValueError("Invalid multisig script") - n = sc.data[-2] - 0x50 - if n < m or n > 16: - raise ValueError("Invalid multisig script") - s = BytesIO(sc.data) - # drop first byte - s.read(1) - # read pubkeys - pubkeys = [] - for i in range(n): - char = s.read(1) - if char != b"\x21": - raise ValueError("Invlid pubkey") - pubkeys.append(ec.PublicKey.parse(s.read(33))) - # check that nothing left - if s.read() != sc.data[-2:]: - raise ValueError("Invalid multisig script") - return m, n, pubkeys - - def get_cosigners(pubkeys, derivations, xpubs): - """Returns xpubs used to derive pubkeys using global xpub field from psbt""" - cosigners = [] - for i, pubkey in enumerate(pubkeys): - if pubkey not in derivations: - raise ValueError("Missing derivation") - der = derivations[pubkey] - for xpub in xpubs: - origin_der = xpubs[xpub] - # check fingerprint - if origin_der.fingerprint == der.fingerprint: - # check derivation - last two indexes give pub from xpub - if origin_der.derivation == der.derivation[:-2]: - # check that it derives to pubkey actually - if xpub.derive(der.derivation[-2:]).key == pubkey: - # append strings so they can be sorted and compared - cosigners.append(xpub.to_base58()) - break - if len(cosigners) != len(pubkeys): - raise RuntimeError("Can't get all cosigners") - return sorted(cosigners) - - def get_policy(scope, scriptpubkey, xpubs): + def get_policy(self, scope, scriptpubkey, xpubs): """Parse scope and get policy""" # we don't know the policy yet, let's parse it script_type = scriptpubkey.script_type() @@ -272,7 +155,7 @@ class BlueVaultWallet(Wallet): policy = { "type": script_type } # expected multisig if "p2wsh" in script_type and scope.witness_script is not None: - m, n, pubkeys = BlueVaultWallet.parse_multisig(scope.witness_script) + m, n, pubkeys = super().parse_multisig(scope.witness_script) # check pubkeys are derived from cosigners policy.update({ diff --git a/models/generic_ur2_wallet.py b/models/generic_ur2_wallet.py new file mode 100644 index 00000000..b0b77d1f --- /dev/null +++ b/models/generic_ur2_wallet.py @@ -0,0 +1,202 @@ +from wallet import Wallet +from buttons import Buttons, B +from camera_process import CameraProcess +from camera_process import CameraPoll +from view import View +from qr import QR + +# External Dependencies +import time +from embit.bip39 import mnemonic_to_bytes +from embit.bip39 import mnemonic_from_bytes +from embit import bip39 +from embit import script +from embit import bip32 +from embit import psbt +from embit.networks import NETWORKS +from embit import ec +from io import BytesIO +from binascii import unhexlify, hexlify, a2b_base64, b2a_base64 +from bcur import bcur_decode, cbor_decode, bc32decode, bc32encode, cbor_encode, bcur_encode +from ur2.ur_decoder import URDecoder +from ur2.ur_encoder import UREncoder +from ur2.cbor_lite import CBOREncoder +from ur2.ur import UR +import re +import textwrap + +class GenericUR2Wallet(Wallet): + + def __init__(self, current_network = "main", hardened_derivation = "m/48h/0h/0h/2h") -> None: + if current_network == "main": + Wallet.__init__(self, current_network, "m/48h/0h/0h/2h") + elif current_network == "test": + Wallet.__init__(self, current_network, "m/48h/1h/0h/2h") + else: + Wallet.__init__(self, current_network, hardened_derivation) + + self.qrsize = 80 + + def set_seed_phrase(self, seed_phrase): + Wallet.set_seed_phrase(self, seed_phrase) + self.ur_decoder = URDecoder() + + def get_name(self) -> str: + return "UR 2.0 Multisig" + + def import_qr(self) -> str: + xpubstring = "[%s%s]%s" % ( + hexlify(self.fingerprint).decode('utf-8'), + self.hardened_derivation[1:], + self.bip48_xpub.to_base58(NETWORKS[self.current_network]["Zpub"])) + + return xpubstring + + def parse_psbt(self, raw_psbt) -> bool: + self.tx = psbt.PSBT.parse(raw_psbt) + + (self.inp_amount, policy) = self.input_amount(self.tx) + (self.change, self.fee, self.spend, self.destinationaddress) = self.change_fee_spend_amounts(self.tx, self.inp_amount, policy, self.current_network) + + return True + + def sign_transaction(self) -> (str): + + # sign the transaction + self.tx.sign_with(self.root) + + signed_psbt = self.tx.serialize() + + return signed_psbt + + def scan_animated_qr_pbst(self, controller) -> str: + self.controller = controller + self.buttons = controller.buttons + self.controller.menu_view.draw_modal(["Initializing Camera"]) # TODO: Move to Controller + # initialize camera + self.controller.to_camera_queue.put(["start"]) + # First get blocking, this way it's clear when the camera is ready for the end user + self.controller.from_camera_queue.get() + self.camera_loop_timer = CameraPoll(0.05, self.process_camera_data) + + input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT]) + if input in (B.KEY_LEFT, B.KEY_RIGHT): + self.camera_loop_timer.stop() + self.controller.to_camera_queue.put(["stop"]) + return "nodata" + elif input == B.OVERRIDE: + self.camera_loop_timer.stop() + self.controller.to_camera_queue.put(["stop"]) + if self.qr_data[0] == "invalid": + return "invalid" + return cbor_decode(self.ur_decoder.result.cbor) + + def process_camera_data(self): + try: + data = self.controller.from_camera_queue.get(False) + except: + data = ["nodata"] + + if data[0] != "nodata": + if self.qr_total_frames == 0: + # get total frames if not set + self.qr_total_frames = type(self).total_frames_parse(data[0]) + if self.qr_total_frames == -1: + # when invalid, trigger override to display error + self.qr_data = ["invalid"] + self.buttons.trigger_override() # something went wrong, invalid QR + return + + # create qr_data list with number of total frames + self.qr_data = ["empty"] * self.qr_total_frames + # create frame display / progress with number of total frames + self.frame_display = ["-"] * self.qr_total_frames + + # get data and percentage + self.ur_decoder.receive_part(data[0]) + self.percentage_complete = self.ur_decoder.estimated_percent_complete() + frames_complete = round(self.percentage_complete * self.qr_total_frames) + for i in range(frames_complete): + self.frame_display[i] = "*" + + # checking if all frames has been captured, exit camera processing + if self.capture_complete(): + self.buttons.trigger_override() + + # if all frames have not all been captured, display progress to screen/display + if not self.capture_complete(): + View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) + tw, th = View.draw.textsize("Collecting QR Codes:", font=View.IMPACT22) + View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill="ORANGE", font=View.IMPACT22) + lines = textwrap.wrap("".join(self.frame_display), width=11) + yheight = 60 + for line in lines: + tw, th = View.draw.textsize(line, font=View.COURIERNEW30) + View.draw.text(((240 - tw) / 2, yheight), line, fill="ORANGE", font=View.COURIERNEW30) + yheight += 30 + tw, th = View.draw.textsize("Right to Exit", font=View.IMPACT18) + View.draw.text(((240 - tw) / 2, 215), "Right to Exit", fill="ORANGE", font=View.IMPACT18) + View.DispShowImage() + + elif self.scan_started_ind == 0: + self.scan_started_ind = 1 + self.controller.menu_view.draw_modal(["Scan Animated QR"], "", "Right to Exit") + + def total_frames_parse(data) -> int: + return 10 + + def capture_complete(self) -> bool: + if self.ur_decoder.is_complete(): + return True + else: + return False + + def set_network(self, network) -> bool: + if network == "main": + self.current_network = "main" + self.hardened_derivation = "m/48h/0h/0h/2h" + elif network == "test": + self.current_network = "test" + self.hardened_derivation = "m/48h/1h/0h/2h" + else: + return False + + return True + + def make_xpub_qr_codes(self, data, callback = None) -> []: + qr = QR() + images = [] + images.append(qr.qrimage(data)) + return images + + def make_signing_qr_codes(self, data, callback = None) -> []: + qr = QR() + images = [] + cnt = 0 + + cbor_encoder = CBOREncoder() + cbor_encoder.encodeBytes(data) + qr_ur_bytes = UR("crypto-psbt", cbor_encoder.get_bytes()) + ur2_encode = UREncoder(qr_ur_bytes,self.qrsize,0) + qr_cnt = ur2_encode.fountain_encoder.seq_len() + + while not ur2_encode.is_complete(): + + part = ur2_encode.next_part() + images.append(qr.qrimage(part)) + print(part) + cnt += 1 + + if callback != None: + callback((cnt * 100.0) / qr_cnt) + + return images + + def qr_sleep(self): + time.sleep(0.5) + + def set_qr_density(density): + if density == Wallet.LOW: + self.qrsize = 80 + elif density == Wallet.HIGH: + self.qrsize = 100 \ No newline at end of file diff --git a/models/sparrow_multisig_wallet.py b/models/sparrow_multisig_wallet.py new file mode 100644 index 00000000..dc582df6 --- /dev/null +++ b/models/sparrow_multisig_wallet.py @@ -0,0 +1,239 @@ +from wallet import Wallet +from buttons import Buttons, B +from camera_process import CameraProcess +from camera_process import CameraPoll +from view import View +from qr import QR + +# External Dependencies +import time +from embit.bip39 import mnemonic_to_bytes +from embit.bip39 import mnemonic_from_bytes +from embit import bip39 +from embit import script +from embit import bip32 +from embit import psbt +from embit.networks import NETWORKS +from embit import ec +from io import BytesIO +from binascii import unhexlify, hexlify, a2b_base64, b2a_base64 +from bcur import bcur_decode, cbor_decode, bc32decode, bc32encode, cbor_encode, bcur_encode +from ur2.ur_decoder import URDecoder +from ur2.ur_encoder import UREncoder +from ur2.cbor_lite import CBOREncoder +from ur2.ur import UR +import re +import textwrap + +class SparrowMultiSigWallet(Wallet): + + def __init__(self, current_network = "main", hardened_derivation = "m/48h/0h/0h/2h") -> None: + if current_network == "main": + Wallet.__init__(self, current_network, "m/48h/0h/0h/2h") + elif current_network == "test": + Wallet.__init__(self, current_network, "m/48h/1h/0h/2h") + else: + Wallet.__init__(self, current_network, hardened_derivation) + + self.qrsize = 70 + self.blink = False + + def set_seed_phrase(self, seed_phrase): + Wallet.set_seed_phrase(self, seed_phrase) + self.ur_decoder = URDecoder() + + def get_name(self) -> str: + return "Sparrow Multisig" + + # def import_qr(self) -> str: + # xpubstring = '{"xfp": "' + hexlify(self.fingerprint).decode('utf-8') + '","p2wsh": "' + self.bip48_xpub.to_base58(NETWORKS[self.current_network]["Zpub"]) + '","p2wsh_deriv": "' + self.hardened_derivation[1:].replace("h", "'") + '"}' + + # return xpubstring + + def import_qr(self) -> str: + xpubstring = "[%s%s]%s" % ( + hexlify(self.fingerprint).decode('utf-8'), + self.hardened_derivation[1:], + self.bip48_xpub.to_base58(NETWORKS[self.current_network]["Zpub"])) + + return xpubstring + + def parse_psbt(self, raw_psbt) -> bool: + base64_psbt = a2b_base64(raw_psbt) + self.tx = psbt.PSBT.parse(base64_psbt) + + (self.inp_amount, policy) = self.input_amount(self.tx) + (self.change, self.fee, self.spend, self.destinationaddress) = self.change_fee_spend_amounts(self.tx, self.inp_amount, policy, self.current_network) + + return True + + def sign_transaction(self) -> (str): + + # sign the transaction + self.tx.sign_with(self.root) + + for inp in self.tx.inputs: + if inp.witness_utxo is not None: + inp.non_witness_utxo = None + + #remove scripts from outputs (DIY should know about the wallet) + for out in self.tx.outputs: + out.witness_script = None + out.redeem_script = None + + raw_trimmed_signed_psbt = self.tx.serialize() + + # #added section to trim psbt + # trimmed_psbt = psbt.PSBT(self.tx.tx) + # sigsEnd = 0 + # for i, inp in enumerate(self.tx.inputs): + # sigsEnd += len(list(inp.partial_sigs.keys())) + # trimmed_psbt.inputs[i].partial_sigs = inp.partial_sigs + + # raw_trimmed_signed_psbt = trimmed_psbt.serialize() + + # convert to base64 + b64_psbt = b2a_base64(raw_trimmed_signed_psbt) + # somehow b2a ends with \n... + if b64_psbt[-1:] == b"\n": + b64_psbt = b64_psbt[:-1] + + return b64_psbt.decode('utf-8') + + def scan_animated_qr_pbst(self, controller) -> str: + self.controller = controller + self.buttons = controller.buttons + self.controller.menu_view.draw_modal(["Initializing Camera"]) # TODO: Move to Controller + # initialize camera + self.controller.to_camera_queue.put(["start"]) + # First get blocking, this way it's clear when the camera is ready for the end user + self.controller.from_camera_queue.get() + self.camera_loop_timer = CameraPoll(0.05, self.process_camera_data) + + input = self.buttons.wait_for([B.KEY_LEFT, B.KEY_RIGHT]) + if input in (B.KEY_LEFT, B.KEY_RIGHT): + self.camera_loop_timer.stop() + self.controller.to_camera_queue.put(["stop"]) + return "nodata" + elif input == B.OVERRIDE: + self.camera_loop_timer.stop() + self.controller.to_camera_queue.put(["stop"]) + if self.qr_data[0] == "invalid": + return "invalid" + return b2a_base64(cbor_decode(self.ur_decoder.result.cbor)) + + def process_camera_data(self): + try: + data = self.controller.from_camera_queue.get(False) + except: + data = ["nodata"] + + if data[0] != "nodata": + if self.qr_total_frames == 0: + # get total frames if not set + self.qr_total_frames = type(self).total_frames_parse(data[0]) + if self.qr_total_frames == -1: + # when invalid, trigger override to display error + self.qr_data = ["invalid"] + self.buttons.trigger_override() # something went wrong, invalid QR + return + + # create qr_data list with number of total frames + self.qr_data = ["empty"] * self.qr_total_frames + # create frame display / progress with number of total frames + self.frame_display = ["-"] * self.qr_total_frames + + # get data and percentage + self.ur_decoder.receive_part(data[0]) + self.percentage_complete = self.ur_decoder.estimated_percent_complete() + frames_complete = round(self.percentage_complete * self.qr_total_frames) + for i in range(frames_complete): + self.frame_display[i] = "*" + + # checking if all frames has been captured, exit camera processing + if self.capture_complete(): + self.buttons.trigger_override() + + # if all frames have not all been captured, display progress to screen/display + if not self.capture_complete(): + View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) + tw, th = View.draw.textsize("Collecting QR Codes:", font=View.IMPACT22) + View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill="ORANGE", font=View.IMPACT22) + lines = textwrap.wrap("".join(self.frame_display), width=11) + yheight = 60 + for line in lines: + tw, th = View.draw.textsize(line, font=View.COURIERNEW30) + View.draw.text(((240 - tw) / 2, yheight), line, fill="ORANGE", font=View.COURIERNEW30) + yheight += 30 + tw, th = View.draw.textsize("Right to Exit", font=View.IMPACT18) + View.draw.text(((240 - tw) / 2, 215), "Right to Exit", fill="ORANGE", font=View.IMPACT18) + if self.blink == True: + View.draw.text((230, 5), "+", fill="ORANGE", font=View.IMPACT16) + self.blink = False + else: + self.blink = True + View.DispShowImage() + + elif self.scan_started_ind == 0: + self.scan_started_ind = 1 + self.controller.menu_view.draw_modal(["Scan Animated QR"], "", "Right to Exit") + + def total_frames_parse(data) -> int: + return 10 + + def capture_complete(self) -> bool: + if self.ur_decoder.is_complete(): + return True + else: + return False + + def set_network(self, network) -> bool: + if network == "main": + self.current_network = "main" + self.hardened_derivation = "m/48h/0h/0h/2h" + elif network == "test": + self.current_network = "test" + self.hardened_derivation = "m/48h/1h/0h/2h" + else: + return False + + return True + + def make_xpub_qr_codes(self, data, callback = None) -> []: + qr = QR() + images = [] + images.append(qr.qrimage(data)) + return images + + def make_signing_qr_codes(self, data, callback = None) -> []: + qr = QR() + + cnt = 0 + images = [] + start = 0 + stop = self.qrsize + qr_cnt = (len(data) // self.qrsize) + 1 + + while cnt < qr_cnt: + part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + data[start:stop] + images.append(qr.qrimage(part)) + print(part) + start = start + self.qrsize + stop = stop + self.qrsize + if stop > len(data): + stop = len(data) + cnt += 1 + + if callback != None: + callback((cnt * 100.0) / qr_cnt) + + return images + + def qr_sleep(self): + time.sleep(0.4) + + def set_qr_density(density): + if density == Wallet.LOW: + self.qrsize = 70 + elif density == Wallet.HIGH: + self.qrsize = 90 \ No newline at end of file diff --git a/models/specter_desktop_multisig_wallet.py b/models/specter_desktop_multisig_wallet.py index c8b5184e..79be1743 100644 --- a/models/specter_desktop_multisig_wallet.py +++ b/models/specter_desktop_multisig_wallet.py @@ -24,7 +24,7 @@ class SpecterDesktopMultisigWallet(Wallet): else: Wallet.__init__(self, current_network, hardened_derivation) - self.qrsize = 60 + self.qrsize = 80 def get_name(self) -> str: return "Specter Desktop" @@ -42,8 +42,8 @@ class SpecterDesktopMultisigWallet(Wallet): base64_psbt = a2b_base64(raw_psbt) self.tx = psbt.PSBT.parse(base64_psbt) - (self.inp_amount, policy) = Wallet.input_amount(self.tx) - (self.change, self.fee, self.spend, self.destinationaddress) = Wallet.change_fee_spend_amounts(self.tx, self.inp_amount, policy, self.current_network) + (self.inp_amount, policy) = self.input_amount(self.tx) + (self.change, self.fee, self.spend, self.destinationaddress) = self.change_fee_spend_amounts(self.tx, self.inp_amount, policy, self.current_network) return True @@ -84,8 +84,8 @@ class SpecterDesktopMultisigWallet(Wallet): def data_parse(data) -> str: return data.split(" ")[-1].strip() - def capture_complete(qr_data = []) -> bool: - if "empty" not in qr_data: + def capture_complete(self) -> bool: + if "empty" not in self.qr_data: return True else: return False diff --git a/models/wallet.py b/models/wallet.py index 4152eb2f..acc24186 100644 --- a/models/wallet.py +++ b/models/wallet.py @@ -5,6 +5,7 @@ from camera_process import CameraPoll from view import View # External Dependencies +import time from embit.bip39 import mnemonic_to_bytes from embit.bip39 import mnemonic_from_bytes from embit import bip39 @@ -90,7 +91,7 @@ class Wallet: # parse qr data to string to be cancatinated together into a pbst transaction return "empty" - def capture_complete(qr_data = []) -> bool: + def capture_complete() -> bool: # returns true if the qr data list is complete return False @@ -150,11 +151,11 @@ class Wallet: self.percentage_complete = int((self.qr_cur_frame_count / self.qr_total_frames) * 100) # checking if all frames has been captured, exit camera processing - if type(self).capture_complete(self.qr_data): + if self.capture_complete(): self.buttons.trigger_override() # if all frames have not all been captured, display progress to screen/display - if not type(self).capture_complete(self.qr_data): + if not self.capture_complete(): View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) tw, th = View.draw.textsize("Collecting QR Codes:", font=View.IMPACT22) View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill="ORANGE", font=View.IMPACT22) @@ -178,6 +179,9 @@ class Wallet: def make_signing_qr_codes(self, data, callback = None) -> []: return [] + def qr_sleep(self): + time.sleep(0.2) + def set_qr_density(density): if density == Wallet.LOW: self.qrsize = 60 @@ -201,7 +205,7 @@ class Wallet: ### Internal Wallet Transactions ### - def input_amount(tx) -> (float, str): + def input_amount(self, tx) -> (float, str): # Check inputs of the transaction and check that they use the same script type # For multisig parsed policy will look like this: # { script_type: p2wsh, cosigners: [xpubs strings], m: 2, n: 3} @@ -210,7 +214,7 @@ class Wallet: for inp in tx.inputs: inp_amount += inp.witness_utxo.value # get policy of the input - inp_policy = Wallet.get_policy(inp, inp.witness_utxo.script_pubkey, tx.xpubs) + inp_policy = self.get_policy(inp, inp.witness_utxo.script_pubkey, tx.xpubs) # if policy is None - assign current if policy is None: policy = inp_policy @@ -222,12 +226,12 @@ class Wallet: return (inp_amount, policy) - def change_fee_spend_amounts(tx, inp_amount, policy, currentnetwork) -> (float, float, float): + def change_fee_spend_amounts(self, tx, inp_amount, policy, currentnetwork) -> (float, float, float): spend = 0 change = 0 destinationaddress = "" for i, out in enumerate(tx.outputs): - out_policy = Wallet.get_policy(out, tx.tx.vout[i].script_pubkey, tx.xpubs) + out_policy = self.get_policy(out, tx.tx.vout[i].script_pubkey, tx.xpubs) is_change = False # if policy is the same - probably change if out_policy == policy: @@ -268,7 +272,7 @@ class Wallet: return (change, fee, spend, destinationaddress) - def parse_multisig(sc): + def parse_multisig(self, sc): """Takes a script and extracts m,n and pubkeys from it""" # OP_m ... OP_n OP_CHECKMULTISIG # check min size @@ -295,7 +299,7 @@ class Wallet: raise ValueError("Invalid multisig script") return m, n, pubkeys - def get_cosigners(pubkeys, derivations, xpubs): + def get_cosigners(self, pubkeys, derivations, xpubs): """Returns xpubs used to derive pubkeys using global xpub field from psbt""" cosigners = [] for i, pubkey in enumerate(pubkeys): @@ -317,7 +321,7 @@ class Wallet: raise RuntimeError("Can't get all cosigners") return sorted(cosigners) - def get_policy(scope, scriptpubkey, xpubs): + def get_policy(self, scope, scriptpubkey, xpubs): """Parse scope and get policy""" # we don't know the policy yet, let's parse it script_type = scriptpubkey.script_type() @@ -331,10 +335,10 @@ class Wallet: policy = { "type": script_type } # expected multisig if "p2wsh" in script_type and scope.witness_script is not None: - m, n, pubkeys = Wallet.parse_multisig(scope.witness_script) + m, n, pubkeys = self.parse_multisig(scope.witness_script) # check pubkeys are derived from cosigners - cosigners = Wallet.get_cosigners(pubkeys, scope.bip32_derivations, xpubs) + cosigners = self.get_cosigners(pubkeys, scope.bip32_derivations, xpubs) policy.update({ "m": m, "n": n, "cosigners": cosigners }) diff --git a/views/settings_tools_view.py b/views/settings_tools_view.py index cb3496df..6d0f545a 100644 --- a/views/settings_tools_view.py +++ b/views/settings_tools_view.py @@ -37,11 +37,15 @@ class SettingsToolsView(View): ### Display Wallet Selection def display_wallet_selection(self) ->str: - r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Specter Desktop", "Blue Wallet Vault"], "Which Wallet?") + r = self.controller.menu_view.display_generic_selection_menu(["... [ Return to Settings ]", "Specter Desktop", "Blue Wallet Vault", "Sparrow Multisig", "UR 2.0 Multisig"], "Which Wallet?") if r == 2: return "Specter Desktop" elif r == 3: return "Blue Wallet Vault" + elif r == 4: + return "Sparrow Multisig" + elif r == 5: + return "UR 2.0 Multisig" # elif r == 3: # return "Specter Desktop Single Sig" else: diff --git a/views/signing_tools_view.py b/views/signing_tools_view.py index 3595f9bc..5b925af3 100644 --- a/views/signing_tools_view.py +++ b/views/signing_tools_view.py @@ -16,8 +16,6 @@ class SigningToolsView(View): ### def display_xpub_qr(self, wallet): - self.draw_modal(["Generating QR ..."]) - xpubstring = wallet.import_qr() print(xpubstring) @@ -25,17 +23,39 @@ class SigningToolsView(View): xpub_images = wallet.make_xpub_qr_codes(xpubstring) cnt = 0 + step = False if len(xpub_images) == 1: View.DispShowImage(xpub_images[0]) while True: if len(xpub_images) != 1: - View.DispShowImage(xpub_images[cnt]) - cnt += 1 - if cnt >= len(xpub_images): - cnt = 0 - time.sleep(0.2) - if self.buttons.check_for_low(B.KEY_RIGHT): - return + if step == False: + View.DispShowImage(xpub_images[cnt]) + else: + frame_text = (str(cnt+1) + " of " + str(len(xpub_images))) + View.DispShowImageWithText(xpub_images[cnt], frame_text) + time.sleep(0.3) + # View.DispShowImage(xpub_images[cnt]) + if step == False: + cnt += 1 + if cnt >= len(xpub_images): + cnt = 0 + wallet.qr_sleep() + if self.buttons.check_for_low(B.KEY_RIGHT): + return + if self.buttons.check_for_low(B.KEY1): + step = True + else: + input = self.buttons.wait_for([B.KEY1, B.KEY_RIGHT, B.KEY_UP, B.KEY_DOWN]) + if input == B.KEY_RIGHT: + return + elif input == B.KEY1 or input == B.KEY_DOWN: + cnt += 1 + if cnt >= len(xpub_images): + cnt = 0 + elif input == B.KEY_UP: + cnt -= 1 + if cnt < 0: + cnt = len(xpub_images) - 1 ### ### Sign Transaction @@ -48,14 +68,36 @@ class SigningToolsView(View): images = wallet.make_signing_qr_codes(psbt, SigningToolsView.qr_gen_status) cnt = 0 + step = False while True: - View.DispShowImage(images[cnt]) - cnt += 1 - if cnt >= len(images): - cnt = 0 - time.sleep(0.2) - if self.buttons.check_for_low(B.KEY_RIGHT): - return + if step == False: + View.DispShowImage(images[cnt]) + else: + frame_text = (str(cnt+1) + " of " + str(len(images))) + View.DispShowImageWithText(images[cnt], frame_text) + time.sleep(0.3) + if step == False: + cnt += 1 + if cnt >= len(images): + cnt = 0 + wallet.qr_sleep() + if self.buttons.check_for_low(B.KEY_RIGHT): + return + if self.buttons.check_for_low(B.KEY1): + step = True + else: + input = self.buttons.wait_for([B.KEY1, B.KEY_RIGHT, B.KEY_UP, B.KEY_DOWN]) + if input == B.KEY_RIGHT: + return + elif input == B.KEY1 or input == B.KEY_DOWN: + cnt += 1 + if cnt >= len(images): + cnt = 0 + elif input == B.KEY_UP: + cnt -= 1 + if cnt < 0: + cnt = len(images) - 1 + def display_transaction_information(self, wallet) -> None: self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) diff --git a/views/view.py b/views/view.py index d780295e..5f46cf70 100644 --- a/views/view.py +++ b/views/view.py @@ -26,6 +26,7 @@ class View: IMPACT26 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 26) IMPACT35 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 35) IMPACT50 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 50) + COURIERNEW14 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 14) COURIERNEW38 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 38) COURIERNEW30 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 30) @@ -67,6 +68,13 @@ class View: image = View.canvas View.disp.ShowImage(image, 0, 0) + def DispShowImageWithText(image, text): + image_copy = image.copy() + draw = ImageDraw.Draw(image_copy) + tw, th = draw.textsize(text, font=View.COURIERNEW14) + draw.text(((240 - tw) / 2, 228), text, fill="GREY", font=View.COURIERNEW14) + View.disp.ShowImage(image_copy, 0, 0) + def draw_modal(self, lines = [], title = "", bottom = "") -> None: View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0)