mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
add bitcoin address verification
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
# External Dependencies
|
||||
import time
|
||||
import re
|
||||
from multiprocessing import Process, Queue
|
||||
from subprocess import call
|
||||
import os, sys
|
||||
from embit import bip32
|
||||
from embit import bip32, script, ec
|
||||
from embit.networks import NETWORKS
|
||||
from embit.descriptor import Descriptor
|
||||
from binascii import hexlify
|
||||
from threading import Thread
|
||||
|
||||
@@ -516,7 +518,6 @@ class Controller(Singleton):
|
||||
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
|
||||
xprv = root.derive(derivation)
|
||||
xpub = xprv.to_public()
|
||||
print(derivation)
|
||||
xpub_base58 = xpub.to_string(version=version)
|
||||
|
||||
self.signing_tools_view.display_xpub_info(fingerprint, derivation, xpub_base58)
|
||||
@@ -654,6 +655,204 @@ class Controller(Singleton):
|
||||
else:
|
||||
return Path.MAIN_MENU
|
||||
|
||||
elif decoder.isComplete() and decoder.isAddress():
|
||||
|
||||
address = decoder.getAddress()
|
||||
address_type = decoder.getAddressType()
|
||||
# address_parts = textwrap.wrap(address, 30)
|
||||
address_parts = [address[i:i+22] for i in range(0, len(address), 22)]
|
||||
address_part_1 = address_parts[0]
|
||||
address_part_2 = address_parts[1] if len(address_parts) >= 2 else ""
|
||||
address_part_3 = address_parts[2] if len(address_parts) >= 3 else ""
|
||||
self.menu_view.draw_modal(["Bitcoin Address", address_part_1, address_part_2, address_part_3], "", "Right to Continue")
|
||||
self.buttons.wait_for([B.KEY_RIGHT])
|
||||
|
||||
validate_network = NETWORKS[self.settings.network]
|
||||
if "main" in address_type:
|
||||
validate_network = NETWORKS["main"]
|
||||
elif "test" in address_type:
|
||||
validate_network = NETWORKS["test"]
|
||||
|
||||
r = 0
|
||||
if address_type in ("Bech32-main", "Bech32-test") and len(address) == 62:
|
||||
r = 2
|
||||
else:
|
||||
# check single sig vs multi sig address
|
||||
r = self.menu_view.display_generic_selection_menu(["Single Sig Seed", "JSON Wallet Desc"], "Validate Bitcoin Address")
|
||||
|
||||
if r == 1:
|
||||
|
||||
# validate single sig using seed
|
||||
|
||||
# No valid seed yet, If there is a saved seed, ask to use saved seed
|
||||
if self.storage.num_of_saved_seeds() > 0:
|
||||
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Use Save Seed?")
|
||||
if r == 1: #Yes
|
||||
slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None)
|
||||
if slot_num == 0:
|
||||
return Path.MAIN_MENU
|
||||
seed = self.storage.get_seed(slot_num)
|
||||
used_saved_seed = True
|
||||
|
||||
if not seed:
|
||||
# no valid seed yet, gather seed phrase
|
||||
# display menu to select 12 or 24 word seed for last word
|
||||
ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Main Menu ]")
|
||||
if ret_val == Path.SEED_WORD_12:
|
||||
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12)
|
||||
elif ret_val == Path.SEED_WORD_24:
|
||||
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24)
|
||||
elif ret_val == Path.SEED_WORD_QR:
|
||||
seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr()
|
||||
else:
|
||||
return Path.MAIN_MENU
|
||||
|
||||
if not seed:
|
||||
return Path.MAIN_MENU
|
||||
|
||||
# check if seed phrase is valid
|
||||
self.menu_view.draw_modal(["Validating Seed ..."])
|
||||
if not seed:
|
||||
self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue")
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT])
|
||||
return Path.MAIN_MENU
|
||||
|
||||
if len(seed.passphrase) == 0:
|
||||
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?")
|
||||
if r == 1:
|
||||
# display a tool to pick letters/numbers to make a passphrase
|
||||
seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
|
||||
if len(seed.passphrase) == 0:
|
||||
self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT])
|
||||
if input == B.KEY_LEFT:
|
||||
return Path.MAIN_MENU
|
||||
else:
|
||||
self.menu_view.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue")
|
||||
self.buttons.wait_for([B.KEY_RIGHT])
|
||||
|
||||
# display seed phrase
|
||||
while True:
|
||||
r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue")
|
||||
if r == True:
|
||||
break
|
||||
else:
|
||||
# Cancel
|
||||
return Path.MAIN_MENU
|
||||
|
||||
# Ask to save seed
|
||||
if self.storage.slot_avaliable() and used_saved_seed == False:
|
||||
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Save Seed?")
|
||||
if r == 1: #Yes
|
||||
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
|
||||
if slot_num in (1,2,3):
|
||||
self.storage.add_seed(seed, slot_num)
|
||||
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT])
|
||||
|
||||
#
|
||||
# Validate if address from seed
|
||||
#
|
||||
|
||||
# choose derivation standard
|
||||
r = self.menu_view.display_generic_selection_menu(["Native Segwit", "Nested Segwit", "Custom"], "Derivation Path?")
|
||||
if r == 1:
|
||||
script_type = "native segwit"
|
||||
elif r == 2:
|
||||
script_type = "nested segwit"
|
||||
elif r == 3:
|
||||
script_type = "custom"
|
||||
|
||||
# calculated derivation or get custom from keyboard entry
|
||||
if script_type == "custom":
|
||||
derivation = self.settings_tools_view.draw_derivation_keyboard_entry(existing_derivation=self.settings.custom_derivation)
|
||||
self.settings.custom_derivation = derivation # save for next time
|
||||
else:
|
||||
derivation = Settings.calc_derivation(validate_network, "single sig", script_type)
|
||||
|
||||
if derivation == "" or derivation == None:
|
||||
self.menu_view.draw_modal(["Invalid Derivation", "try again"], "", "Right to Continue")
|
||||
return Path.SEED_TOOLS_SUB_MENU
|
||||
|
||||
self.menu_view.draw_modal(["Checking Address", ""], "", "")
|
||||
|
||||
version = bip32.detect_version(derivation, default="xpub", network=validate_network)
|
||||
root = bip32.HDKey.from_seed(seed.seed, version=validate_network["xprv"])
|
||||
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
|
||||
xprv = root.derive(derivation)
|
||||
xpub = xprv.to_public()
|
||||
|
||||
for i in range(500):
|
||||
r_pubkey = xpub.derive([0,i]).key
|
||||
c_pubkey = xpub.derive([1,i]).key
|
||||
|
||||
recieve_address = ""
|
||||
change_address = ""
|
||||
|
||||
if "P2PKH" in address_type:
|
||||
recieve_address = script.p2pkh(r_pubkey).address(network=validate_network)
|
||||
change_address = script.p2pkh(c_pubkey).address(network=validate_network)
|
||||
elif "Bech32" in address_type:
|
||||
recieve_address = script.p2wpkh(r_pubkey).address(network=validate_network)
|
||||
change_address = script.p2wpkh(c_pubkey).address(network=validate_network)
|
||||
elif "P2SH" in address_type:
|
||||
recieve_address = script.p2sh(script.p2wpkh(r_pubkey)).address(network=validate_network)
|
||||
change_address = script.p2sh(script.p2wpkh(c_pubkey)).address(network=validate_network)
|
||||
|
||||
if address == recieve_address:
|
||||
self.menu_view.draw_modal(["Recieved Address "+str(i), "Verified"], "", "Right to Exit")
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT])
|
||||
return Path.MAIN_MENU
|
||||
if address == change_address:
|
||||
self.menu_view.draw_modal(["Change Address "+str(i), "Verified"], "", "Right to Exit")
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT])
|
||||
return Path.MAIN_MENU
|
||||
else:
|
||||
self.menu_view.draw_modal(["Checking Address "+str(i), "..."], "", "Right to Abort")
|
||||
if self.buttons.check_for_low(B.KEY_RIGHT) or self.buttons.check_for_low(B.KEY_LEFT):
|
||||
return Path.MAIN_MENU
|
||||
|
||||
if r == 2:
|
||||
|
||||
# second QR scan need PSBT now
|
||||
decoder2 = scan_qr("Scan Backup JSON QR")
|
||||
|
||||
if decoder2.isComplete() and decoder2.isWalletDescriptor():
|
||||
desc_str = decoder2.getWalletDescriptor()
|
||||
try:
|
||||
if len(re.findall(r'\[([0-9,a-f,A-F]+?)(\/[0-9,\/,h\']+?)\].*?(\/0\/\*)', desc_str)) > 0:
|
||||
p = re.compile(r'(\[[0-9,a-f,A-F]+?\/[0-9,\/,h\']+?\].*?)(\/0\/\*)')
|
||||
desc_str = p.sub(r'\1/{0,1}/*', desc_str)
|
||||
except:
|
||||
desc_str = decoder2.getWalletDescriptor()
|
||||
desc = Descriptor.from_string(desc_str)
|
||||
else:
|
||||
return Path.MAIN_MENU
|
||||
|
||||
#
|
||||
# Validate if address from descriptor
|
||||
#
|
||||
|
||||
for i in range(1000):
|
||||
r_derived = desc.derive(i,branch_index=0)
|
||||
c_derived = desc.derive(i,branch_index=1)
|
||||
|
||||
recieve_address = r_derived.address(validate_network)
|
||||
change_address = c_derived.address(validate_network)
|
||||
|
||||
if address == recieve_address:
|
||||
self.menu_view.draw_modal(["Recieved Address "+str(i), "Verified"], "", "Right to Exit")
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT])
|
||||
return Path.MAIN_MENU
|
||||
if address == change_address:
|
||||
self.menu_view.draw_modal(["Change Address "+str(i), "Verified"], "", "Right to Exit")
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT])
|
||||
return Path.MAIN_MENU
|
||||
else:
|
||||
self.menu_view.draw_modal(["Checking Address "+str(i), "..."], "", "Right to Abort")
|
||||
if self.buttons.check_for_low(B.KEY_RIGHT) or self.buttons.check_for_low(B.KEY_LEFT):
|
||||
return Path.MAIN_MENU
|
||||
|
||||
elif ( decoder.isComplete() and not decoder.isPSBT() ) or decoder.isInvalid():
|
||||
self.menu_view.draw_modal(["Not a valid PSBT QR"], "", "Right to Exit")
|
||||
input = self.buttons.wait_for([B.KEY_RIGHT])
|
||||
|
||||
@@ -2,6 +2,7 @@ from pyzbar import pyzbar
|
||||
from pyzbar.pyzbar import ZBarSymbol
|
||||
from enum import IntEnum
|
||||
import re
|
||||
import json
|
||||
import base64
|
||||
from embit import psbt
|
||||
from binascii import a2b_base64, b2a_base64
|
||||
@@ -25,6 +26,7 @@ class DecodeQR:
|
||||
self.base64_qr = Base64DecodeQR() # Single Segments Base64
|
||||
self.base43_qr = Base43DecodeQR() # Single Segment Base43
|
||||
self.address_qr = BitcoinAddressQR() # Single Segment bitcoin address
|
||||
self.specter_wallet_qr = SpecterDecodeWalletQR() # Specter Desktop Wallet Export decoder
|
||||
self.wordlist = None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
@@ -115,6 +117,12 @@ class DecodeQR:
|
||||
if rt == DecodeQRStatus.COMPLETE:
|
||||
self.complete = True
|
||||
return rt
|
||||
|
||||
elif self.qr_type == QRType.SPECTERWALLETQR:
|
||||
rt = self.specter_wallet_qr.add(qr_str)
|
||||
if rt == DecodeQRStatus.COMPLETE:
|
||||
self.complete = True
|
||||
return rt
|
||||
|
||||
else:
|
||||
return DecodeQRStatus.INVALID
|
||||
@@ -163,6 +171,9 @@ class DecodeQR:
|
||||
|
||||
def getAddressType(self):
|
||||
return self.address_qr.getAddressType()
|
||||
|
||||
def getWalletDescriptor(self):
|
||||
return self.specter_wallet_qr.getWalletDescriptor()
|
||||
|
||||
def getPercentComplete(self) -> int:
|
||||
if self.qr_type == QRType.PSBTUR2:
|
||||
@@ -207,7 +218,12 @@ class DecodeQR:
|
||||
return False
|
||||
|
||||
def isAddress(self):
|
||||
if self.qr_type in (QRType.BITCOINADDRESSQR):
|
||||
if self.qr_type == QRType.BITCOINADDRESSQR:
|
||||
return True
|
||||
return False
|
||||
|
||||
def isWalletDescriptor(self):
|
||||
if self.qr_type in (QRType.SPECTERWALLETQR, QRType.URWALLETQR, QRType.BLUEWALLETQR):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -231,22 +247,36 @@ class DecodeQR:
|
||||
# PSBT
|
||||
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
|
||||
return QRType.PSBTUR2
|
||||
elif re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE):
|
||||
elif re.search(r'^p(\d+)of(\d+) ([A-Za-z0-9+\/=]+$)', s, re.IGNORECASE): #must be base64 characters only in segment
|
||||
return QRType.PSBTSPECTER
|
||||
elif re.search("^UR:BYTES/", s, re.IGNORECASE):
|
||||
return QRType.PSBTURLEGACY
|
||||
elif DecodeQR.isBase64PSBT(s):
|
||||
return QRType.PSBTBASE64
|
||||
|
||||
# Bitcoin Address
|
||||
if DecodeQR.isBitcoinAddress(s):
|
||||
return QRType.BITCOINADDRESSQR
|
||||
|
||||
# Wallet Descriptor
|
||||
elif re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE):
|
||||
# when not a SPECTER Base64 PSBT from above, assume it's json
|
||||
return QRType.SPECTERWALLETQR
|
||||
elif re.search(r'^\{\"label\".*\"descriptor\"\:.*', s, re.IGNORECASE):
|
||||
# if json starting with label and contains descriptor, assume specter wallet json
|
||||
return QRType.SPECTERWALLETQR
|
||||
|
||||
_4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
|
||||
# create 4 letter wordlist only if not PSBT (performance gain)
|
||||
try:
|
||||
_4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
|
||||
except:
|
||||
_4LETTER_WORDLIST = []
|
||||
|
||||
# Seed
|
||||
if re.search(r'\d{48,96}', s):
|
||||
return QRType.SEEDSSQR
|
||||
|
||||
# Bitcoin Address
|
||||
elif DecodeQR.isBitcoinAddress(s):
|
||||
return QRType.BITCOINADDRESSQR
|
||||
|
||||
# Seed
|
||||
elif all(x in wordlist for x in s.strip().split(" ")):
|
||||
# checks if all words in list are in bip39 word list
|
||||
return QRType.SEEDMNEMONIC
|
||||
@@ -255,8 +285,8 @@ class DecodeQR:
|
||||
return QRType.SEED4LETTERMNEMONIC
|
||||
elif DecodeQR.isBase43PSBT(s):
|
||||
return QRType.PSBTBASE43
|
||||
else:
|
||||
return QRType.INVALID
|
||||
|
||||
return QRType.INVALID
|
||||
|
||||
@staticmethod
|
||||
def isBase64(s):
|
||||
@@ -709,6 +739,90 @@ class BitcoinAddressQR:
|
||||
return "Unknown"
|
||||
return None
|
||||
|
||||
###
|
||||
### SpecterDecodeWalletQR Class
|
||||
### Purpose: used in DecodeQR to decode animated frames to get a wallet descriptor from Specter Desktop
|
||||
###
|
||||
|
||||
class SpecterDecodeWalletQR:
|
||||
|
||||
def __init__(self):
|
||||
self.total_segments = None
|
||||
self.collected_segments = 0
|
||||
self.complete = False
|
||||
self.segments = []
|
||||
|
||||
def add(self, segment):
|
||||
if self.total_segments == None:
|
||||
self.total_segments = SpecterDecodeWalletQR.totalSegmentNum(segment)
|
||||
self.segments = [None] * self.total_segments
|
||||
elif self.total_segments != SpecterDecodeWalletQR.totalSegmentNum(segment):
|
||||
raise Exception('Specter Desktop segment total changed unexpectedly')
|
||||
|
||||
if self.segments[SpecterDecodeWalletQR.currentSegmentNum(segment) - 1] == None:
|
||||
self.segments[SpecterDecodeWalletQR.currentSegmentNum(segment) - 1] = SpecterDecodeWalletQR.parseSegment(segment)
|
||||
self.collected_segments += 1
|
||||
if self.total_segments == self.collected_segments:
|
||||
if self.validateWalletDescriptor():
|
||||
self.complete = True
|
||||
return DecodeQRStatus.COMPLETE
|
||||
else:
|
||||
return DecodeQRStatus.INVALID
|
||||
return DecodeQRStatus.PART_COMPLETE # new segment added
|
||||
|
||||
return DecodeQRStatus.PART_EXISTING # segment not added because it's already been added
|
||||
|
||||
def validateJson(self) -> str:
|
||||
try:
|
||||
j = "".join(self.segments)
|
||||
json.loads(j)
|
||||
except json.decoder.JSONDecodeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def validateWalletDescriptor(self):
|
||||
if self.validateJson():
|
||||
j = "".join(self.segments)
|
||||
data = json.loads(j)
|
||||
if "descriptor" in data:
|
||||
return True
|
||||
return False
|
||||
|
||||
def getWalletDescriptor(self) -> str:
|
||||
if self.validateWalletDescriptor():
|
||||
j = "".join(self.segments)
|
||||
data = json.loads(j)
|
||||
return data['descriptor']
|
||||
return None
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
if self.complete and self.validateWalletDescriptor():
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def currentSegmentNum(segment) -> int:
|
||||
if DecodeQR.SegmentType(segment) == QRType.SPECTERWALLETQR:
|
||||
if re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE) != None:
|
||||
return int(re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE).group(1))
|
||||
else:
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def totalSegmentNum(segment) -> int:
|
||||
if DecodeQR.SegmentType(segment) == QRType.SPECTERWALLETQR:
|
||||
if re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE) != None:
|
||||
return int(re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE).group(2))
|
||||
else:
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def parseSegment(segment) -> str:
|
||||
try:
|
||||
return re.search(r'^p(\d+)of(\d+) (.+$)', segment, re.IGNORECASE).group(3)
|
||||
except:
|
||||
return segment
|
||||
|
||||
###
|
||||
### DecodeQRStatus Class IntEum
|
||||
### Purpose: used in DecodeQR to communicate status of adding qr frame/segment
|
||||
|
||||
@@ -18,4 +18,7 @@ class QRType(IntEnum):
|
||||
XPUBQR = 20
|
||||
SPECTERXPUBQR = 21
|
||||
BITCOINADDRESSQR = 30
|
||||
SPECTERWALLETQR = 40
|
||||
URWALLETQR = 41
|
||||
BLUEWALLETQR = 42
|
||||
INVALID = 100
|
||||
@@ -404,6 +404,40 @@ def test_bitcoin_address():
|
||||
assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
|
||||
assert d.getAddressType() == "Bech32-main"
|
||||
|
||||
|
||||
|
||||
def test_seed_qr():
|
||||
seed = "121802020768124106400009195602431595117715840445"
|
||||
|
||||
d = DecodeQR(wordlist=bip39.WORDLIST)
|
||||
d.addString(seed)
|
||||
|
||||
assert d.qrType() == QRType.SEEDSSQR
|
||||
assert d.getSeedPhrase() == "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split()
|
||||
|
||||
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.addString(parts[1])
|
||||
d.addString(parts[2])
|
||||
d.addString(parts[0])
|
||||
|
||||
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():
|
||||
|
||||
1_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.addString(1_part)
|
||||
|
||||
assert d.qrType() == QRType.SPECTERWALLETQR
|
||||
assert d.isComplete() == True
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user