mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-06 07:28:24 +00:00
345 lines
14 KiB
Python
345 lines
14 KiB
Python
# Internal Dependencies
|
|
from buttons import Buttons, B
|
|
from camera_process import CameraProcess
|
|
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
|
|
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
|
|
import textwrap
|
|
|
|
class Wallet:
|
|
|
|
LOW = 0
|
|
HIGH = 1
|
|
|
|
def __init__(self, current_network, hardened_derivation) -> None:
|
|
self.current_network = current_network
|
|
self.hardened_derivation = hardened_derivation
|
|
self.qrsize = 60
|
|
|
|
def set_seed_phrase(self, seed_phrase):
|
|
# requires a valid seed phrase or error will be thrown
|
|
self.seed_phrase = seed_phrase
|
|
self.seed = bip39.mnemonic_to_seed((" ".join(self.seed_phrase)).strip())
|
|
self.root = bip32.HDKey.from_seed(self.seed, version=NETWORKS[self.current_network]["xprv"])
|
|
self.fingerprint = self.root.child(0).fingerprint
|
|
self.bip48_xprv = self.root.derive(self.hardened_derivation)
|
|
self.bip48_xpub = self.bip48_xprv.to_public()
|
|
|
|
self.tx = None
|
|
self.inp_amount = None
|
|
self.fee = None
|
|
self.spend = None
|
|
self.destinationaddress = None
|
|
self.controller = None
|
|
self.buttons = None
|
|
|
|
self.camera_loop_timer = None
|
|
self.camera_data = None
|
|
self.is_camera_data = False
|
|
|
|
self.qr_total_frames = 0
|
|
self.qr_cur_frame_count = 0
|
|
self.qr_data = []
|
|
self.frame_display = []
|
|
self.percentage_complete = 0
|
|
|
|
self.scan_started_ind = 0
|
|
|
|
###
|
|
### Required Methods to implement for Child Wallet Class
|
|
###
|
|
### import_qr, parse_psbt, sign_transaction, total_frames_parse, current_frame_parse, data_parse, capture_complete
|
|
### get_name, set_network, make_xpub_qr_codes, make_signing_qr_codes, set_qr_density
|
|
|
|
def import_qr(self) -> str:
|
|
return "empty"
|
|
|
|
def parse_psbt(self, raw_psbt) -> bool:
|
|
# decodes and parses raw_psbt, also calculates the following instance values
|
|
self.inp_amount = None
|
|
self.change = None
|
|
self.fee = None
|
|
self.spend = None
|
|
self.destinationaddress = None
|
|
return False
|
|
|
|
def sign_transaction(self) -> (bool, str):
|
|
# signs transaction/pbst last passed to parse_psbt method
|
|
return (False, '')
|
|
|
|
def total_frames_parse(data) -> int:
|
|
# parse and returns total number of frames from qr data frame
|
|
return -1
|
|
|
|
def current_frame_parse(data) -> int:
|
|
# parses and returns current frame number from qr data frame
|
|
return -1
|
|
|
|
def data_parse(data) -> str:
|
|
# parse qr data to string to be cancatinated together into a pbst transaction
|
|
return "empty"
|
|
|
|
def capture_complete() -> bool:
|
|
# returns true if the qr data list is complete
|
|
return False
|
|
|
|
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 "".join(self.qr_data)
|
|
|
|
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 current frame
|
|
current_frame = type(self).current_frame_parse(data[0])
|
|
if self.qr_data[current_frame - 1] == "empty":
|
|
# if frame has never been captured, store data element in it
|
|
self.qr_data[current_frame - 1] = type(self).data_parse(data[0])
|
|
# increment number of frames captured
|
|
self.qr_cur_frame_count += 1
|
|
# show in frame display / progress of captured frame
|
|
self.frame_display[current_frame - 1] = "*"
|
|
# calculate percentage complete of captured frames
|
|
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 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 make_xpub_qr_codes(self, data, callback = None) -> []:
|
|
return []
|
|
|
|
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
|
|
elif density == Wallet.HIGH:
|
|
self.qrsize = 100
|
|
|
|
###
|
|
### Network Related Methods
|
|
###
|
|
|
|
def get_network(self) -> str:
|
|
return self.current_network
|
|
|
|
def get_hardened_derivation(self) -> str:
|
|
return self.hardened_derivation
|
|
|
|
def set_network(self, network) -> bool:
|
|
return False
|
|
|
|
###
|
|
### Internal Wallet Transactions
|
|
###
|
|
|
|
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}
|
|
policy = None
|
|
inp_amount = 0.0
|
|
for inp in tx.inputs:
|
|
inp_amount += inp.witness_utxo.value
|
|
# get policy of the input
|
|
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
|
|
# 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(self, tx, inp_amount, policy, currentnetwork) -> (float, float, float):
|
|
spend = 0
|
|
change = 0
|
|
destinationaddress = ""
|
|
for i, out in enumerate(tx.outputs):
|
|
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:
|
|
# 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(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
|
|
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(self, 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(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()
|
|
# p2sh can be either legacy multisig, or nested segwit multisig
|
|
# or nested segwit singlesig
|
|
if script_type == "p2sh":
|
|
if scope.witness_script is not None:
|
|
script_type = "p2sh-p2wsh"
|
|
elif scope.redeem_script is not None and scope.redeem_script.script_type() == "p2wpkh":
|
|
script_type = "p2sh-p2wpkh"
|
|
policy = { "type": script_type }
|
|
# expected multisig
|
|
if "p2wsh" in script_type and scope.witness_script is not None:
|
|
m, n, pubkeys = self.parse_multisig(scope.witness_script)
|
|
|
|
# check pubkeys are derived from cosigners
|
|
cosigners = self.get_cosigners(pubkeys, scope.bip32_derivations, xpubs)
|
|
policy.update({
|
|
"m": m, "n": n, "cosigners": cosigners
|
|
})
|
|
return policy |