mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-06 07:28:24 +00:00
work in progress, starting to look good
This commit is contained in:
+10
-1
@@ -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
@@ -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
@@ -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({
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user