Refactored EncodeQR; pythonic refactors; Seed slots removed

This commit is contained in:
kdmukai
2022-02-20 16:30:25 -06:00
parent bc98eefab5
commit 5e62059861
13 changed files with 353 additions and 517 deletions
+3 -3
View File
@@ -608,8 +608,8 @@ class QRDisplayScreen(BaseScreen):
settings = Settings.get_instance()
cur_brightness = settings.get_value(SettingsConstants.SETTING__QR_BRIGHTNESS)
if self.qr_encoder.totalParts() == 1:
image = self.qr_encoder.nextPartImage(240,240,1)
if self.qr_encoder.total_parts() == 1:
image = self.qr_encoder.next_part_image(240,240,1)
self.renderer.show_image(image)
self.hw_inputs.wait_for([B.KEY_RIGHT])
@@ -617,7 +617,7 @@ class QRDisplayScreen(BaseScreen):
while True:
# convert the cur_brightness integer (31-255) into hex triplets
hex_color = (hex(cur_brightness).split('x')[1]) * 3
image = self.qr_encoder.nextPartImage(240,240, border=2, background_color=hex_color)
image = self.qr_encoder.next_part_image(240,240, border=2, background_color=hex_color)
self.renderer.show_image(image)
# Target n held frames per second before rendering next QR image
+1 -1
View File
@@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
### Purpose: used to process images or string data from animated qr codes with psbt data to create binary/base64 psbt
###
class DecodeQR:
def __init__(self, qr_type=None, wordlist_language_code=None):
def __init__(self, qr_type=None, wordlist_language_code=SeedConstants.WORDLIST_LANGUAGE__ENGLISH):
self.complete = False
self.qr_type = qr_type
self.wordlist_language_code = wordlist_language_code
+165 -184
View File
@@ -16,6 +16,8 @@ from seedsigner.models import Seed, QRType
from urtypes.crypto import PSBT as UR_PSBT
from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP
from seedsigner.models.seed import SeedConstants
@dataclass
@@ -25,117 +27,175 @@ class EncodeQR:
"""
# TODO: Refactor so that this is a base class with implementation classes for each
# QR type. No reason exterior code can't directly instantiate the encoder it needs.
psbt: PSBT = None
seed_phrase: List[str] = None
passphrase: str = None
derivation: str = None
network: str = None
qr_type: str = None
qr_density: str = None
wordlist_language_code: str = None
# Class constants to be referenced internally and externally
DENSITY__LOW = "Low"
DENSITY__MEDIUM = "Medium"
DENSITY__HIGH = "High"
ALL_DENSITIES = [DENSITY__LOW, DENSITY__MEDIUM, DENSITY__HIGH]
WORDLIST = None
# Dataclass input vars on __init__()
psbt: PSBT = None
seed_phrase: List[str] = None
passphrase: str = None
derivation: str = None
network: str = SeedConstants.MAINNET
qr_type: str = None
qr_density: str = DENSITY__MEDIUM
wordlist_language_code: str = SeedConstants.WORDLIST_LANGUAGE__ENGLISH
def __post_init__(self):
self.qr = QR()
if self.qr_type == None:
raise Exception('Encoder Type Required')
if not self.qr_type:
raise Exception('qr_type is required')
if self.qr_density == None:
self.qr_density = EncodeQR.DENSITY__MEDIUM
self.encoder = None
self.encoder: BaseQrEncoder = None
# PSBT formats
if self.qr_type == QRType.PSBTSPECTER:
self.encoder = SpecterEncodePSBTQR(self.psbt, self.qr_density)
self.encoder = SpecterPsbtQrEncoder(psbt=self.psbt, qr_density=self.qr_density)
elif self.qr_type == QRType.PSBTUR2:
self.encoder = UREncodePSBTQR(self.psbt, self.qr_density)
elif self.qr_type == QRType.URXPUBQR:
self.encoder = URXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.qr_density, self.wordlist_language_code)
self.encoder = UrPsbtQrEncoder(psbt=self.psbt, qr_density=self.qr_density)
# XPUB formats
elif self.qr_type == QRType.XPUBQR:
self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.wordlist_language_code)
self.encoder = XpubQrEncoder(
seed_phrase=self.seed_phrase,
passphrase=self.passphrase,
derivation=self.derivation,
network=self.network,
wordlist_language_code=self.wordlist_language_code
)
elif self.qr_type == QRType.URXPUBQR:
self.encoder = UrXpubQrEncoder(
qr_density=self.qr_density,
seed_phrase=self.seed_phrase,
passphrase=self.passphrase,
derivation=self.derivation,
network=self.network,
wordlist_language_code=self.wordlist_language_code
)
elif self.qr_type == QRType.SPECTERXPUBQR:
self.encoder = SpecterXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.qr_density, self.wordlist_language_code)
self.encoder = SpecterXPubQrEncoder(
qr_density=self.qr_density,
seed_phrase=self.seed_phrase,
passphrase=self.passphrase,
derivation=self.derivation,
network=self.network,
wordlist_language_code=self.wordlist_language_code
)
# SeedQR formats
elif self.qr_type == QRType.SEEDQR:
self.encoder = SeedQR(self.seed_phrase, self.wordlist_language_code)
self.encoder = SeedQrEncoder(seed_phrase=self.seed_phrase,
wordlist_language_code=self.wordlist_language_code)
elif self.qr_type == QRType.COMPACTSEEDQR:
self.encoder = CompactSeedQR(self.seed_phrase, self.wordlist_language_code)
self.encoder = CompactSeedQrEncoder(seed_phrase=self.seed_phrase,
wordlist_language_code=self.wordlist_language_code)
else:
raise Exception('Encoder Type not Supported')
raise Exception('QR Type not supported')
def totalParts(self):
return self.encoder.seqLen()
def total_parts(self):
return self.encoder.seq_len()
def nextPart(self):
return self.encoder.nextPart()
def next_part(self):
return self.encoder.next_part()
def part2Image(self, part, width=240, height=240, border=3):
def part_to_image(self, part, width=240, height=240, border=3):
return self.qr.qrimage_io(part, width, height, border)
def nextPartImage(self, width=240, height=240, border=3, background_color="bdbdbd"):
part = self.nextPart()
def next_part_image(self, width=240, height=240, border=3, background_color="bdbdbd"):
part = self.next_part()
if self.qr_type == QRType.SEEDQR:
return self.qr.qrimage(part, width, height, border)
else:
return self.qr.qrimage_io(part, width, height, border, background_color=background_color)
def isComplete(self):
return self.encoder.isComplete()
# TODO: Make these properties?
def is_complete(self):
return self.encoder.is_complete
def getQRDensity(self):
def get_qr_density(self):
return self.qr_density
def getQRType(self):
def get_qr_type(self):
return self.qr_type
class UREncodePSBTQR:
def __init__(self, p, qr_density):
self.psbt = p
self.qr_max_fragement_size = 20
class BaseQrEncoder:
def seq_len(self):
raise Exception("Not implemented in child class")
def next_part(self) -> str:
raise Exception("Not implemented in child class")
@property
def is_complete(self):
raise Exception("Not implemented in child class")
def _create_parts(self):
raise Exception("Not implemented in child class")
class BasePsbtQrEncoder(BaseQrEncoder):
def __init__(self, psbt: PSBT):
self.psbt = psbt
class UrPsbtQrEncoder(BasePsbtQrEncoder):
def __init__(self, psbt, qr_density):
super().__init__(psbt)
self.qr_max_fragment_size = 20
qr_ur_bytes = UR("crypto-psbt", UR_PSBT(self.psbt.serialize()).to_cbor())
if qr_density == EncodeQR.DENSITY__LOW:
self.qr_max_fragement_size = 10
self.qr_max_fragment_size = 10
elif qr_density == EncodeQR.DENSITY__MEDIUM:
self.qr_max_fragement_size = 30
self.qr_max_fragment_size = 30
elif qr_density == EncodeQR.DENSITY__HIGH:
self.qr_max_fragement_size = 120
self.qr_max_fragment_size = 120
self.ur2_encode = UREncoder(qr_ur_bytes,self.qr_max_fragement_size,0)
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
def seqLen(self):
def seq_len(self):
return self.ur2_encode.fountain_encoder.seq_len()
def nextPart(self) -> str:
def next_part(self) -> str:
return self.ur2_encode.next_part().upper()
def isComplete(self):
@property
def is_complete(self):
return self.ur2_encode.is_complete()
class SpecterEncodePSBTQR:
def __init__(self, p, qr_density):
self.psbt = p
class SpecterPsbtQrEncoder(BasePsbtQrEncoder):
def __init__(self, psbt, qr_density):
super().__init__(psbt)
self.qr_max_fragement_size = 65
self.parts = []
self.part_num_sent = 0
@@ -148,10 +208,10 @@ class SpecterEncodePSBTQR:
elif qr_density == EncodeQR.DENSITY__HIGH:
self.qr_max_fragement_size = 90
self.__createParts()
self._create_parts()
def __createParts(self):
def _create_parts(self):
base64_psbt = b2a_base64(self.psbt.serialize())
if base64_psbt[-1:] == b"\n":
@@ -178,11 +238,11 @@ class SpecterEncodePSBTQR:
cnt += 1
def seqLen(self):
def seq_len(self):
return len(self.parts)
def nextPart(self) -> str:
def next_part(self) -> str:
# if part num sent is gt number of parts, start at 0
if self.part_num_sent > (len(self.parts) - 1):
self.part_num_sent = 0
@@ -199,13 +259,15 @@ class SpecterEncodePSBTQR:
return part
def isComplete(self):
@property
def is_complete(self):
return self.sent_complete
class SeedQR:
class SeedQrEncoder(BaseQrEncoder):
def __init__(self, seed_phrase: List[str], wordlist_language_code: str):
super().__init__()
self.seed_phrase = seed_phrase
self.wordlist = Seed.get_wordlist(wordlist_language_code)
@@ -213,11 +275,11 @@ class SeedQR:
raise Exception('Wordlist Required')
def seqLen(self):
def seq_len(self):
return 1
def nextPart(self):
def next_part(self):
data = ""
# Output as Numeric data format
for word in self.seed_phrase:
@@ -226,12 +288,14 @@ class SeedQR:
return data
def isComplete(self):
@property
def is_complete(self):
return True
class CompactSeedQR(SeedQR):
def nextPart(self):
class CompactSeedQrEncoder(SeedQrEncoder):
def next_part(self):
# Output as binary data format
binary_str = ""
for word in self.seed_phrase:
@@ -260,11 +324,12 @@ class CompactSeedQR(SeedQR):
class XPubQR:
class XpubQrEncoder(BaseQrEncoder):
def __init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code):
self.seed_phrase = seed_phrase
self.passphrase = passphrase
self.derivation = derivation
self.network = network
self.wordlist = Seed.get_wordlist(wordlist_language_code)
self.parts = []
self.part_num_sent = 0
@@ -273,61 +338,66 @@ class XPubQR:
if self.wordlist == None:
raise Exception('Wordlist Required')
version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[network])
self.seed = Seed(mnemonic=self.seed_phrase, passphrase=self.passphrase, wordlist_language_code=wordlist_language_code)
self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[network]["xprv"])
version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[self.network])
self.seed = Seed(mnemonic=self.seed_phrase,
passphrase=self.passphrase,
wordlist_language_code=wordlist_language_code)
self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[self.network]["xprv"])
self.fingerprint = self.root.child(0).fingerprint
self.xprv = self.root.derive(self.derivation)
self.xpub = self.xprv.to_public()
self.xpub_base58 = self.xpub.to_string(version=version)
self.network = network
self.xpubstring = "[%s%s]%s" % (hexlify(self.fingerprint).decode('utf-8'),self.derivation[1:],self.xpub_base58)
self.xpubstring = "[{}{}]{}".format(
hexlify(self.fingerprint).decode('utf-8'),
self.derivation[1:],
self.xpub_base58
)
if isinstance(self, XPubQR):
self.__createParts()
self._create_parts()
def __createParts(self):
def _create_parts(self):
self.parts = []
self.parts.append(self.xpubstring)
def nextPart(self) -> str:
def next_part(self) -> str:
if len(self.parts) > 0:
self.sent_complete = True
return self.parts[0]
def seqLen(self):
def seq_len(self):
return len(self.parts)
def isComplete(self):
@property
def is_complete(self):
return self.sent_complete
class SpecterXPubQR(XPubQR):
def __init__(self, seed_phrase, passphrase, derivation, network, qr_density, wordlist_language_code):
self.qr_max_fragement_size = 65
class SpecterXPubQrEncoder(XpubQrEncoder):
def __init__(self, qr_density, **kwargs):
# Must set up qr_max_fragment_size before calling super().__init__()
self.qr_max_fragment_size = 65
if qr_density == EncodeQR.DENSITY__LOW:
self.qr_max_fragement_size = 40
self.qr_max_fragment_size = 40
elif qr_density == EncodeQR.DENSITY__MEDIUM:
self.qr_max_fragement_size = 65
self.qr_max_fragment_size = 65
elif qr_density == EncodeQR.DENSITY__HIGH:
self.qr_max_fragement_size = 90
self.qr_max_fragment_size = 90
super().__init__(seed_phrase, passphrase, derivation, network, wordlist_language_code)
self.__createParts()
super().__init__(**kwargs)
def __createParts(self):
def _create_parts(self):
self.parts = []
start = 0
stop = self.qr_max_fragement_size
qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragement_size) + 1
stop = self.qr_max_fragment_size
qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragment_size) + 1
if qr_cnt == 1:
self.parts.append(self.xpubstring[start:stop])
@@ -337,14 +407,14 @@ class SpecterXPubQR(XPubQR):
part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + self.xpubstring[start:stop]
self.parts.append(part)
start = start + self.qr_max_fragement_size
stop = stop + self.qr_max_fragement_size
start = start + self.qr_max_fragment_size
stop = stop + self.qr_max_fragment_size
if stop > len(self.xpubstring):
stop = len(self.xpubstring)
cnt += 1
def nextPart(self) -> str:
def next_part(self) -> str:
# if part num sent is gt number of parts, start at 0
if self.part_num_sent > (len(self.parts) - 1):
self.part_num_sent = 0
@@ -361,25 +431,17 @@ class SpecterXPubQR(XPubQR):
return part
def seqLen(self):
return len(self.parts)
def isComplete(self):
return self.sent_complete
class URXPubQR(XPubQR):
def __init__(self, seed_phrase, passphrase, derivation, network, qr_density, wordlist_language_code):
super().__init__(seed_phrase, passphrase, derivation, network, wordlist_language_code)
class UrXpubQrEncoder(XpubQrEncoder):
def __init__(self, qr_density, **kwargs):
super().__init__(**kwargs)
if qr_density == EncodeQR.DENSITY__LOW:
self.qr_max_fragement_size = 10
self.qr_max_fragment_size = 10
elif qr_density == EncodeQR.DENSITY__MEDIUM:
self.qr_max_fragement_size = 30
self.qr_max_fragment_size = 30
elif qr_density == EncodeQR.DENSITY__HIGH:
self.qr_max_fragement_size = 120
self.qr_max_fragment_size = 120
def derivation_to_keypath(path: str) -> list:
arr = path.split("/")
@@ -399,7 +461,7 @@ class URXPubQR(XPubQR):
return Keypath(arr, self.root.my_fingerprint, len(arr))
origin = derivation_to_keypath(derivation)
origin = derivation_to_keypath(self.derivation)
self.ur_hdkey = HDKey({ 'key': self.xpub.key.serialize(),
'chain_code': self.xpub.chain_code,
@@ -432,93 +494,12 @@ class URXPubQR(XPubQR):
qr_ur_bytes = UR("crypto-account", ur_account.to_cbor())
self.ur2_encode = UREncoder(qr_ur_bytes, self.qr_max_fragement_size, 0)
self.ur2_encode = UREncoder(ur=qr_ur_bytes, max_fragment_len=self.qr_max_fragment_size)
def seqLen(self):
return self.ur2_encode.fountain_encoder.seq_len()
def seq_len(self):
return self.ur2_encode.fountain_encoder.seq_len()
def nextPart(self) -> str:
def next_part(self) -> str:
return self.ur2_encode.next_part().upper()
def isComplete(self):
return self.sent_complete
class URXPubQR(XPubQR):
def __init__(self, seed_phrase, passphrase, derivation, network, qr_density, wordlist_language_code):
super().__init__(self, seed_phrase, passphrase, derivation, network, wordlist_language_code)
if qr_density == EncodeQR.LOW:
self.qr_max_fragement_size = 10
elif qr_density == EncodeQR.MEDIUM:
self.qr_max_fragement_size = 30
elif qr_density == EncodeQR.HIGH:
self.qr_max_fragement_size = 120
def derivation_to_keypath(path: str) -> list:
arr = path.split("/")
if arr[0] == "m":
arr = arr[1:]
if len(arr) == 0:
return Keypath([],self.root.my_fingerprint, None)
if arr[-1] == "":
# trailing slash
arr = arr[:-1]
for i, e in enumerate(arr):
if e[-1] == "h" or e[-1] == "'":
arr[i] = PathComponent(int(e[:-1]), True)
else:
arr[i] = PathComponent(int(e), False)
return Keypath(arr, self.root.my_fingerprint, len(arr))
origin = derivation_to_keypath(derivation)
self.ur_hdkey = HDKey({ 'key': self.xpub.key.serialize(),
'chain_code': self.xpub.chain_code,
'origin': origin,
'parent_fingerprint': self.xpub.fingerprint})
ur_outputs = []
if len(origin.components) > 0:
if origin.components[0].index == 84: # Native Single Sig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
elif origin.components[0].index == 49: # Nested Single Sig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
elif origin.components[0].index == 48: # Multisig
if len(origin.components) >= 4:
if origin.components[3].index == 2: # Native Multisig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
elif origin.components[3].index == 1: # Nested Multisig
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
# If empty, add all script types
if len(ur_outputs) == 0:
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]],self.ur_hdkey))
ur_outputs.append(Output([SCRIPT_EXPRESSION_TAG_MAP[403]],self.ur_hdkey))
ur_account = Account(self.root.my_fingerprint, ur_outputs)
qr_ur_bytes = UR("crypto-account", ur_account.to_cbor())
self.ur2_encode = UREncoder(qr_ur_bytes, self.qr_max_fragement_size, 0)
def seqLen(self):
return self.ur2_encode.fountain_encoder.seq_len()
def nextPart(self) -> str:
return self.ur2_encode.next_part().upper()
def isComplete(self):
return self.ur2_encode.is_complete()
+31 -21
View File
@@ -34,11 +34,13 @@ class PSBTParser():
def get_change_data(self, change_num: int) -> dict:
if change_num < len(self.change_data):
return self.change_data[change_num]
@property
def num_change_outputs(self):
return len(self.change_data)
@property
def is_multisig(self):
"""
@@ -46,14 +48,17 @@ class PSBTParser():
"""
return "m" in self.policy
@property
def num_destinations(self):
return len(self.destination_addresses)
def __setRoot(self):
self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[self.network]["xprv"])
def _set_root(self):
self.root = bip32.HDKey.from_seed(self.seed.seed_bytes, version=NETWORKS[self.network]["xprv"])
print(f"root: {self.root}")
def parse(self):
if self.psbt is None:
print(f"self.psbt is None!!")
@@ -63,33 +68,35 @@ class PSBTParser():
print("self.seed is None!")
return False
self.__setRoot()
self._set_root()
rt = self.__parseInputs()
rt = self._parse_inputs()
if rt == False:
return False
rt = self.__parseOutputs()
rt = self._parse_outputs()
if rt == False:
return False
return True
def __parseInputs(self):
def _parse_inputs(self):
self.input_amount = 0
print(f"psbt.inputs: {self.psbt.inputs}")
self.num_inputs = len(self.psbt.inputs)
for inp in self.psbt.inputs:
if inp.witness_utxo:
self.input_amount += inp.witness_utxo.value
inp_policy = PSBTParser.__get_policy(inp, inp.witness_utxo.script_pubkey, self.psbt.xpubs)
inp_policy = PSBTParser._get_policy(inp, inp.witness_utxo.script_pubkey, self.psbt.xpubs)
if self.policy == None:
self.policy = inp_policy
else:
if self.policy != inp_policy:
raise RuntimeError("Mixed inputs in the transaction")
def __parseOutputs(self):
def _parse_outputs(self):
self.spend_amount = 0
self.change_amount = 0
self.change_data = []
@@ -97,7 +104,7 @@ class PSBTParser():
self.destination_addresses = []
self.destination_amounts = []
for i, out in enumerate(self.psbt.outputs):
out_policy = PSBTParser.__get_policy(out, self.psbt.tx.vout[i].script_pubkey, self.psbt.xpubs)
out_policy = PSBTParser._get_policy(out, self.psbt.tx.vout[i].script_pubkey, self.psbt.xpubs)
is_change = False
# if policy is the same - probably change
@@ -160,6 +167,7 @@ class PSBTParser():
self.fee_amount = self.psbt.fee()
return True
@staticmethod
def trim(tx):
trimmed_psbt = psbt.PSBT(tx.tx)
@@ -170,8 +178,9 @@ class PSBTParser():
return trimmed_psbt
@staticmethod
def sigCount(tx):
def sig_count(tx):
cnt = 0
for i, inp in enumerate(tx.inputs):
cnt += len(list(inp.partial_sigs.keys()))
@@ -216,7 +225,7 @@ class PSBTParser():
@staticmethod
def __get_policy(scope, scriptpubkey, xpubs):
def _get_policy(scope, scriptpubkey, xpubs):
"""Parse scope and get policy"""
# we don't know the policy yet, let's parse it
script_type = scriptpubkey.script_type()
@@ -233,18 +242,18 @@ class PSBTParser():
policy = {"type": script_type}
# expected multisig
if "p2wsh" in script_type and scope.witness_script is not None:
m, n, pubkeys = PSBTParser.__parse_multisig(scope.witness_script)
m, n, pubkeys = PSBTParser._parse_multisig(scope.witness_script)
# check pubkeys are derived from cosigners
try:
cosigners = PSBTParser.__get_cosigners(pubkeys, scope.bip32_derivations, xpubs)
cosigners = PSBTParser._get_cosigners(pubkeys, scope.bip32_derivations, xpubs)
policy.update({"m": m, "n": n, "cosigners": cosigners})
except:
policy.update({"m": m, "n": n})
return policy
# returns m, n, and pubkeys from multisig script
@staticmethod
def __parse_multisig(sc):
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
@@ -271,8 +280,9 @@ class PSBTParser():
raise ValueError("Invalid multisig script")
return m, n, pubkeys
@staticmethod
def __get_cosigners(pubkeys, derivations, xpubs):
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):
@@ -295,8 +305,8 @@ class PSBTParser():
return sorted(cosigners)
@classmethod
def get_input_fingerprints(cls, psbt: PSBT) -> List[str]:
@staticmethod
def get_input_fingerprints(psbt: PSBT) -> List[str]:
"""
Exctracts the fingerprint from each input's derivation path.
@@ -311,8 +321,8 @@ class PSBTParser():
return list(fingerprints)
@classmethod
def has_matching_input_fingerprint(cls, psbt: PSBT, seed: Seed, network: str = "main"):
@staticmethod
def has_matching_input_fingerprint(psbt: PSBT, seed: Seed, network: str = "main"):
"""
Extracts the fingerprint from each psbt input utxo. Returns True if any match
the current seed.
+39 -44
View File
@@ -55,26 +55,31 @@ class SeedConstants:
class InvalidSeedException(Exception):
pass
class Seed:
def __init__(self,
mnemonic = None,
passphrase = "",
wordlist_language_code = SeedConstants.WORDLIST_LANGUAGE__ENGLISH) -> None:
mnemonic: List[str] = None,
passphrase: str = "",
wordlist_language_code: str = SeedConstants.WORDLIST_LANGUAGE__ENGLISH) -> None:
self.wordlist_language_code = wordlist_language_code
# The setters trigger a _generate_seed() call so we have to get the order right.
self._passphrase = ""
if not mnemonic:
raise Exception("Must initialize a Seed with a mnemonic List[str]")
self._mnemonic: List[str] = unicodedata.normalize("NFKD", " ".join(mnemonic).strip()).split()
# First set the mnemonic go through its setter's filtering
self.mnemonic = mnemonic
self._passphrase: str = ""
self.set_passphrase(passphrase, regenerate_seed=False)
if passphrase:
# Now run the passphrase's setter filter to change the final seed
self.passphrase = passphrase
self.seed_bytes: bytes = None
self._generate_seed()
@staticmethod
def get_wordlist(wordlist_language_code: str = SeedConstants.WORDLIST_LANGUAGE__ENGLISH):
def get_wordlist(wordlist_language_code: str = SeedConstants.WORDLIST_LANGUAGE__ENGLISH) -> List[str]:
# TODO: Support other bip-39 wordlist languages!
if wordlist_language_code == SeedConstants.WORDLIST_LANGUAGE__ENGLISH:
return bip39.WORDLIST
@@ -84,41 +89,30 @@ class Seed:
def _generate_seed(self) -> bool:
try:
self.seed = bip39.mnemonic_to_seed(self.mnemonic, password=self.passphrase, wordlist=self.wordlist)
return True
self.seed_bytes = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist)
except Exception as e:
print(repr(e))
return False
raise InvalidSeedException(repr(e))
@property
def mnemonic(self) -> str:
return self._mnemonic
def mnemonic_str(self) -> str:
return " ".join(self._mnemonic)
@property
def mnemonic_list(self) -> List[str]:
return self._mnemonic.split()
return self._mnemonic
@property
def mnemonic_display(self) -> str:
return unicodedata.normalize("NFC", self._mnemonic)
def mnemonic_display_str(self) -> str:
return unicodedata.normalize("NFC", " ".join(self._mnemonic))
@property
def mnemonic_display_list(self) -> List[str]:
return unicodedata.normalize("NFC", self._mnemonic).split()
@mnemonic.setter
def mnemonic(self, value):
if isinstance(value, list):
self._mnemonic = unicodedata.normalize("NFKD", " ".join(value).strip())
elif isinstance(value, str):
self._mnemonic = unicodedata.normalize("NFKD", value.strip())
self._valid = self._generate_seed()
return unicodedata.normalize("NFC", " ".join(self._mnemonic)).split()
@property
@@ -131,35 +125,36 @@ class Seed:
return unicodedata.normalize("NFC", self._passphrase)
@passphrase.setter
def passphrase(self, value):
if isinstance(value, str):
self._passphrase = unicodedata.normalize("NFKD", value)
def set_passphrase(self, passphrase: str, regenerate_seed: bool = True):
if passphrase:
self._passphrase = unicodedata.normalize("NFKD", passphrase)
else:
# Passphrase must always have a string value, even if it's just the empty
# string.
self._passphrase = ""
self._valid = self._generate_seed()
if regenerate_seed:
# Regenerate the internal seed since passphrase changes the result
self._generate_seed()
@property
def wordlist(self):
def wordlist(self) -> List[str]:
return Seed.get_wordlist(self.wordlist_language_code)
@wordlist.setter
def wordlist(self, language_code: str):
def set_wordlist_language_code(self, language_code: str):
# TODO: Support other bip-39 wordlist languages!
raise Exception("Not yet implemented!")
def get_fingerprint(self, network: str = "main"):
root = bip32.HDKey.from_seed(self.seed, version=NETWORKS[network]["xprv"])
def get_fingerprint(self, network: str = SeedConstants.MAINNET) -> str:
root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[network]["xprv"])
return hexlify(root.child(0).fingerprint).decode('utf-8')
### override operators
def __eq__(self, other):
if isinstance(other, Seed):
return self.seed == other.seed
return self.seed_bytes == other.seed_bytes
return False
def __bool__(self):
return self._valid
+31 -183
View File
@@ -1,199 +1,47 @@
# External Dependencies
from typing import List
from seedsigner.models import Seed
from seedsigner.models.seed import InvalidSeedException
class SeedStorage:
def __init__(self) -> None:
self.seeds: List[Seed] = []
self.pending_seed: Seed = None
def __init__(self) -> None:
self.seeds = []
self.pending_seed = None
# to be removed once the concepts of slots is removed
# this is a list of 3 indexes to the actual seed/mnemonic/passphrase data
# -1 means the slot is empty
self.slots = [-1,-1,-1]
def set_pending_seed(self, seed: Seed):
self.pending_seed = seed
def set_pending_seed(self, seed: Seed):
self.pending_seed = seed
def get_pending_seed(self) -> Seed:
return self.pending_seed
def finalize_pending_seed(self) -> int:
# Finally store the pending seed and return its index
self.seeds.append(self.pending_seed)
self.pending_seed = None
return len(self.seeds) - 1
def get_pending_seed(self) -> Seed:
return self.pending_seed
def clear_pending_seed(self):
self.pending_seed = None
# only adds a seed if it's valid and does not already exist
def add_mnemonic(self, mnemonic, passphrase: str = "", slot_num = -1) -> bool:
self.seeds.append(Seed(mnemonic, passphrase))
# used to track virtual slot number, TODO: remove concept in UI of slots
if slot_num > 0:
self.slots[slot_num - 1] = len(self.seeds) - 1
def add_seed(self, seed: Seed, slot_num = -1):
self.seeds.append(seed)
# used to track virtual slot number, TODO: remove concept in UI of slots
if slot_num > 0:
self.slots[slot_num - 1] = len(self.seeds) - 1
def add_passphrase_to_seed(self, seed, passphrase: str = ""):
if not isinstance(passphrase, str):
raise ValueError("Invalid passphrase format")
# find existing seed to add passphrase to
if seed in self.seeds:
idx = self.seeds.index(seed)
seed.passphrase = passphrase
return True
else:
return False
def remove_passphrase_from_seed(self, seed):
return self.add_passphrase_to_seed(seed, "")
# validates mnemonic part of a seed in list or string format
def validate_mnemonic(self, seed_phrase) -> bool:
try:
seed = Seed(seed_phrase)
except Exception as e:
return False
return True
def seed_count(self):
return len(self.seeds)
def finalize_pending_seed(self) -> int:
# Finally store the pending seed and return its index
if self.pending_seed in self.seeds:
index = self.seeds.index(self.pending_seed)
else:
self.seeds.append(self.pending_seed)
index = len(self.seeds) - 1
self.pending_seed = None
return index
#
# Slot related methods to be removed once the concepts of slots is removed
# TODO: remove concept in UI of slots
#
def get_mnemonic_from_slot(self, slot_num = 0):
if self.slots[slot_num - 1] == -1:
raise ValueError("Slot is unexpectedly empty")
return self.seeds[self.slots[slot_num - 1]].mnemonic
def get_passphrase_from_slot(self, slot_num = 0):
if self.slots[slot_num - 1] == -1:
raise ValueError("Slot is unexpectedly empty")
return self.seeds[self.slots[slot_num - 1]].passphrase
def add_passphrase_to_slot(self, passphrase: str = "", slot_num = -1):
if self.slots[slot_num - 1] == -1:
raise ValueError("Slot is unexpectedly empty")
return self.add_passphrase_to_seed(self.seeds[self.slots[slot_num - 1]], passphrase)
def clear_pending_seed(self):
self.pending_seed = None
def remove_passphrase_from_slot(self, slot_num = -1):
if self.slots[slot_num - 1] == -1:
raise ValueError("Slot is unexpectedly empty")
return self.remove_passphrase_from_seed(self.seeds[self.slots[slot_num - 1]])
def get_first_seed_slot(self):
if self.slots[0] != -1:
return self.slots[0] + 1
elif self.slots[1] != -1:
return self.slots[1] + 1
elif self.slots[2] != -1:
return self.slots[2] + 1
def validate_mnemonic(self, mnemonic: List[str]) -> bool:
try:
Seed(mnemonic=mnemonic)
except InvalidSeedException as e:
return False
return True
def check_slot_1(self) -> bool:
if self.slots[0] != -1:
return True
else:
return False
def check_slot_2(self) -> bool:
if self.slots[1] != -1:
return True
else:
return False
def check_slot_3(self) -> bool:
if self.slots[2] != -1:
return True
else:
return False
def check_slot(self, slot_num) -> bool:
if self.slots[slot_num-1] != -1:
return True
else:
return False
def check_slot_passphrase(self, slot_num) -> bool:
if self.slots[slot_num-1] != -1:
if self.seeds[self.slots[slot_num-1]].passphrase != "":
return True
return False
def slot_avaliable(self) -> bool:
if self.slots[0] == -1 or self.slots[1] == -1 or self.slots[2] == -1:
return True
return True
def num_of_saved_seeds(self) -> int:
count = 0
if self.slots[0] != -1:
count += 1
if self.slots[1] != -1:
count += 1
if self.slots[2] != -1:
count += 1
return count
def num_of_free_slots(self) -> int:
count = 0
if self.slots[0] == -1:
count += 1
if self.slots[1] == -1:
count += 1
if self.slots[2] == -1:
count += 1
return count
def save_seed_phrase(self, seed_phrase = [], slot_num = 0) -> bool:
return self.add_mnemonic(seed_phrase,slot_num=slot_num)
def save_passphrase(self, passphrase, slot_num = 0) -> bool:
return self.add_passphrase_to_slot(passphrase, slot_num=slot_num)
def get_seed_phrase(self, slot_num) -> list:
if self.slots[slot_num - 1] == -1:
return []
return self.seeds[self.slots[slot_num - 1]].mnemonic.split()
def get_passphrase(self, slot_num) -> str:
if self.slots[slot_num - 1] == -1:
return ""
return self.seeds[self.slots[slot_num - 1]].passphrase
def get_seed(self, slot_num) -> Seed:
if self.slots[slot_num - 1] == -1:
return Seed()
return self.seeds[self.slots[slot_num - 1]]
def delete_passphrase(self, slot_num) -> bool:
if self.slots[slot_num - 1] == -1:
return False
return self.remove_passphrase_from_seed(self.seeds[self.slots[slot_num - 1]])
def check_if_seed_valid(self, seed_phrase) -> bool:
return self.validate_mnemonic(seed_phrase)
def num_seeds(self):
return len(self.seeds)
+2 -2
View File
@@ -294,13 +294,13 @@ class PSBTFinalizeView(View):
loading_screen = LoadingScreenThread(text="Signing PSBT...")
loading_screen.start()
sig_cnt = PSBTParser.sigCount(psbt)
sig_cnt = PSBTParser.sig_count(psbt)
psbt.sign_with(psbt_parser.root)
trimmed_psbt = PSBTParser.trim(psbt)
loading_screen.stop()
if sig_cnt == PSBTParser.sigCount(trimmed_psbt):
if sig_cnt == PSBTParser.sig_count(trimmed_psbt):
# Signing failed / didn't do anything
# TODO: Reserved for Nick. Are there different failure scenarios that we can detect?
# Would be nice to alter the message on the next screen w/more detail.
+2 -2
View File
@@ -742,7 +742,7 @@ class SeedToolsView(View):
e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.COMPACTSEEDQR, wordlist=self.controller.settings.wordlist)
else:
e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEEDQR, wordlist=self.controller.settings.wordlist)
data = e.nextPart()
data = e.next_part()
qr = QR()
image = qr.qrimage(
data=data,
@@ -780,7 +780,7 @@ class SeedToolsView(View):
width = (qr_border + num_modules + qr_border) * pixels_per_block
height = width
data = e.nextPart()
data = e.next_part()
qr = QR()
image = qr.qrimage(
data,
+2 -2
View File
@@ -412,7 +412,7 @@ class SeedExportXpubDetailsView(View):
)
root = embit.bip32.HDKey.from_seed(
self.seed.seed,
self.seed.seed_bytes,
version=embit.networks.NETWORKS[self.controller.settings.get_value(SettingsConstants.SETTING__NETWORK)]["xprv"]
)
@@ -593,7 +593,7 @@ class SeedAddPassphraseView(View):
return Destination(BackStackView)
# The new passphrase will be the return value
self.seed.passphrase = ret
self.seed.set_passphrase(ret)
return Destination(SeedValidView)
+45 -43
View File
@@ -1,8 +1,11 @@
import pytest
from mock import MagicMock
from seedsigner.models import Seed, DecodeQR, DecodeQRStatus, QRType, EncodeQRDensity, PSBTParser
from seedsigner.models import Seed, DecodeQR, DecodeQRStatus, QRType, PSBTParser
from embit import psbt, bip39
from seedsigner.models.seed import SeedConstants
# this is an of this bug: https://github.com/Foundation-Devices/foundation-ur-py/issues/3
def test_ur_qr_decode_multisig():
@@ -39,7 +42,7 @@ def test_ur_qr_decode_multisig():
, "UR:CRYPTO-PSBT/26-29/LPCSCYCSCACFCMCPCYBBRSPTSKHDSSKSKGDYBYDMPEYTWFDIROBZCHGWCEFMFECKZEDYAEAELAAEAEAELAAEAEAELAAOAEAELAADAEAEAEAEAEAEAECPAOAXREAMJNKKZCESYKSWJOJPTBRDEOKGBSHHDTIYPSBEPSVWKSYKJPCPDKUYIMPEOSFECEQZEOVTMDDYAEAELAAEAEAELAAEAEAELAAOAEAELAADAEAEAEAEAEAEAECPAOAXWTSOSKSKFNWYBYLBNSHTEEEEECTORFLRBYCWROTKVTEMBDPDQDTEVALECAWNWDVYCELPDNDYMYDYAEAELAAEAEAELAAEAEAELAAOAEAELAADAEAEAEAEAEAEAEAEADADTKGHCLAOOEWLYATYBAOYLKOLJESGHTGYJLCLRHSGLFKPBDCLSEHYMT"
]
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
for i in qrcodes:
d.add_data(i)
assert d.qrType() == QRType.PSBTUR2
@@ -55,13 +58,13 @@ def test_ur_qr_decode_multisig():
assert str(tx) == base64_psbt
def test_base64_single_frame_singlsig():
def test_base64_single_frame_singlsig():
qrcodes = [
"cHNidP8BAHICAAAAAQDo5ey+2HIrNUkExsFhsImv1OK1cYA9x/bRjYQD+0UaAQAAAAD9////Apg6AAAAAAAAF6kUVuVZEcdpQ2zgABa9dRUNYHD4VuaHgSYAAAAAAAAWABQaLE4t0JbDRg4pNnmcf+cAWIcyawAAAAAAAQEfqGEAAAAAAAAWABRyuw9od6yuS0yiZljV0X12wG9e5CIGA/ZlEZvQubb6PmcnK+vlnd8aftYnrQ8wHYSxsD8tDp61GIshjoFUAACAAQAAgAAAAIAAAAAAAAAAAAAAAA=="
]
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
for i in qrcodes:
d.add_data(i)
assert d.qrType() == QRType.PSBTBASE64
@@ -79,11 +82,11 @@ def test_base64_single_frame_singlsig():
mnemonic = "height demise useless trap grow lion found off key clown transfer enroll"
pw = ""
seed = Seed(mnemonic, passphrase=pw, wordlist=bip39.WORDLIST)
seed = Seed(mnemonic, passphrase=pw)
assert seed.mnemonic == mnemonic
assert seed.mnemonic_str == mnemonic
assert seed.seed != None
assert seed.seed_bytes != None
pp = PSBTParser(tx,seed,"test")
@@ -97,8 +100,8 @@ def test_base64_single_frame_singlsig():
assert len(pp.destination_addresses) == 2
def test_ur2_sparrow_singlesig_to_self():
def test_ur2_sparrow_singlesig_to_self():
qrcodes = [
"UR:CRYPTO-PSBT/435-3/LPCFADQDAXCFAOOECYMSTBHDCSHDVYHTAXLATPBEOEDPAYIAWSYNAMSGSARSURCLDRKNFYZEWLHLMDTNATNYNSETJNZOKBGLFMRTMWOTLAFSSTYNTIPTLTTLWPVOZEHTAHTESNYADSHYVYFPDEBTBAIHIYTLWFUERDLKWTLSNLMWCHNDPSBWGRUTKSYLFLATVDCYNTDNGDTNURYLTTGUSSHNAEEEFZSRLEKEHDEMCYOTLDEMOTZORFDPKBCLBEHNREFWIDOSRSLRIOIAOXNLADDYENFMVOGLSGECDKBECSDSLUHNDTWYYTWFSNSPPESAZCGUDLDABAISLNJLWKJPRHBBRTEHYALRJYNYKPEOGLGLOLCPPYSEZTCHVLFMPELDGWCXGMSPHPGWAOKNBALOFXJLKNONPRBAPKYKIEVETEDWFXMDRTVWBYKSZTBTCLSGCSCPIYTPTYGYKIYNRTJLHLIEMNJYCTVYUEHGPRHN"
, "UR:CRYPTO-PSBT/440-3/LPCFADROAXCFAOOECYMSTBHDCSHDVYAOAEAEAEAORTHKYLIDWSLTBGSGTBSNIHDMPKVSAAWYKNIACELDCMGRLULRKNLUMKRSBACNCPTBADAEAEAECNCPAECXJPRPLBLURFIHEMSWRFLNECGOTKGRMHLBCSAXTYJKREPKWEBWEYAYIYNDJPCWFSLSZCZMZMZMDLIHKNPYASHLWECEMEGSSSVTAEEEFZSRNSKEGSVYWNCMAMTNATMWCYMYWTUEPDTDAEAEAEAECNCPAECXONRSTTHSLBCPREBTRHBGJKSEWYPMTNPKEHIHHELPQDLSLPFRWNKBBTVAPMDTBEDAZCZMZMZMAOPKPEAOAEAEAEAEAECHPTBBCAWPEEVAOYIMDISSIDNNRFISRHBDGLATSASWAXGULTPDHSAEAEAEAEAEAECMAEBBJPRKBSISKTPSPLGRGSOEIYHDTLTTKIKORTJLHYVEMNKPCTAEDLFPBASG"
@@ -106,7 +109,7 @@ def test_ur2_sparrow_singlesig_to_self():
, "UR:CRYPTO-PSBT/443-3/LPCFADRKAXCFAOOECYMSTBHDCSHDVYAXADCTPDIARTHKYLIDWSLTAASGSARSUECLSANEPDFZEHDLRNWSGLNNHTYTBNGRYLVYWDCPCLTDAEAEAEAEADDKAXTBCHOSVEHPAHTESNYAUYOYCKRNDRTBGWIHIYTLWFUERDNYWTMSLSROHKRPKELPLONDKOUEJSKBKGIHKNDNAYHLWENSMEGSSSHNAEEEFZSRNSKEGSVYWNEEAATAPYIMPASNHPFWMETEAYBYHFRSBBZCDSRDTTLNADLBEMFTTSSOAHENPFNTVSVEBDHNDTWYKBBDEYTSLPFRJSLBBTVADPDTBEDAKIZEZMZMAOPKPEAOAEAECPAOAXWKGRNLJEVSZMWSIDSOTTEHHETYFTFXCASBMKGTURBWJLIMTLLUFWSTSSKIZMLAFXBALUECZTFTHPISKTDWPEGRGSCPIYHDTLGYKIKORTJLHLVEMNKPCTAERKWPCMJN"
]
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
for i in qrcodes:
d.add_data(i)
assert d.qrType() == QRType.PSBTUR2
@@ -122,11 +125,11 @@ def test_ur2_sparrow_singlesig_to_self():
assert str(tx) == d.getBase64PSBT()
def test_base64_2_input_p2wsh():
def test_base64_2_input_p2wsh():
base64_psbt = "cHNidP8BALICAAAAAq1DhxRK+mUH4T6uUNob8bUaZ7MP+44MW4+Y9bOxpjhZAAAAAAD9////aWclWQ+45HKrI07r878E2UrAupT2paT4QurbmtNjYNQBAAAAAP3///8CQEIPAAAAAAAiACCpkDPDhmIzPlkJrjw9A71xjbIUWf3VUB7ooFJhTVm04tjSIQEAAAAAIgAgjQKFDauIXsV5u23LBdYgOwX1FwGGrLiQfWzBtFKZ7dIAAAAATwEENYfPBD5i336AAAACQStJhNVJul7vHKbo83VdmuAW2m0WaXLKDlFANn7dUNoCNbhLMdw4Knz7Q7o6exdL6UFhQegW9nJb0SUStbLEpawUAgjLdzAAAIABAACAAAAAgAIAAIBPAQQ1h88EnbHQAIAAAAI/2Nc7x7iMpJNapTe/OJTV4oifqzQcYY9KV2+PGRjCdQJoww1WnSNqfcxXGyux0q1PqfmzUqgJNqKJCpmqI9t47BQmu4PEMAAAgAEAAIAAAACAAgAAgE8BBDWHzwS6wUg5gAAAAh1Pvr3ZZ+GvcUwJl9OPz2cLXOnTAcBEC7zDtqIOt3IcA1aOofNgUZFu0baQw54SqOcGA7KAvTDOXygfKRilU2OqFHPF2gowAACAAQAAgAAAAIACAACAAAEBK4CWmAAAAAAAIgAgiYAxcG7dnrEiZ4VHFVHOo18XCalvhZYuMqBr9n7HESQBBWlSIQJOjQgMfX26XEf+trHIEk3rYkEX5Y2NfrFKQARPcd2X8iEDBWHUgq25PfHvE+hlcBryJG7wo2y8jKUSPY7sd85OOMchA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHU64iBgJOjQgMfX26XEf+trHIEk3rYkEX5Y2NfrFKQARPcd2X8hwmu4PEMAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgYDBWHUgq25PfHvE+hlcBryJG7wo2y8jKUSPY7sd85OOMccAgjLdzAAAIABAACAAAAAgAIAAIAAAAAAAQAAACIGA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHHHPF2gowAACAAQAAgAAAAIACAACAAAAAAAEAAAAAAQErgJaYAAAAAAAiACAzd60wM9EFnPHSNbsSJfyipL8myVLVP2/vwzotVUSNxQEFaVIhAiKCMRLlzIhLkRbLIUIMx5KYJM0v6LcjW/mS6K7eFGwiIQKDzUflU23LeecRgzDo5IBCEvaWGfHW7JkNxzXvuc7FdCEDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgFTriIGAiKCMRLlzIhLkRbLIUIMx5KYJM0v6LcjW/mS6K7eFGwiHAIIy3cwAACAAQAAgAAAAIACAACAAAAAAAAAAAAiBgKDzUflU23LeecRgzDo5IBCEvaWGfHW7JkNxzXvuc7FdBwmu4PEMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIgYDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgEcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAAAAAAABAWlSIQKtIdmtKKuZrH7f2R4iIU8RWVOrCdHVWBCS+0e9pZJy/iEDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYhA6GoQ/otQdk71nUpYZFfbkSKdBkkSj4CuPTPYrzGp6JrU64iAgKtIdmtKKuZrH7f2R4iIU8RWVOrCdHVWBCS+0e9pZJy/hwCCMt3MAAAgAEAAIAAAACAAgAAgAEAAAAAAAAAIgIDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYcc8XaCjAAAIABAACAAAAAgAIAAIABAAAAAAAAACICA6GoQ/otQdk71nUpYZFfbkSKdBkkSj4CuPTPYrzGp6JrHCa7g8QwAACAAQAAgAAAAIACAACAAQAAAAAAAAAAAA=="
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(base64_psbt)
assert d.qrType() == QRType.PSBTBASE64
@@ -135,7 +138,7 @@ def test_base64_2_input_p2wsh():
assert str(tx) == base64_psbt
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test")
pp = PSBTParser(tx, Seed(mnemonic), "test")
assert tx.inputs[0].witness_utxo.value == 10000000 # input amount 1 in psbt
@@ -149,11 +152,11 @@ def test_base64_2_input_p2wsh():
assert len(pp.destination_addresses) == 1
def test_base64_1_input_p2sh_p2wsh():
def test_base64_1_input_p2sh_p2wsh():
base64_psbt = "cHNidP8BAHICAAAAAR30J629i3Y/R8woRpLQ9JUa31rKxyM+Ny4NEsme48GWAAAAAAD9////Atw5XQUAAAAAF6kUdSESczdYagEyToVUSXyT8VTNz+OHgJaYAAAAAAAWABTmav7/w4OOcfCiewfjsA7eaujhYAAAAABPAQQ1h88EPmLffoAAAAHdXEj2dn8EYJ+rRdXEYu5laq6lJI5Mp+3t63ckwty05QKrJBNPewhwQaGPYRif6+XaxozFXvTXn7pU24H6fRy1FxQCCMt3MAAAgAEAAIAAAACAAQAAgE8BBDWHzwSdsdAAgAAAAeeOv56oeaaFTrNonMKDHk1C8brbWGFvdlecVue+v0/RAn/g4yI3oYsyen7OOcT7caYl4Mn7nQbyonHcusUR+GhzFCa7g8QwAACAAQAAgAAAAIABAACATwEENYfPBLrBSDmAAAABpzrb4oeEh2NNy/w/fr3osfyZTx7AaGDPAcP+LqeR3bYC5ioqmXPuazp69HwimlvecLylm9BLuyl/VpPXqiVrl20Uc8XaCjAAAIABAACAAAAAgAEAAIAAAQEgAOH1BQAAAAAXqRSv3gkn8731qcPbSDu4TJOlJJZ/PocBBCIAIOeiFBX5x0vX6CacrAUVovrs1DDCcKJS5qptFS3sjpDpAQVpUiECZ+pFYkOTVjB+eG+vQFA3MNjZWiA6DjRcs1Wl36A/zgMhA2Ygckuwjah29wiVRgA6wFx51+6ayrzeCIQ2eE4zfxPtIQOnUlBn22cn2CPCZkMSI6cDaZK2SlLV20rT6pqMoQCJsFOuIgYCZ+pFYkOTVjB+eG+vQFA3MNjZWiA6DjRcs1Wl36A/zgMcc8XaCjAAAIABAACAAAAAgAEAAIAAAAAAAAAAACIGA2Ygckuwjah29wiVRgA6wFx51+6ayrzeCIQ2eE4zfxPtHCa7g8QwAACAAQAAgAAAAIABAACAAAAAAAAAAAAiBgOnUlBn22cn2CPCZkMSI6cDaZK2SlLV20rT6pqMoQCJsBwCCMt3MAAAgAEAAIAAAACAAQAAgAAAAAAAAAAAAAEAIgAgZBwTq05RkpqKv6FV6LQjuM07Qv0/bYfWVc9NUQOFvwQBAWlSIQI32jVSdTgeu7+YZKrWfgOZ2J/LV36c5rBoApTzhrNlDCEC+hEqm3XmRt862AFFeyJ7p1m8A+V7czj6OajUNgCfg4EhA/MUVfxGh4k3Po3LB8CmMRsvIHcGNO0elUgETaITZA3UU64iAgI32jVSdTgeu7+YZKrWfgOZ2J/LV36c5rBoApTzhrNlDBwCCMt3MAAAgAEAAIAAAACAAQAAgAEAAAAAAAAAIgIC+hEqm3XmRt862AFFeyJ7p1m8A+V7czj6OajUNgCfg4EcJruDxDAAAIABAACAAAAAgAEAAIABAAAAAAAAACICA/MUVfxGh4k3Po3LB8CmMRsvIHcGNO0elUgETaITZA3UHHPF2gowAACAAQAAgAAAAIABAACAAQAAAAAAAAAAAA=="
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(base64_psbt)
assert d.qrType() == QRType.PSBTBASE64
@@ -206,7 +209,7 @@ def test_ur_legacy():
, "UR:BYTES/8OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/E0DKKUXEJDPX589HZPQ6SVQQQZQQZQQQSQQQQQYQQGQQPQQPQQQQQPQQQQQZYQSZ56D86Z6EZJEJALSG9KYCEJD4ES9TQR59AR7VDQQ9FKL44LSKUNL3E38ZAMFNQQQQSQQSQQYQQQQQPQQZQQQGQQGQQQQQGQQQQQQQ0P6SEH"
]
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
for i in qrcodes:
if d.add_data(i) == DecodeQRStatus.COMPLETE:
break
@@ -230,8 +233,8 @@ def test_ur_legacy():
assert pp.change_amount == 76318
def test_specter_multisig_animated_qr():
def test_specter_multisig_animated_qr():
qrcodes = [
"p2of5 OUsU2k9a6Valj6kLb/Bh2gB7YuxenNz7LY5PFJkKc60wAACAAQAAgAAAAIACAACATwEENYfPBEgFouKAAAACOeERUyCmIJuSdQNDB1mxkahw4tc2UCtjxJQoQGnRCtQCZUSXwQF+nA8dl/RG6U1b9ViWJxkzNrDCCagYEWch72cUrnhJ4jAAAIABAACAAAAAgAIAAIAAAQErckUPAAAAAAAiACDwHjagvyTvVDVn+RqxW0xaMLODBm2IVCwDBQm5uK8rDQEFR1IhAi/hk/Tz0aHcNwsVyv86bqQj30sUexPl"
, "p3of5 c738CqJg+flLIQKmgeTdVjPh9P+MPnsTPbFRcGGLPAO3iTyYBR7eYQC4VlKuIgYCL+GT9PPRodw3CxXK/zpupCPfSxR7E+VzvfwKomD5+UscrnhJ4jAAAIABAACAAAAAgAIAAIABAAAACgAAACIGAqaB5N1WM+H0/4w+exM9sVFwYYs8A7eJPJgFHt5hALhWHJkKc60wAACAAQAAgAAAAIACAACAAQAAAAoAAAAAAQErMHUAAAAAAAAiACAUS+ztgOw4wxEOYDdQEZp42Ii3sA39MdXZMWolM3s9VgEFR1Ih"
@@ -240,7 +243,7 @@ def test_specter_multisig_animated_qr():
, "p1of5 cHNidP8BAKQCAAAAA6DLc9RAdKwWQb/7Nrq1FyAtDQ3e0w5E4LkLpcrwBL0aAAAAAAD9////78NG2vqjywKaa0QHfo6C44o1+odXeWZ43FfHy4bLdxABAAAAAP3///8rhueJha90HzgJ6alnJ1uvi0Zbq0JQoXnwDb0Cjn79DwAAAAAA/f///wHWCBAAAAAAABYAFDraKFKubXzlSvRQy8V4X+TwKhLXAAAAAE8BBDWHzwQJeplqgAAAAqmHnnoRR+XJd7gJt/PhLazKeWAfCBLJ4/OF/s0PqqF1AzK8fctt"
]
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
for i in qrcodes:
if d.add_data(i) == DecodeQRStatus.COMPLETE:
break
@@ -278,7 +281,7 @@ def test_specter_multisig_animated_qr():
, "p2of5 OUsU2k9a6Valj6kLb/Bh2gB7YuxenNz7LY5PFJkKc60wAACAAQAAgAAAAIACAACATwEENYfPBEgFouKAAAACOeERUyCmIJuSdQNDB1mxkahw4tc2UCtjxJQoQGnRCtQCZUSXwQF+nA8dl/RG6U1b9ViWJxkzNrDCCagYEWch72cUrnhJ4jAAAIABAACAAAAAgAIAAIAAAQErckUPAAAAAAAiACDwHjagvyTvVDVn+RqxW0xaMLODBm2IVCwDBQm5uK8rDQEFR1IhAi/hk/Tz0aHcNwsVyv86bqQj30sUexPl"
]
d2 = DecodeQR(wordlist=bip39.WORDLIST)
d2 = DecodeQR()
for i in qrcodes2:
if d2.add_data(i) == DecodeQRStatus.COMPLETE:
break
@@ -308,26 +311,26 @@ def test_specter_multisig_animated_qr():
assert str(trimmed_tx2) == expected_signed_trimmed_base642
def test_mnemonic_qr():
def test_mnemonic_qr():
mnemonic = "height demise useless trap grow lion found off key clown transfer enroll"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
assert d.add_data(mnemonic) == DecodeQRStatus.COMPLETE
assert d.getSeedPhrase() == mnemonic.split()
bad_mnemonic = "height useless trap grow lion found off key clown transfer enroll"
d2 = DecodeQR(wordlist=bip39.WORDLIST)
d2 = DecodeQR()
assert d2.add_data(bad_mnemonic) == DecodeQRStatus.INVALID
bad2_mnemonic = "words not in the list of bip39"
d3 = DecodeQR(wordlist=bip39.WORDLIST)
d3 = DecodeQR()
assert d3.add_data(bad2_mnemonic) == DecodeQRStatus.INVALID
def test_short_4_letter_mnemonic_qr():
def test_short_4_letter_mnemonic_qr():
short_nm = "heig demi usel trap grow lion foun off key clow tran enro"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(short_nm)
assert d.isComplete() == True
@@ -351,77 +354,78 @@ def test_bitcoin_address():
main_bech32_address2 = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=12000"
main_bech32_address3 = "BITCOIN:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?junk"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(bad1)
assert d.qrType() == QRType.INVALID
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(legacy_address1)
assert d.getAddress() == legacy_address1
assert d.getAddressType() == "P2PKH-main"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(legacy_address2)
assert d.getAddress() == legacy_address2
assert d.getAddressType() == "P2PKH-main"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(main_bech32_address)
assert d.getAddress() == main_bech32_address
assert d.getAddressType() == "Bech32-main"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(test_bech32_address)
assert d.getAddress() == test_bech32_address
assert d.getAddressType() == "Bech32-test"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(main_nested_segwit_address)
assert d.getAddress() == main_nested_segwit_address
assert d.getAddressType() == "P2SH-main"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(test_nested_segwit_address)
assert d.getAddress() == test_nested_segwit_address
assert d.getAddressType() == "P2SH-test"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(main_bech32_address2)
assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
assert d.getAddressType() == "Bech32-main"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(main_bech32_address3)
assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
assert d.getAddressType() == "Bech32-main"
def test_seed_qr():
seed = "121802020768124106400009195602431595117715840445"
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(seed)
assert d.qrType() == QRType.SEEDQR
assert d.getSeedPhrase() == "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split()
def test_specter_wallet_json():
def test_specter_wallet_json():
parts = [
'p1of3 {"label": "SeedSigner Dev Funds", "blockheight": 692143, "descriptor": "wsh(sortedmulti(4,[e0811b6b/48h/0h/0h/2h]xpub6E8v7uy63pCeJvHe5W8ea8zTnCtKMFgMRb5bueWWcUFMw6sWmUwTqxM8cFiKQRWkA2Fxth9HJZufJwjWTTvU1UGZNpTrh9khrswYMgeHiCt/0/*,[852b308f/48h/0h/0h/2h]xpub6ErhgAWfnEqW7xDBm1iLq5JjNyUS65YUFnjHLrRv9zmdDEtuE75bpWQ8o6bSBnpT6AkrrsA8eA5SmEFArZn11KEPaZJzx9mHTXPWZCsxLyh/0/*,[7edf9c59/48h/0h/0h/2h]xpub6DaFfKoe7Wpofr'
, 'p2of3 bYeNo3Wv2AiLUMeyrPwotXfukFxUHbK4JxaLHTd5394QtH5wnjFzBgr2YnJpHhXv25Zsqv2APmMFvH1DsKHj5LCr3pmXs/0/*,[b433e095/48h/0h/0h/2h]xpub6EF51itHko2YhGTjVeuYbBgJjVbTzzpYzn2a3JwZHpDrMePRVgXGBHMx2Yv1KwgLsUn9i7ExcAo8uqMx4pDjVRY9J7qnceFAwRRj16dd5AS/0/*,[184d07eb/48h/0h/0h/2h]xpub6EEoTpcQu7N4R8D84pJjZ69j3mi'
, 'p3of3 nevnYLDDoo2HBzYBXTQ4rGVf4XGTyCYFwJuZdsF9MyFYJNzYEjg5LGMA1ubTGWuDnjHAZz6ficVRDTSy/0/*,[3e451efe/48h/0h/0h/2h]xpub6ExQPvQxGBMaPxr8Fv7Vq91ztJFFX3VWvtpvex6UPZ1AptTeuAiJGCtKkgwJkrwpMZMagh9ex6rL4sM8axfFcdQbERoFCRUKTJxrBkJh56g/0/*))#c44hel9e", "devices": [{"type": "other", "label": "Keith"}, {"type": "other", "label": "Nick"}, {"type": "other", "label": "Richard"}, {"type": "other", "label": "Stephan"}, {"type": "other", "label": "SeedSigner 1"}, {"type": "other", "label": "SeedSigner 2"}]}'
]
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(parts[1])
d.add_data(parts[2])
d.add_data(parts[0])
@@ -429,15 +433,13 @@ def test_specter_wallet_json():
assert d.qrType() == QRType.SPECTERWALLETQR
assert d.isComplete() == True
assert d.getWalletDescriptor() == "wsh(sortedmulti(4,[e0811b6b/48h/0h/0h/2h]xpub6E8v7uy63pCeJvHe5W8ea8zTnCtKMFgMRb5bueWWcUFMw6sWmUwTqxM8cFiKQRWkA2Fxth9HJZufJwjWTTvU1UGZNpTrh9khrswYMgeHiCt/0/*,[852b308f/48h/0h/0h/2h]xpub6ErhgAWfnEqW7xDBm1iLq5JjNyUS65YUFnjHLrRv9zmdDEtuE75bpWQ8o6bSBnpT6AkrrsA8eA5SmEFArZn11KEPaZJzx9mHTXPWZCsxLyh/0/*,[7edf9c59/48h/0h/0h/2h]xpub6DaFfKoe7WpofrbYeNo3Wv2AiLUMeyrPwotXfukFxUHbK4JxaLHTd5394QtH5wnjFzBgr2YnJpHhXv25Zsqv2APmMFvH1DsKHj5LCr3pmXs/0/*,[b433e095/48h/0h/0h/2h]xpub6EF51itHko2YhGTjVeuYbBgJjVbTzzpYzn2a3JwZHpDrMePRVgXGBHMx2Yv1KwgLsUn9i7ExcAo8uqMx4pDjVRY9J7qnceFAwRRj16dd5AS/0/*,[184d07eb/48h/0h/0h/2h]xpub6EEoTpcQu7N4R8D84pJjZ69j3minevnYLDDoo2HBzYBXTQ4rGVf4XGTyCYFwJuZdsF9MyFYJNzYEjg5LGMA1ubTGWuDnjHAZz6ficVRDTSy/0/*,[3e451efe/48h/0h/0h/2h]xpub6ExQPvQxGBMaPxr8Fv7Vq91ztJFFX3VWvtpvex6UPZ1AptTeuAiJGCtKkgwJkrwpMZMagh9ex6rL4sM8axfFcdQbERoFCRUKTJxrBkJh56g/0/*))#c44hel9e"
def test_specter_wallet_json2():
part = '{"label": "Testnet Single Zone", "blockheight": 2090512, "descriptor": "wpkh([990a73ad/84h/1h/0h]tpubDDHQMDnFdan2GyHBsG32VW9qiygbhVizGRTjiS3H79M49FSvpsvLXqLgp1yC7r43dXVHozWavi2Fc4WHUpZmQYmzoQbit28qJhLjScbAQWU/0/*)#ujr0xunp","devices": [{"type": "seedsigner", "label": "Single Seed Zone Testnet"}]}'
d = DecodeQR(wordlist=bip39.WORDLIST)
d = DecodeQR()
d.add_data(part)
assert d.qrType() == QRType.SPECTERWALLETQR
assert d.isComplete() == True
+15 -15
View File
@@ -16,8 +16,8 @@ def test_ur_qr_encode():
cnt = 0
while cnt <= 10:
fragment = e.nextPart()
img = e.part2Image(fragment)
fragment = e.next_part()
img = e.part_to_image(fragment)
cnt += 1
@@ -31,7 +31,7 @@ def test_specter_qr_encode():
cnt = 0
while cnt <= 10:
fragment = e.nextPart()
fragment = e.next_part()
if (cnt+1) == 1:
assert fragment == "p1of117 cHNidP8BAIkCAAAAAaLlQ/VRNpx3IFtoRTOCnq2xfJwg/n7R9XB0TTTnlX/UHQAAA"
elif (cnt+1) == 2:
@@ -55,7 +55,7 @@ def test_specter_qr_encode():
elif (cnt+1) == 11:
assert fragment == "p11of117 REKtbWkqONAvETqIlMPWJ/f1uBvSCGFm+zzDYnnEBtuAYjZiQrzj9mFLQz4JUwAAC"
img = e.part2Image(fragment)
img = e.part_to_image(fragment)
cnt += 1
def test_seedsigner_qr():
@@ -64,9 +64,9 @@ def test_seedsigner_qr():
e = EncodeQR(seed_phrase=mnemonic.split(" "), qr_type=QRType.SEEDQR)
print(e.nextPart())
print(e.next_part())
assert e.nextPart() == "121802020768124106400009195602431595117715840445"
assert e.next_part() == "121802020768124106400009195602431595117715840445"
def test_xpub_qr():
@@ -74,7 +74,7 @@ def test_xpub_qr():
e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUBQR, network="test", derivation="m/48h/1h/0h/2h")
assert e.nextPart() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5"
assert e.next_part() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5"
def test_specter_xpub_qr():
@@ -82,10 +82,10 @@ def test_specter_xpub_qr():
e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQR.DENSITY__LOW)
assert e.nextPart() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN"
assert e.nextPart() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo"
assert e.nextPart() == "p3of4 MxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1J"
assert e.nextPart() == "p4of4 sZsfdBzsK3VUC5"
assert e.next_part() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN"
assert e.next_part() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo"
assert e.next_part() == "p3of4 MxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1J"
assert e.next_part() == "p4of4 sZsfdBzsK3VUC5"
def test_ur_xpub_qr():
@@ -93,8 +93,8 @@ def test_ur_xpub_qr():
e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.URXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQR.DENSITY__MEDIUM)
assert e.nextPart() == "UR:CRYPTO-ACCOUNT/1-4/LPADAACSJKCYYNRDRTYAHDCAOEADCYSSMECPONAOLYTAADMETAADDLOXAXHDCLAOKSRLNLKPUEGYATHPMNRTAAOLAH"
assert e.nextPart() == "UR:CRYPTO-ACCOUNT/2-4/LPAOAACSJKCYYNRDRTYAHDCASNKKGHZMLUZORPVDGUOTECSTTKTOLPCWPTNTLKZTTIZTBEAAHDCXVDTPMYQDLAWNGL"
assert e.nextPart() == "UR:CRYPTO-ACCOUNT/3-4/LPAXAACSJKCYYNRDRTYAHDCARSTDSPZSBZSPGERLGDATUYNLPYBTGYIYYKBTWTAOSWKSVTSGCHBYDKYAVDSEPDJLAT"
assert e.nextPart() == "UR:CRYPTO-ACCOUNT/4-4/LPAAAACSJKCYYNRDRTYAHDCAAMTAADDYOEADLOCSDYYKADYKAEYKAOYKAOCYSSMECPONAYCYIOREKKJKAECADRRKIE"
assert e.next_part() == "UR:CRYPTO-ACCOUNT/1-4/LPADAACSJKCYYNRDRTYAHDCAOEADCYSSMECPONAOLYTAADMETAADDLOXAXHDCLAOKSRLNLKPUEGYATHPMNRTAAOLAH"
assert e.next_part() == "UR:CRYPTO-ACCOUNT/2-4/LPAOAACSJKCYYNRDRTYAHDCASNKKGHZMLUZORPVDGUOTECSTTKTOLPCWPTNTLKZTTIZTBEAAHDCXVDTPMYQDLAWNGL"
assert e.next_part() == "UR:CRYPTO-ACCOUNT/3-4/LPAXAACSJKCYYNRDRTYAHDCARSTDSPZSBZSPGERLGDATUYNLPYBTGYIYYKBTWTAOSWKSVTSGCHBYDKYAVDSEPDJLAT"
assert e.next_part() == "UR:CRYPTO-ACCOUNT/4-4/LPAAAACSJKCYYNRDRTYAHDCAAMTAADDYOEADLOCSDYYKADYKAEYKAOYKAOCYSSMECPONAYCYIOREKKJKAECADRRKIE"
+16 -16
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,7 +16,7 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
assert len(seed_phrase) == mnemonic_length
e = EncodeQR(seed_phrase=seed_phrase, qr_type=qr_type, wordlist=bip39.WORDLIST)
data = e.nextPart()
data = e.next_part()
print(data)
qr = QR()