work in progress, starting to look good

This commit is contained in:
newtonick
2021-06-07 13:46:15 -04:00
parent d841538fa3
commit 2988eb5e1a
10 changed files with 551 additions and 160 deletions
+10 -1
View File
@@ -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
+1 -1
View File
@@ -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'))
+7 -124
View File
@@ -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 <len:pubkey> ... <len:pubkey> 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({
+202
View File
@@ -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
+239
View File
@@ -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
+5 -5
View File
@@ -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
+16 -12
View File
@@ -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 <len:pubkey> ... <len:pubkey> 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
})
+5 -1
View File
@@ -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:
+58 -16
View File
@@ -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)
+8
View File
@@ -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)