From 2cf759e96e4ada92b1d913596f151af5d52ebd7e Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Wed, 4 Sep 2024 11:11:16 -0400 Subject: [PATCH 01/60] added support for BBQr decoding support --- src/seedsigner/models/decode_qr.py | 75 +++++++++++++++++++++++++++--- src/seedsigner/models/qr_type.py | 1 + 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 4ac16642..c0dcbf33 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -2,6 +2,7 @@ import base64 import json import logging import re +import zlib from binascii import a2b_base64, b2a_base64 from enum import IntEnum @@ -11,6 +12,7 @@ from pyzbar.pyzbar import ZBarSymbol from urtypes.crypto import PSBT as UR_PSBT from urtypes.crypto import Account, Output from urtypes.bytes import Bytes +from base64 import b32encode, b32decode from seedsigner.helpers.ur2.ur_decoder import URDecoder from seedsigner.models.qr_type import QRType @@ -74,6 +76,9 @@ class DecodeQR: elif self.qr_type == QRType.PSBT__BASE43: self.decoder = Base43PsbtQrDecoder() # Single Segment Base43 + elif self.qr_type == QRType.PSBT__BBQR: + self.decoder = BBQRPsbtQrDecoder() # BBQr Decoder + elif self.qr_type in [QRType.SEED__SEEDQR, QRType.SEED__COMPACTSEEDQR, QRType.SEED__MNEMONIC, QRType.SEED__FOUR_LETTER_MNEMONIC, QRType.SEED__UR2]: self.decoder = SeedQrDecoder(wordlist_language_code=self.wordlist_language_code) @@ -229,7 +234,7 @@ class DecodeQR: if self.qr_type in [QRType.PSBT__UR2, QRType.OUTPUT__UR, QRType.ACCOUNT__UR, QRType.BYTES__UR]: return int(self.decoder.estimated_percent_complete(weight_mixed_frames=weight_mixed_frames) * 100) - elif self.qr_type in [QRType.PSBT__SPECTER]: + elif self.qr_type in [QRType.PSBT__SPECTER, QRType.PSBT__BBQR]: if self.decoder.total_segments == None: return 0 return int((self.decoder.collected_segments / self.decoder.total_segments) * 100) @@ -262,6 +267,7 @@ class DecodeQR: QRType.PSBT__SPECTER, QRType.PSBT__BASE64, QRType.PSBT__BASE43, + QRType.PSBT__BBQR, ] @@ -326,9 +332,6 @@ class DecodeQR: @staticmethod def detect_segment_type(s, wordlist_language_code=None): - # print("-------------- DecodeQR.detect_segment_type --------------") - # print(type(s)) - # print(len(s)) try: # Convert to str data @@ -338,6 +341,9 @@ class DecodeQR: # TODO: Convert the test suite rather than handle here? s = s.decode('utf-8') + logger.debug(f"segment string: {s}") + logger.debug(f"segment string length: {len(s)}") + # PSBT if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE): return QRType.PSBT__UR2 @@ -357,6 +363,9 @@ class DecodeQR: elif DecodeQR.is_base64_psbt(s): return QRType.PSBT__BASE64 + elif re.search("^B\$[2HZ]P[0-9A-Z]...", s): # https://github.com/coinkite/BBQr/blob/master/BBQr.md#spliting-the-data + return QRType.PSBT__BBQR + # Wallet Descriptor desc_str = s.replace("\n","").replace(" ","") if re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE): @@ -654,8 +663,9 @@ class BaseAnimatedQrDecoder(BaseQrDecoder): elif self.total_segments != self.total_segment_nums(segment): raise Exception('Segment total changed unexpectedly') - if self.segments[self.current_segment_num(segment) - 1] == None: - self.segments[self.current_segment_num(segment) - 1] = self.parse_segment(segment) + current_segment_num = self.current_segment_num(segment) + if self.segments[current_segment_num - 1] == None: + self.segments[current_segment_num - 1] = self.parse_segment(segment) self.collected_segments += 1 if self.total_segments == self.collected_segments: if self.is_valid: @@ -704,6 +714,59 @@ class SpecterPsbtQrDecoder(BaseAnimatedQrDecoder): +class BBQRPsbtQrDecoder(BaseAnimatedQrDecoder): + """ + Used to decode BBQR Animated PSBT encoding. + """ + def __init__(self): + super().__init__() + self.encoding = None + + + def get_data(self) -> str: + logger.debug("BBQRPsbtQrDecoder get_data") + data = "".join(self.segments) + if self.complete and self.encoding: + if self.encoding == 'H': + return b''.join(bytes.fromhex(s) for s in self.segments) + + # base32 decode, but insert padding for API + rv = b'' + for p in self.segments: + padding = (8 - (len(p) % 8)) % 8 + rv += b32decode(p + (padding*'=')) + + if self.encoding == 'Z': + # decompress + z = zlib.decompressobj(wbits=-10) + rv = z.decompress(rv) + rv += z.flush() + + return rv + + return None + + def current_segment_num(self, segment) -> int: + current_segment = int(segment[6:8], 36) + 1 + logger.debug(f"BBQRPsbtQrDecoder current_segment_num {current_segment}") + return current_segment + + + def total_segment_nums(self, segment) -> int: + total_segments = int(segment[4:6], 36) + logger.debug(f"BBQRPsbtQrDecoder total_segment_nums {total_segments}") + return total_segments + + + def parse_segment(self, segment) -> str: + self.encoding = segment[2] + file_type = segment[3] + data = segment[8:] + + return data.strip() + + + class Base64PsbtQrDecoder(BaseSingleFrameQrDecoder): """ Decodes single frame base64 encoded qr image. diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py index 6ec1f5ec..bd678f7e 100644 --- a/src/seedsigner/models/qr_type.py +++ b/src/seedsigner/models/qr_type.py @@ -6,6 +6,7 @@ class QRType: PSBT__SPECTER = "psbt__specter" PSBT__BASE43 = "psbt__base43" PSBT__UR2 = "psbt__ur2" + PSBT__BBQR = "psbt__bbqr" SEED__SEEDQR = "seed__seedqr" SEED__COMPACTSEEDQR = "seed__compactseedqr" From 389f0479d21b18894f4e58830ff4eb5d1e41d4e0 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Wed, 4 Sep 2024 21:02:23 -0400 Subject: [PATCH 02/60] add unit tests for bbqr and fix deprecation warning for invalid escape sequence '$' --- src/seedsigner/models/decode_qr.py | 2 +- tests/test_decodepsbtqr.py | 95 ++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index c0dcbf33..ec99f292 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -363,7 +363,7 @@ class DecodeQR: elif DecodeQR.is_base64_psbt(s): return QRType.PSBT__BASE64 - elif re.search("^B\$[2HZ]P[0-9A-Z]...", s): # https://github.com/coinkite/BBQr/blob/master/BBQr.md#spliting-the-data + elif re.search(r"^B\$[2HZ]P[0-9A-Z]{4}", s): # https://github.com/coinkite/BBQr/blob/master/BBQr.md#spliting-the-data return QRType.PSBT__BBQR # Wallet Descriptor diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index 9b7d6981..230d8e68 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -357,6 +357,7 @@ def test_seed_qr(): assert d.get_seed_phrase() == "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' @@ -374,6 +375,7 @@ def test_specter_wallet_json(): assert d.get_wallet_descriptor() == "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"}]}' @@ -382,3 +384,96 @@ def test_specter_wallet_json2(): assert d.qr_type == QRType.WALLET__SPECTER assert d.is_complete + + + +def test_basic_1_in_2_out_bbqr_psbt_qr(): + parts = [ + 'B$ZP0100FMUE4KXZZ7EHBEUJQGAYDGMXLP2O34WEVGERCKNOQWTY3URDYXHGTXTTHOVHKW6YXZQYEVSN6UGQMEHYB4CP4RI3BJK43MVLSZ7ZRH5R5SB4527A5C6N3LL65GZRPNZZGU2NK332BFLVR7VVKSYTCILLOLCJZ5PGFQGCC2U37YW5J6CTXMW6NHMT533MMPJ3YXQQVGL2ZZ76AADRETFVNCV4MGOMY6GEPPPG5XSZFIOQ6PW2JUFQVDWX5FMIHLHCI3Y23V5WGPPK5OF5GSFOKZI576FTVA5FGM5NNACNMNSAQBLZQZY62PJQS7Q72PQGVTABGZRVEPB4HL5MGCB3SVONS5P7W7BGVVT557AFA33L63OW43GW5BAYVTCMNLH5XCUEDNRF4HXFFLMHCWV25FZM6VQNQFPW2PZWI5MXP3QKHGIKOPMXR6CZGOO6CFSQ42IKN3USTQ44OHX53YYXTSSX74XXZ7ZPFXMRMCDTBDJU3JIJ2Z6ZXJXOK3LD6YHA3NYV4PDUDFR2K2H6RIPZ4BI7V2YOXRXPQY5YJELSQ4EHNXBM6HQX74PC26NXEXI6B7UR6IQMH446CDUR36RHYRKV5DE5ZUUVS5FEIWNKJLGJXHGPK66WGP5YNX3PEW24ZRLTJYAOMEBNXIGEYT2OSSUTLGKFZX7M7VTZUGI5LH5GM6X6PC76XZOO3PM7WV67BCM3D5M3XMAEQBYPJAWRAJZQAMEQA' + ] + + d = DecodeQR() + for i in parts: + if d.add_data(i) == DecodeQRStatus.COMPLETE: + break + assert d.qr_type == QRType.PSBT__BBQR + + #complete should be true + assert d.is_complete + + + +def test_basic_1_in_2_out_bbqr_non_compressed_psbt_qr(): + parts = [ + 'B$2P0200OBZWE5H7AEAMGAQAAAAAGLZDYX3EGIZDZMNR5DQRFVCQAIG3FL5BAHWRJ3PGCJ3NTFFLVFRKBIAAAAAA7X77776SNCAHN5R4BCQGWFWOT7MQUMN7IYDICAIMVZOQWEMKWXPVVJWTZ4AAAAAAAD6777775FCZSAY26LQG5V3DG7J4Y6HFZSAL65OCPEMYB6SLACVNXVAAIOPAAAAAAAAP377774BBAJYAAAAAAAAACYABITRMR72K5TBKF5JQHFEP3N7MTN6BSTVZO6AAAAAAAAAAAALAAFH572746E5HEAFOBJOKAWW3AZSTJ5YGQ4IAAAAAATYBAQ2YPTYD3PPOOG4AAAAABMZKKMFX7N7X3N7UFH3DJ6TPIJJNKDDEZ2V5WXVEFF7ED2KPRLQWAJQJYJPKMVHIF3B567MHOVVTZEREDZ3JU5K7433BO4PCIWIAXMMKCEDF7NB74VAAACAACAAAQAAAAAEAAAAQB7MIAEBAAAAAAFYISWF3KWA2BQJIWFBLOAQUC75FZ7U663FLHY2UPMSMBIORDQQ4ICAAAAAAB7P7777QWEBHAAAAAAAAAALAAFDLBORR4YUXZXUT2AMGPV5STUNSAWXZC3AQE4AAAAAAAAABMAAUGQY57O3ZSGKZQPJIVDDRHCZR26EBKEXBCATQAAAAAAAAAFQACRQDOY52JHJTNRMSBEU3ZNF7MU3CQ67OE7UAGAAAAAAAAAAWAAKHTOIMMYL4F27RNYMKHR27HEACX2GC5OBRAJYAAAAAAAAACYABIUXQGIMLE6T4JV4HB7DBKMD2ZSOAJ3BNWEBHAAAAAAAAAALAAFG6U2YBVVC5EOTPROOXWHTT3NQNDN24INYQE4AAAAAAAAABMAAUCUT7GZTM564L2AHZT2WWKSNMW2XN6SRTTQZSWAAAAAAAAFQACSIVWLYCFXTXUOPJHVTG424NY2MHVFAUJ4ICOAAAAAAAAAAWAAKO5BX4TBBZOLBTGINJPGZXHCHXLSFXOGFRAJYAAAAAAAAACYABJ6E4FYLPW652VRHZ2TXZ', + 'B$2P0201MH36EIIPZFAIMEBHAAAAAAAAAALAAFGZMA575IOQ64JRLZ7FDFP5HXDSI7T5TCB3K4TQAAIBD4ICOAAAAAAAAAAWAAKNSYB372Q5B5YTCXT6KGK72POHER7H3GECEBQDUQRHUJOBI3ZLGB5D45DTXHXDO2KR3SYOVVK7B3IWBLBTMZOLAWMBQZP3IP7FIAAAQAAQAAEAAAAABAAAAAAAALAAAAAAAAIAOEBAAAAAAGYGM76R4MFE4PVHWKO7UMETKM7CMCYSGXHLZBKFSTV77SGMMGAIWAIAAAAAB7P7777QF2ADAAAAAAAAAALAAFDVHQFTAZIUYBERQK72U7UJXL4TP4IATCF7VQXQAAAAAAABMAAUJKWTGIZ24GA6VEFIYS3FJBDRMPCZEAN75DGSKAABAEP6QAYAAAAAAAAACYABI5J4BMYGKFGAJEMCX6VH5CN27E37CAEYQIQGALL3CUCJCC5XCJYUJJZQTXXO3YZORCQGK4GZNW3IGGPLOVQF2VCBEGDF7NB74VAAACAACAAAQAAAAAEAAAAAAAAEAAAAAAABABJAEAAAAAA7YEPDL6LPKO5S4PGHT7RSZD7WRGO3JUDPI2L2LPGRPBZ3ORRB5NYAAAAAAAH577776ALCA4AAAAAAAAABMAAUXRNOMGZL4W3EIBN7B33H6UPCYV3NJF32PVLCOAABAEPWEBYAAAAAAAAACYABJPC24YNSXZNWIQC36DXWP5I6FRLW2SLXUIQGALPV276UOSFQ4GYKIORFRCTTGOSCPBCJ7QWFFL2K4UCZDA27YOVQGGDF7NB74VAAACAACAAAQAAAAAEAAAAAAAA6AAAAAABCAIB2VZDSEABFIYX7GUZUJM27QMBDRYRRCHKG6SGYIID7ZDZIBMWDLLAYMX5UH7SUAAAIAAIAACAAAAAAQAAAAAAAGUAAAAAAEIBAENBW7O5JKFANLCOJJSIFIZLZ2JPIDZRECITJNP2QDDZACAOE57MRDBS7WQ76KQAABAABAAAIAAAAACAACAAAAAIQAAAAAA', + ] + + d = DecodeQR() + for i in parts: + if d.add_data(i) == DecodeQRStatus.COMPLETE: + break + assert d.qr_type == QRType.PSBT__BBQR + + #complete should be true + assert d.is_complete + + + +def test_large_multipart_bbqr_psbt_qr(): + parts = [ + 'B$ZP04003WJHSUCTQ4PIK3YSIJKDNJLBCXBGKDICWLEI4D4CBWEBAEIEEJFSCLFSDACSVGGKEJBAQWCBWAUAEATBRHAFHCXMKEQBDGVAMKSFIZBJFCSWAUMUPUVSRMR77N6H7NBTZROGMLDKL5TH5E3E4YTHHPXPTSJ2O7BINMYCAEIOAAAAZH3LXF52M3I63NSBGPULBVT34BA7XQIKWNHBOV7D37BSCX6WIKYV63XCCYDFNVWQ37VDXBIMUH27CQHIGNTLS44FRYBV7QW32CULRZGLY5WO5ZXO6NJRFHY2VCG5IHZ3BJ6RYABDZ7L7R47JFBP5UBXVR46NCVK3OLSXIGCCGO7FUIPWFYLRUH4PDZPEOQAVTQJ7DYBWAMDVMDDXMJOY7OPYAJLROUZR2KBVLUU6H6DPT55OQDW55CXBZW5WARPNAPUQPH4J4BGVI6GGXH2KU42DFLE5NKJQQXX2EOISJVAFG4I5L4MMFYV44ZDWFPKYJS5QHRZGK63T53YJHEIJORCV5F5VFDU233LWQZIPDRORLJDLD7JC3IY2M4EBURPMKHSTNPXRMKXGTKVWSNMXOMCYKQUCR6QBZHYXWMWMUQBZHVQYYCZB3GT52KDFO6ZZHJXA3Y6ZMFOZ7UIQPHGYDKYW4J7VLVFECBJ4PJIHB7C3HRJPJXYXMLHIIZKSOG6HWENEGPLFKCICHNU7RCCOGZEHW7HGGXENTRV4OYJ32HKZEUZ6SJRBLO4TLF3YQVW3LNS4OHEPIBA3ZSXW6JZQM5UTQIM732LBENYKXYDDPMVI6AVN6I22JJTXYEDP23BANCYIKE3MPQ42AIBV5F3EDWGH53GCP5XGYSBC557I2FQJTF7EUQQEZYTOM4DJ2BBIUKCMEDTMUPQFZ4QIEWD4MJZBHIR36DXB336K57AWDDHKQSG3MDHIOCBRBO3YZXPTLZV4LJPL4ZVQ5HFCCHLJ6OBIJE6AJSXJFS7S6N7Y5MCHH6K32CDA6BBIF6OM7SVGUBJ46OT2YSCHGONHGJ4U2GBBYPUEZQDIKMMHRS5OV2WS6DWFTW4MQ5Z55JTR256REQT3S5FMMOSCVKDBJUMWMQINRUVZCCFAE2DBUTIX4Z4VTL4FM52OONLPKAFPPTIKFVSB42FTPMV3', + 'B$ZP0401R2NIJC6YGI5PXFPPSXIZIJ6F5MPABQ7JTUK3MABHDBG7WWGCED33GILBH3IMEZENRW77JOMKNVVTZNDY4XGRE5PH7EQFYQ4WTBWIJBVLV7CVL453J47I4EHIPX6D2QRWWCQDPWWCFPSRT5R27HHKS22TMGPIZZMDYLEWXUTPMXHAM7LRETD4ANY5OS4EDCC5XTTO6GTTLYWS6XZT3B2OLEEOWR4UDQSJMATFPSLF7G4WGTHBAJE33CR5U3LN4LKUZQ2VPU3KF3TQUCUFAQPUCOJ6F5TFS3CEDD2D4PRDMEXXYE3FUPKTQME6VJAXRXJRS7JR2YIK7VXXD4K2O6RWAAKXOMNRLSBAWCSXGIIUAXIMESFD7XHCUNPRVXXJZZVM5IAV56NAIWWYGDHUYKQPN4ZZBGJRPB5KS6CYJGQQSSYSECQTRC43ET6BGOECAFU7TDOIJ2AOXR54PO7SPNYBRIZ2MGRS2A6VBDKMJCJ5JXFBUACAQW6TXYH5AHXT4A7S4JIE27BYDNZ4RKFLVYXA3T46WXGXQOHHNUNHPXJHEG2HPKLEVQR2I2SICFW7TILMU2ERRSPV3GQHNDQEUL3GGPHEYWQ3NVQA5EAKVP6NOYQIEJHDWX76IAHRC4H7T5PNATTCYDJH4TJ5WUXCI3A5PYCGCYKV5ZL3IRF6TDAP4LGVUNT7X5IPYUD4LC5G5SH2DULRDITMR2DTKBIGXQXMSOQY7XMIL7643CIEJXX5DIWREMX5SSCAR7GTWNAXLW2XGNPLQPW3UOP4UXQEGB4CDQL4MZ7FLNICT34VGVVEUMM43OITZJUECDQ7IKDIL7GU3GQXUIYTKZM6XMOBERAPWVB2UWIWD3KUTNCPBUQ7RESKCLVYRGUI4Z3ULFYSNM23VETFZGBELD7VFP7N5SRZ3GIS2SKM4376WUWWRZ4T7IO35UVCXNBTKVZHWTKZSYQABQE72N74FPJHY4DOFGUGIL2XCAVZU3NHE57HTSA7OGUWNF7DBE63DSAY4N6OGHMGL4SBFGSLYEBW7HERFLABVNQYLELH2X7VNLYM24WN52CWDZPFSA3YXDAQYOCCQNXIEWSTKNMPBVLXYJNX4SDW5KYPNKOSYJRHYRYB23LRXC7F7SVC7PMCBPMR', + 'B$ZP0401R2NIJC6YGI5PXFPPSXIZIJ6F5MPABQ7JTUK3MABHDBG7WWGCED33GILBH3IMEZENRW77JOMKNVVTZNDY4XGRE5PH7EQFYQ4WTBWIJBVLV7CVL453J47I4EHIPX6D2QRWWCQDPWWCFPSRT5R27HHKS22TMGPIZZMDYLEWXUTPMXHAM7LRETD4ANY5OS4EDCC5XTTO6GTTLYWS6XZT3B2OLEEOWR4UDQSJMATFPSLF7G4WGTHBAJE33CR5U3LN4LKUZQ2VPU3KF3TQUCUFAQPUCOJ6F5TFS3CEDD2D4PRDMEXXYE3FUPKTQME6VJAXRXJRS7JR2YIK7VXXD4K2O6RWAAKXOMNRLSBAWCSXGIIUAXIMESFD7XHCUNPRVXXJZZVM5IAV56NAIWWYGDHUYKQPN4ZZBGJRPB5KS6CYJGQQSSYSECQTRC43ET6BGOECAFU7TDOIJ2AOXR54PO7SPNYBRIZ2MGRS2A6VBDKMJCJ5JXFBUACAQW6TXYH5AHXT4A7S4JIE27BYDNZ4RKFLVYXA3T46WXGXQOHHNUNHPXJHEG2HPKLEVQR2I2SICFW7TILMU2ERRSPV3GQHNDQEUL3GGPHEYWQ3NVQA5EAKVP6NOYQIEJHDWX76IAHRC4H7T5PNATTCYDJH4TJ5WUXCI3A5PYCGCYKV5ZL3IRF6TDAP4LGVUNT7X5IPYUD4LC5G5SH2DULRDITMR2DTKBIGXQXMSOQY7XMIL7643CIEJXX5DIWREMX5SSCAR7GTWNAXLW2XGNPLQPW3UOP4UXQEGB4CDQL4MZ7FLNICT34VGVVEUMM43OITZJUECDQ7IKDIL7GU3GQXUIYTKZM6XMOBERAPWVB2UWIWD3KUTNCPBUQ7RESKCLVYRGUI4Z3ULFYSNM23VETFZGBELD7VFP7N5SRZ3GIS2SKM4376WUWWRZ4T7IO35UVCXNBTKVZHWTKZSYQABQE72N74FPJHY4DOFGUGIL2XCAVZU3NHE57HTSA7OGUWNF7DBE63DSAY4N6OGHMGL4SBFGSLYEBW7HERFLABVNQYLELH2X7VNLYM24WN52CWDZPFSA3YXDAQYOCCQNXIEWSTKNMPBVLXYJNX4SDW5KYPNKOSYJRHYRYB23LRXC7F7SVC7PMCBPMR', + 'B$ZP0402CZV73DGFLBG3EGYTGIKV35X6GU4QSR7BE43JT5KS7C4DSBBW3CTBEH674AVXGNWAF65UGU65RJALJ5PUVX2KNTYJ6MXTTVFFIZT3FCJZRAG4LXJ3A52IYP7UFTRI2K5FGFJNVOYLK4NZC6ZZAZWMVQEJT7JAMXEN3ENQB6VM4L7N7WMFA35IPRMF6RH4ZALTUHBZI7U4GAN3CK4PQJZMWQTO5F5PZ7EV5R6QKCR27AMNN4H2GTXGJR2YM5CXOLMBSDZKHABOYMD46G2TWREHUOSNGQY3BJ6EBENF5DOF5VOXJF5Z7LN4LJRZR3MLNG4OPOMGNNIPANZB7JJJQ3OUVIU3R6FI2SD4HUINHY2ZOQ4F7ZDWIG6S7PHO6IZZNM3C7KAMAN2TW6CTLY44TAZ2DOQTXHTKOE6UEVHE7JVMJJ5PVTWBLWIRD6SCDXYPIXOXZGTVNGKGPY24F7L4OIFGLJIYF4ZGDFNFAEAFJWF3APPL777XEWHYEKVCAGERBXPKG7ZUYBTRLJE5BXOPHSYDGZ5QMBUJYJVWPB2G4V3I25AJUFAU7NTLHCJAZF4O2P5REH7MEBZVEMLCJX5H52HNRU5EXR7HP5VJORFCHQ62VE3ITYFFH4JESQS5OAJVCHOP5CZOETDGW5JE3OCIEBVD7RSDD3SSLJJVGWHQ2V34EW36JB3OVMHWVHJMEYT4I4A5NVY3RPS7ZKRPXIA5PTVH2Y7JXIQ6WO5AG7JGFLM7WEMKWSLWMNBKNIWL327SMJ7BFD6MIJVTLYFB6N4HWD4WYH62N5P426HB7JWG2KXR6EG34MTHAP6LEOZU2WWQQRC3J7PUU372TQYP7ESDLVVHJZVH54GYVTJUCER3SHQ3ABBYEUHLQXY26SHTYG42LIBQWHC4S473LH6JNLYUEUWH3OC6YY5AVGMJ47OHOXJ4H2ZVFMBHNVPSSKHG34DDHIM2G42OX4K5RYENJ4QPTWFJ2PWKK6ZIOUBVNTRE5F6ZZEDA3HUKK4CARMT3R73TBY73N6ESQBTU6BUZJOUGR367NVNNRZEPHNF5NQYGFFWKPPRMP4RQDCW3M4LBZKPTOJHM56KSX6B5GWYFJD7ETR3SFEANFZLMI6KVAPJZMSRUXHLQUQ2G5UWSSGRI', + 'B$ZP0402CZV73DGFLBG3EGYTGIKV35X6GU4QSR7BE43JT5KS7C4DSBBW3CTBEH674AVXGNWAF65UGU65RJALJ5PUVX2KNTYJ6MXTTVFFIZT3FCJZRAG4LXJ3A52IYP7UFTRI2K5FGFJNVOYLK4NZC6ZZAZWMVQEJT7JAMXEN3ENQB6VM4L7N7WMFA35IPRMF6RH4ZALTUHBZI7U4GAN3CK4PQJZMWQTO5F5PZ7EV5R6QKCR27AMNN4H2GTXGJR2YM5CXOLMBSDZKHABOYMD46G2TWREHUOSNGQY3BJ6EBENF5DOF5VOXJF5Z7LN4LJRZR3MLNG4OPOMGNNIPANZB7JJJQ3OUVIU3R6FI2SD4HUINHY2ZOQ4F7ZDWIG6S7PHO6IZZNM3C7KAMAN2TW6CTLY44TAZ2DOQTXHTKOE6UEVHE7JVMJJ5PVTWBLWIRD6SCDXYPIXOXZGTVNGKGPY24F7L4OIFGLJIYF4ZGDFNFAEAFJWF3APPL777XEWHYEKVCAGERBXPKG7ZUYBTRLJE5BXOPHSYDGZ5QMBUJYJVWPB2G4V3I25AJUFAU7NTLHCJAZF4O2P5REH7MEBZVEMLCJX5H52HNRU5EXR7HP5VJORFCHQ62VE3ITYFFH4JESQS5OAJVCHOP5CZOETDGW5JE3OCIEBVD7RSDD3SSLJJVGWHQ2V34EW36JB3OVMHWVHJMEYT4I4A5NVY3RPS7ZKRPXIA5PTVH2Y7JXIQ6WO5AG7JGFLM7WEMKWSLWMNBKNIWL327SMJ7BFD6MIJVTLYFB6N4HWD4WYH62N5P426HB7JWG2KXR6EG34MTHAP6LEOZU2WWQQRC3J7PUU372TQYP7ESDLVVHJZVH54GYVTJUCER3SHQ3ABBYEUHLQXY26SHTYG42LIBQWHC4S473LH6JNLYUEUWH3OC6YY5AVGMJ47OHOXJ4H2ZVFMBHNVPSSKHG34DDHIM2G42OX4K5RYENJ4QPTWFJ2PWKK6ZIOUBVNTRE5F6ZZEDA3HUKK4CARMT3R73TBY73N6ESQBTU6BUZJOUGR367NVNNRZEPHNF5NQYGFFWKPPRMP4RQDCW3M4LBZKPTOJHM56KSX6B5GWYFJD7ETR3SFEANFZLMI6KVAPJZMSRUXHLQUQ2G5UWSSGRI', + 'B$ZP0403GGGKSI5N46HND3DI3RFTMMIH6HI6EHTRJWJDZCBB3NGI63AFRYCSF5WNM5YYF3D2TV7W5USN7RXMS7KEOABUA7273S7SDTB7ZXXANY2CQUU72OCVN4SC47CDOY7ZXOHSFCULALBE2FNHTHDDLX3PYAJ25W53KKHOEWFMUMYDHNZODN3H44P5ZPAXZC7H3SL77GCXENXYTB4WB4HWDFBLXZE65ZPRAAQJVBUQPRQPWG2TZSVR3T426MYPK7OSYTFYY2HKLHA46CX2ZMG2EHU3HBHTWZAK4PVIS636I5OBTJUTVP3POGUK73T5DAKW2GH2KV4NLZIE6X4LT4SUJVCT2W4DGFGV5UTPR6EPOM5MF43L5VJT62QNPIDKN4P3QFND6JTPB6ZPLPGWDJUJDBEQ2AFAYLPB34H6AD4LXE2HIHWWN46Z3LABXII53DM3JBGBDNC6366NSSKW62DAZWPSN32LDOAESPZYOMPQ3RKGE2WLFVTHQONBAC5Z3G6XYKA3WGTUS6L5G35VIOVFSFQ62VE3ITYNEH4JESQS5OEJVCHGO5CZOETLGW5JEZOCIEFVD7RSDDZ24KNCI6IQIO3JSHWZBIOAWRHMTPH6EBGZ6U5P7XFETP4N3EX3RBQMGB7XXPMQ4REQMS6PNHY4ED7OEILCS3FHXYWH6IYBRLNWOFQ4VHZXETWO7FJL7A6TLMCUR7SJY5ZCODZ7P65B7R6TLY4F5N33YWAR45KQ2TC3FLI6IXRLBQM3LN5ENCQMIMFGR22J4O2GWORXCL5B3E7PK7VROTOZB5OYQD7QA' + ] + + d = DecodeQR() + for i in parts: + if d.add_data(i) == DecodeQRStatus.COMPLETE: + break + assert d.qr_type == QRType.PSBT__BBQR + + #complete should be true + assert d.is_complete + + + +def test_bbqr_psbt_p2wsh_qr(): + base64_psbt = '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' + + parts = [ + 'B$ZP05044YHEDDPOKBPHMMY3URG6GYXLQ5ZR5A4CLN3UXSKVLAYAQQIQ4BTBKU2LUWOKPXE663ZRWTRXGZTDOF3RY4CQINUEMTJJVM3G5OKCCIF5GRB7O2TPT7OF5W7VQTHDDQ4X5M4ZU67C5E255ZYWWD5NMI5IGIELQIRRP5F6R3PZI42R36XT6GO6HRJEFGNU4VIS62QTHHTLJL6JRVAVBBODWWT7EDAOK3RLLFUQNG27K5WFNNUJ3W42W7ODTTR6O3352YNGN5ZBODKI4S46OA56X445SLBKQCTHB4J57YRYJUKZFZXCSNJVVYOZIILKO7ZZIS5AVLGD6PAK6PJCLUB5NYLSD3Y2DMAOLF42I5OAHJSOLEMWAXVZB5L6USJGCHJS6NFQ5GNEDQU3Z7KTETRYWRRU57LSHHIP7R3R26RY2AZLTDYDZAYQLNYEN7IIDABMYPFNZ3FUGMBNN7MXWBWLFK5WLC2MVJO4B4NJ6XNU2O7C7SSN3IBQCFL2QTOTFJ526I7U4ZXDMQFAXUNCWINWPDDXSRPXPXP74ODKLQTKGICLJX5F6M2D2SZEL5GPWPSN4XAUMTQ4LNTRPX2H6TUOMGPMJXG3XAUDYAEN3NMDANWB5ZZUYTIYK7R3SWSAWSWENNZ3XSXRG6H3YV35SO5GSDIEE32PKZBGRYMGYTV6SVVUBOXSJIL4HU5BCW6GA7C2UPGKB3R3YXQPEERJADYRALFJIDGFFJIA4I2WASJN44I4XARFYXFCGVIJJOFJKR22E4WCSVQ6NFI3BJEYPHKH6AA', + 'B$ZP0503KUEKQJY5P37EIJDAYR75LV37UXPX2IJ7V3WD2GVR6NO44MLTGHNCRPZJBCARXNQ4PNBSVQSN7DOVL6CHAOBU3TRRNJPMRC4F5Y453TAW23M2G6AUAKHXBPND5FLHK7PA6LEEMNLWCKWSITNPTN6VK2EJN6WIYOKRHEOPOAQC2OPNVCRXHCGWUJJ4VBLYRBECPE2T27DY5WAGJJIGGOOL64XBBVU2MQ6EIGYA5O4W66AHIDKYQ7FXTRB2MEO3F4SIKXBDUZHFSGLAL24Q6V7KJETBDV3LCHCGVVNHCODUOIRFFFBKKFOYQPV25PW2SEBSXWAFUGU6RQVV6J36GNOV6Z677QPQ5QKYWOF75MLOB6PXQVITDTCQFMRN3IFAG6OTMKA4C4XXPFA7TFSRBDIBV7UGXU6VGNOMKB42TXZYVHZWUUKWW6UVRI3PRW5UP7FCOZJGMDAEHRHVT44G2T5KGNWCE5OOZPEJUOIXB7BM6Z5LKJO2OSBWFZHMS7UXYCAY4EYNKIZVO7R6QUCC33FTNJ6GB5CKPCYLNRVVLN2XSWVFSVQEKZB2AC2YUK6PWUORONEWPHXITVISATY3YFTGXL7466V6XJZPAWF6VVG3INQDB5N5JQ2JWEPXA5RUM77LUMAPYVVRGGIVF3F2Q6QQML5G6464W7GPOSEHQFKYQ5RWZP7WRB2FCWEHVQ6FEWNQB2L7HCEPQR24RSRD2UEOXEDVP4SA5MAOTF42NQ3Z5YKYPHD3RBVYFJK5ZQGBPLG37HFJ56MOT4PDFMIMPTOVX4DJICHOELCL7XBSDSVB', + 'B$ZP0503KUEKQJY5P37EIJDAYR75LV37UXPX2IJ7V3WD2GVR6NO44MLTGHNCRPZJBCARXNQ4PNBSVQSN7DOVL6CHAOBU3TRRNJPMRC4F5Y453TAW23M2G6AUAKHXBPND5FLHK7PA6LEEMNLWCKWSITNPTN6VK2EJN6WIYOKRHEOPOAQC2OPNVCRXHCGWUJJ4VBLYRBECPE2T27DY5WAGJJIGGOOL64XBBVU2MQ6EIGYA5O4W66AHIDKYQ7FXTRB2MEO3F4SIKXBDUZHFSGLAL24Q6V7KJETBDV3LCHCGVVNHCODUOIRFFFBKKFOYQPV25PW2SEBSXWAFUGU6RQVV6J36GNOV6Z677QPQ5QKYWOF75MLOB6PXQVITDTCQFMRN3IFAG6OTMKA4C4XXPFA7TFSRBDIBV7UGXU6VGNOMKB42TXZYVHZWUUKWW6UVRI3PRW5UP7FCOZJGMDAEHRHVT44G2T5KGNWCE5OOZPEJUOIXB7BM6Z5LKJO2OSBWFZHMS7UXYCAY4EYNKIZVO7R6QUCC33FTNJ6GB5CKPCYLNRVVLN2XSWVFSVQEKZB2AC2YUK6PWUORONEWPHXITVISATY3YFTGXL7466V6XJZPAWF6VVG3INQDB5N5JQ2JWEPXA5RUM77LUMAPYVVRGGIVF3F2Q6QQML5G6464W7GPOSEHQFKYQ5RWZP7WRB2FCWEHVQ6FEWNQB2L7HCEPQR24RSRD2UEOXEDVP4SA5MAOTF42NQ3Z5YKYPHD3RBVYFJK5ZQGBPLG37HFJ56MOT4PDFMIMPTOVX4DJICHOELCL7XBSDSVB', + 'B$ZP05044YHEDDPOKBPHMMY3URG6GYXLQ5ZR5A4CLN3UXSKVLAYAQQIQ4BTBKU2LUWOKPXE663ZRWTRXGZTDOF3RY4CQINUEMTJJVM3G5OKCCIF5GRB7O2TPT7OF5W7VQTHDDQ4X5M4ZU67C5E255ZYWWD5NMI5IGIELQIRRP5F6R3PZI42R36XT6GO6HRJEFGNU4VIS62QTHHTLJL6JRVAVBBODWWT7EDAOK3RLLFUQNG27K5WFNNUJ3W42W7ODTTR6O3352YNGN5ZBODKI4S46OA56X445SLBKQCTHB4J57YRYJUKZFZXCSNJVVYOZIILKO7ZZIS5AVLGD6PAK6PJCLUB5NYLSD3Y2DMAOLF42I5OAHJSOLEMWAXVZB5L6USJGCHJS6NFQ5GNEDQU3Z7KTETRYWRRU57LSHHIP7R3R26RY2AZLTDYDZAYQLNYEN7IIDABMYPFNZ3FUGMBNN7MXWBWLFK5WLC2MVJO4B4NJ6XNU2O7C7SSN3IBQCFL2QTOTFJ526I7U4ZXDMQFAXUNCWINWPDDXSRPXPXP74ODKLQTKGICLJX5F6M2D2SZEL5GPWPSN4XAUMTQ4LNTRPX2H6TUOMGPMJXG3XAUDYAEN3NMDANWB5ZZUYTIYK7R3SWSAWSWENNZ3XSXRG6H3YV35SO5GSDIEE32PKZBGRYMGYTV6SVVUBOXSJIL4HU5BCW6GA7C2UPGKB3R3YXQPEERJADYRALFJIDGFFJIA4I2WASJN44I4XARFYXFCGVIJJOFJKR22E4WCSVQ6NFI3BJEYPHKH6AA', + 'B$ZP05044YHEDDPOKBPHMMY3URG6GYXLQ5ZR5A4CLN3UXSKVLAYAQQIQ4BTBKU2LUWOKPXE663ZRWTRXGZTDOF3RY4CQINUEMTJJVM3G5OKCCIF5GRB7O2TPT7OF5W7VQTHDDQ4X5M4ZU67C5E255ZYWWD5NMI5IGIELQIRRP5F6R3PZI42R36XT6GO6HRJEFGNU4VIS62QTHHTLJL6JRVAVBBODWWT7EDAOK3RLLFUQNG27K5WFNNUJ3W42W7ODTTR6O3352YNGN5ZBODKI4S46OA56X445SLBKQCTHB4J57YRYJUKZFZXCSNJVVYOZIILKO7ZZIS5AVLGD6PAK6PJCLUB5NYLSD3Y2DMAOLF42I5OAHJSOLEMWAXVZB5L6USJGCHJS6NFQ5GNEDQU3Z7KTETRYWRRU57LSHHIP7R3R26RY2AZLTDYDZAYQLNYEN7IIDABMYPFNZ3FUGMBNN7MXWBWLFK5WLC2MVJO4B4NJ6XNU2O7C7SSN3IBQCFL2QTOTFJ526I7U4ZXDMQFAXUNCWINWPDDXSRPXPXP74ODKLQTKGICLJX5F6M2D2SZEL5GPWPSN4XAUMTQ4LNTRPX2H6TUOMGPMJXG3XAUDYAEN3NMDANWB5ZZUYTIYK7R3SWSAWSWENNZ3XSXRG6H3YV35SO5GSDIEE32PKZBGRYMGYTV6SVVUBOXSJIL4HU5BCW6GA7C2UPGKB3R3YXQPEERJADYRALFJIDGFFJIA4I2WASJN44I4XARFYXFCGVIJJOFJKR22E4WCSVQ6NFI3BJEYPHKH6AA', + 'B$ZP0500QXJHSNCUP4PQP4EZPOGW3CDEZNGMZNKEFBJXNP6UYRCGJCLMMNBFTMUVKD3MVPRHEJNWQIMTFDETEUUWJJ7FJMRGEVMUHJKVVCEHSTHGHD2TZXPHZY4XHTX46F647Z73PV7V5XSHDWHTDUFIAUAA3ICQFDGOTIKJS5LW73YPVGTW3FQ7PFUWAMMVM3R5DWZMVPSRW5BCEZI4GKQQYX6CYMAYBRKLDLVB7TKYZUGV4RFA54VJJ5PX6DUMSKTWUEUCL2VRV5E7GQ4CXOLSSC4DDPPJNTT4NAE23YH6WA4F3WCDJPFW62AIOJ5YXE4X3V2RZZRMOVQOT2YFNYDTXZF6K45JBWR2RD45XHWUPJ6QG7L45MH56SJZEKHIYST2YHHM2H74ZVYJ7GU7LURR22FL3F7CZOVALHI4NRCHILTJWQ4PVDJL22WRYRBLNX7PKGZFRERDIHRE6LNOMBUQQ47SZBWLYRQFLLDFH5S3JEOL43GDFUN3GIYAWN27RDZ2ETGRNRY6LYTM42CE6E3BFAKFAU6LOM6I36XY7DQIKPTCST2VBZWMYBH3OWC7FGPGN7QTBXCXOMFOYDW2UE6XIIJ2MJUE6YPNP3DTMWRXPDAFNR5R3GZJ2DGX7WFW5VZ3NGXAKA4M5E4IIIW73WPSO6U6EMGWSBFBSWJO42IKDSGEEDKEFPR2T6L7QHVRYYPPHQNBD4O7SORVSST77PCQHBWJXRAA2RRZF6AK6XCIIJMFXO2FL6VPPB7FKGKIDRLY53EKBTVHZL56OD5JCYFUAIYRSZQIE2RLVDZZUTQOO5NX', + 'B$ZP0501MYQ75A2GHMJQMOZQJEBRSBKUMKHZCS2YMKT2NWKTBX7RCUDLMBXKBKXVLWBJBHMPPVDXXDJ6X7PLVVTPQ6DDWM2KU3FB432NJUITYOMYJOARTMR7AVR4SHGSQKKXP2ELCFUK4U72Y367ZZDUQB626E4H6RD5H3IPZDZM7HVFCYSACX6WCTCRW2J3Q2DFFQ7LBAY4ERZKLLKWI42JQ5X3CSY5SRLGRW36KCYFE6JIDVTIRQZH6ZG4YIM7BQU73ONOSCC6ZI4PF67OEZ3MXQC7P6I2OOW4KXV3PTAN3W3J6EAIBMULRT2GY3ARBE2ND4QT34P6RUQQ3HYUGT6HNFBQPZZ35ZYHHMYYIFIWHDSJTAQ7PISJ53GCCN54NKBLIJ6LYJEBMRIP4QYVC7OGCLEWMUZFTLD5W5O3PKHEQDRZQAMICDHWF6H36Z5IRHGE3ZHAOYMWYDABH7P5Q7NUROXEOC6P3J532RLQ3NHPDRP7D2BEFIYVUZLK3AB44L7LBTN75HZHUOSXJLVCVU54DVFCGEHH3EF6W6PGN3O5YMRJWWTBQDNSUGLU5IRR5ULBBMFO4QLE3GTTW3BU4LW6VHPOPVMZ7D6TNJDNMH3MIBQL6O2PDIRKLYEFIE5Y2D6XUXCEFWODB7HO2LRNXTMFEUTIJYGYEISYE77M6X2OQKIAAI27SKVMPU4HCI5NDLL6JTFDFFXC3OCZLMX2WI62HPR6JTWTIJG6EIOQJMRMPRWV5SYXZBKK7NPJ7K7CVKGMKIHFS4LUF3ZVBHMDFPDHZ43EBIHXM73T257LLY5ZWKR3', + 'B$ZP0502N6QQLR6S2WU3KVVIEEGC3427H6WUVXVEFQEEZ5X7M5CKWKE3I5I77YADUXLG3RFZNNSQK7VWUK43HONJQVRQNLSWK2SZF3EIZ7AQ7BPDHT72PP5FZ5D3NJPO3AIYPYFDCWEMT7ZHR72AZ7G7XD7ZMRZKDP7TPSN74WIYVRX7BXUW66NEKXRP72UPSVTTVNGCVQ4LI4IPKY533OR6W2P2MOTW4OIHAVXUUJCQQPOU73F3YUOKO4B2KZ5JP44BJLGGM6EXA24EPCO2CCZ5P64HTHA2DELNPHGRJXK6F2ON4MUY54EGACZNLATHDRHLIIX7K27AWJKKHOWTLRVMYPDTB6ENWVXVSZWUSTCH2CGI2NO4XGGUEZKNWRQFHCGMPSBLNZYB25VNTONJJGNA7XODH45B7MVQJSVLZI2JW3DBUQ36S6ZWGVVQV4BFE4UOCL63G3NFIHNYNV7NQR7EZOJPN4O4C3HDAKYZ2X5BFORVJXYRUVNEPN7NNWQQZSYTU5BIEV2Q4C4B2TI5FUHSAYXDSR2XHXO5S374ZLESDD6YWM6HKLQB34FHQLLS2XCH4OKEPIHKZQ2BQC4QPRGHYI5GJZMRVKAHLOE4I6WADVRBFOHVINFMINLB5KLELCD4YPGT4GTNKKYO4P3K4UR7S2CNFSXSOBAE7SWI34MTRHVXTAMCDNZIHIOGMX3QUZT64D4HAT7STN2GCVZARI6M2JIMVY24AR35IVXKUTQ2JU4AAX7735YCPE5BBMDDWBQJYS3NM22UBKMR5IQTS42KNKKAIN5F5OFIDBV5CFDP3JHF', + ] + + d = DecodeQR() + for i in parts: + if d.add_data(i) == DecodeQRStatus.COMPLETE: + break + assert d.qr_type == QRType.PSBT__BBQR + + #complete should be true + assert d.is_complete + + tx = d.get_psbt() + + assert str(tx) == base64_psbt + + assert tx.inputs[0].witness_utxo.value == 3826 # input 1 amount in psbt + assert tx.inputs[1].witness_utxo.value == 1366 # input 2 amount in psbt + assert tx.inputs[2].witness_utxo.value == 4398 # input 3 amount in psbt + assert tx.inputs[3].witness_utxo.value == 3716 # input 4 amount in psbt + assert tx.inputs[4].witness_utxo.value == 2811 # input 5 amount in psbt + + assert len(tx.outputs) == 1 + + From df36c1a26a34f9b6e74224d5a6d948663ee2cd2b Mon Sep 17 00:00:00 2001 From: fedebuyito <126606287+fedebuyito@users.noreply.github.com> Date: Thu, 26 Dec 2024 11:41:12 +0000 Subject: [PATCH 03/60] Update encode_qr.py Some helpers comments. --- src/seedsigner/models/encode_qr.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index c66e8567..37aeb788 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -344,6 +344,9 @@ class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder): return Keypath(arr, self.root.my_fingerprint, len(arr)) origin = derivation_to_keypath(self.derivation) + + # Implemts "use_info" member on HDKey class (urtypes/crypto packages-libs folder) construct, + # so if working on TESTNET, Xpub can be exported accordingly. Default case, MAINNET: None value. self.use_info = None if self.network == SettingsConstants.MAINNET else CoinInfo(type=None, network=1) self.ur_hdkey = HDKey({ 'key': self.xpub.key.serialize(), From 9d0b70cda88b4d5f0b305c6c9a96f641def09cdb Mon Sep 17 00:00:00 2001 From: fedebuyito <126606287+fedebuyito@users.noreply.github.com> Date: Sun, 23 Feb 2025 00:11:22 +0000 Subject: [PATCH 04/60] Update seed_views.py Cleanup the else condition so that the return to SeedTranscribeSeedQRConfirmInvalidQRView occurs on all remaining possibilities (any QR that is not SeedQR). --- src/seedsigner/views/seed_views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 6ce7e163..bd41077e 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -1661,9 +1661,8 @@ class SeedTranscribeSeedQRConfirmScanView(View): else: return Destination(SeedTranscribeSeedQRConfirmSuccessView, view_args={"seed_num": self.seed_num}) - else: - # Will this case ever happen? Will trigger if a different kind of QR code is scanned - return Destination(SeedTranscribeSeedQRConfirmInvalidQRView, skip_current_view=True) + # Will trigger if a different kind of QR code is scanned (non SeedQR) + return Destination(SeedTranscribeSeedQRConfirmInvalidQRView, skip_current_view=True) From 7f5b045707af6af3824208497efd986fc581fb6c Mon Sep 17 00:00:00 2001 From: fedebuyito <126606287+fedebuyito@users.noreply.github.com> Date: Sun, 23 Feb 2025 00:24:39 +0000 Subject: [PATCH 05/60] Update test_flows_seed.py Includes additional failure condition (when QR is recognized but is not a SeedQR) in flow test, directed toward SeedTranscribeSeedQRConfirmInvalidQRView --- tests/test_flows_seed.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_flows_seed.py b/tests/test_flows_seed.py index c1bcc0bb..a9a7f663 100644 --- a/tests/test_flows_seed.py +++ b/tests/test_flows_seed.py @@ -435,6 +435,9 @@ class TestSeedFlows(FlowTest): def load_completely_wrong_qr_type_into_decoder(view: View): view.decoder.add_data("I like cheese") + def load_recognized_qr_type_isnot_seed_into_decoder(view: View): + view.decoder.add_data("UR:CRYPTO-PSBT/HKADHEJOJKIDJYZMADAEGMAOAEAEAEADHHGLEYKGBDSBKEMTSARLVYSGIABEDELFSKWLLUDKLYESJOFDLPJPFXSOHHWZTYWEAEAEAEAEAEZCZMZMZMADBKBZJEDEHEAEAEAECMAEBBVEPFDWDMBBGSPFZTKIMYCLKPSOBDDTRDWTWPYKGAQDKNAEAEGWADAAECLTTKAXWSWTUTSNLAAEAEAEPKCTBSHGISSSRETIVEWSGYNEPTHTESNSWMLBTARYEMHTBTBTLNWSBKJLMKYNFGHLAXJYBBTTIDHKDICHAMLEHHDSRKATGDLYSBIYHNHDNEWPJKZSZMDKVETYNSGOCXKNDNBEOLFSRSNSGHAEAELAADAEAELAAEAEAELAAEADADCTKSBZJEDEHEAEAEAECMAEBBVEPFDWDMBBGSPFZTKIMYCLKPSOBDDTRDWTWPYKGAADAXAAADAEAEAECPAMAODSFTLDTYFRECSKVWYLKNBANNKKZCRTHYJTPSHLHNHKNBPDCLCFDMSOLPDKFXLFQZCSOLFSRSNSGHAEAELAADAEAELAAEAEAELAAEAEAEAEAEAEAEAEAECPAOAODSFTLDTYFRECSKVWYLKNBANNKKZCRTHYJTPSHLHNHKNBPDCLCFDMSOLPDKFXLFQZCSOLFSRSNSGHAEAELAADAEAELAAEAEAELAAEAEAEAEAEAEAEAEAELGMKFZCW") + def load_right_seed_into_decoder(view: View): view.decoder.add_data("0000" * 11 + "0003") @@ -460,6 +463,12 @@ class TestSeedFlows(FlowTest): FlowStep(seed_views.SeedTranscribeSeedQRConfirmInvalidQRView), FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView, is_redirect=True), # Live interactive screens are still weird + # Intentionally scan QR data that makes no sense for this flow because is another QR recognized but is not a SeedQR (e.g., bitcoin address, psbt) + FlowStep(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, button_data_selection=seed_views.SeedTranscribeSeedQRConfirmQRPromptView.SCAN), + FlowStep(seed_views.SeedTranscribeSeedQRConfirmScanView, before_run=load_recognized_qr_type_isnot_seed_into_decoder), + FlowStep(seed_views.SeedTranscribeSeedQRConfirmInvalidQRView), + FlowStep(seed_views.SeedTranscribeSeedQRZoomedInView, is_redirect=True), # Live interactive screens are still weird + # Now scan the correct SeedQR FlowStep(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, button_data_selection=seed_views.SeedTranscribeSeedQRConfirmQRPromptView.SCAN), FlowStep(seed_views.SeedTranscribeSeedQRConfirmScanView, before_run=load_right_seed_into_decoder), From a601721560a62779bb700af68e1fcbfe95337298 Mon Sep 17 00:00:00 2001 From: PROWLERx15 Date: Tue, 1 Apr 2025 18:25:03 +0530 Subject: [PATCH 06/60] Modify CI to run on all commits --- .github/workflows/tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9be339c0..6555914a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,8 +3,7 @@ name: CI on: push: branches: - - dev - - main + - '**' pull_request: concurrency: From 00bcb0fb9bd98cdb7ff14e69727e3d08fb11d8a1 Mon Sep 17 00:00:00 2001 From: Krishnav Bajoria <127018567+krishnavbajoria02@users.noreply.github.com> Date: Tue, 1 Apr 2025 18:33:54 +0530 Subject: [PATCH 07/60] Docs: Clarify that SeedQR uses English BIP39 wordlist --- docs/seed_qr/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/seed_qr/README.md b/docs/seed_qr/README.md index 40beaf8d..85b41ba6 100644 --- a/docs/seed_qr/README.md +++ b/docs/seed_qr/README.md @@ -22,6 +22,8 @@ Specifications for each follow below, as well as discussion of the pros and cons ## Quick Review of BIP-39 Mnemonic Seed Phrases The typical method for backing up a Bitcoin wallet is to store its [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic seed phrase consisting of 12 or 24 words. +**SeedQR specifically assumes and recommends using the English BIP39 wordlist.** While BIP39 supports multiple language-specific wordlists (e.g., Japanese, Spanish, French), there is no mechanism to specify or detect which language wordlist was used when creating or scanning a SeedQR. Using a non-English wordlist may result in incorrect or invalid seeds. + Each word comes from a [list of 2048 words](https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt). The words themselves are meaningless; all that matters is the word's position number (aka index) in the word list. For example, "tomato" is the 1,825th word in the list. From ebcdeadf352cc896c86bcb3c891762c068b84d5f Mon Sep 17 00:00:00 2001 From: Chaitanya-Keyal Date: Wed, 2 Apr 2025 20:31:37 +0000 Subject: [PATCH 08/60] docs: add note about `--ignore-missing` flag issue on older macOS versions --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e483bc41..e7548eee 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,7 @@ Now that you have confirmed that you do have the real SeedSigner Project's Publi ``` shasum -a 256 --ignore-missing --check seedsigner.0.8.5.sha256.txt ``` +Note: macOS versions earlier than v11 (Big Sur) do not support the `--ignore-missing` flag. You can omit it and disregard any missing file warnings. **On Windows (inside Powershell):** Run this command ``` From 27e55a8ee54a656ce3dfc6ce2c4dd488490477f2 Mon Sep 17 00:00:00 2001 From: PROWLERx15 Date: Fri, 4 Apr 2025 13:40:28 +0530 Subject: [PATCH 09/60] removed branches block to make it cleaner --- .github/workflows/tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6555914a..c1e14c97 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,8 +2,6 @@ name: CI on: push: - branches: - - '**' pull_request: concurrency: From c3f60ae188df8ab7b5674d021a09a430a1674ff7 Mon Sep 17 00:00:00 2001 From: Krishnav Bajoria <127018567+krishnavbajoria02@users.noreply.github.com> Date: Sat, 5 Apr 2025 15:22:09 +0530 Subject: [PATCH 10/60] Added 'calculating' spinner --- src/seedsigner/views/tools_views.py | 85 ++++++++++++++++------------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index a6828fe7..bd1eed47 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -127,54 +127,65 @@ class ToolsImageEntropyMnemonicLengthView(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - + mnemonic_length = button_data[selected_menu_num].return_data - preview_images = self.controller.image_entropy_preview_frames - seed_entropy_image = self.controller.image_entropy_final_image + # The entropy calculation can take time, especially with a full image buffer. + # Show a loading spinner to provide feedback during this delay. + from seedsigner.gui.screens.screen import LoadingScreenThread + self.loading_screen = LoadingScreenThread(text=_("Calculating...")) + self.loading_screen.start() - # Build in some hardware-level uniqueness via CPU unique Serial num try: - stream = os.popen("cat /proc/cpuinfo | grep Serial") - output = stream.read() - serial_num = output.split(":")[-1].strip().encode('utf-8') - serial_hash = hashlib.sha256(serial_num) - hash_bytes = serial_hash.digest() - except Exception as e: - logger.info(repr(e), exc_info=True) - hash_bytes = b'0' + preview_images = self.controller.image_entropy_preview_frames + seed_entropy_image = self.controller.image_entropy_final_image - # Build in modest entropy via millis since power on - millis_hash = hashlib.sha256(hash_bytes + str(time.time()).encode('utf-8')) - hash_bytes = millis_hash.digest() + # Build in some hardware-level uniqueness via CPU unique Serial num + try: + stream = os.popen("cat /proc/cpuinfo | grep Serial") + output = stream.read() + serial_num = output.split(":")[-1].strip().encode('utf-8') + serial_hash = hashlib.sha256(serial_num) + hash_bytes = serial_hash.digest() + except Exception as e: + logger.info(repr(e), exc_info=True) + hash_bytes = b'0' - # Build in better entropy by chaining the preview frames - for frame in preview_images: - img_hash = hashlib.sha256(hash_bytes + frame.tobytes()) - hash_bytes = img_hash.digest() + # Build in modest entropy via millis since power on + millis_hash = hashlib.sha256(hash_bytes + str(time.time()).encode('utf-8')) + hash_bytes = millis_hash.digest() - # Finally build in our headline entropy via the new full-res image - final_hash = hashlib.sha256(hash_bytes + seed_entropy_image.tobytes()).digest() + # Build in better entropy by chaining the preview frames + for frame in preview_images: + img_hash = hashlib.sha256(hash_bytes + frame.tobytes()) + hash_bytes = img_hash.digest() - if mnemonic_length == 12: - # 12-word mnemonic only uses the first 128 bits / 16 bytes of entropy - final_hash = final_hash[:16] + # Finally build in our headline entropy via the new full-res image + final_hash = hashlib.sha256(hash_bytes + seed_entropy_image.tobytes()).digest() - # Generate the mnemonic - mnemonic = mnemonic_generation.generate_mnemonic_from_bytes(final_hash) + if mnemonic_length == 12: + # 12-word mnemonic only uses the first 128 bits / 16 bytes of entropy + final_hash = final_hash[:16] - # Image should never get saved nor stick around in memory - seed_entropy_image = None - preview_images = None - final_hash = None - hash_bytes = None - self.controller.image_entropy_preview_frames = None - self.controller.image_entropy_final_image = None + # Generate the mnemonic + mnemonic = mnemonic_generation.generate_mnemonic_from_bytes(final_hash) + + # Image should never get saved nor stick around in memory + seed_entropy_image = None + preview_images = None + final_hash = None + hash_bytes = None + self.controller.image_entropy_preview_frames = None + self.controller.image_entropy_final_image = None + + # Add the mnemonic as an in-memory Seed + seed = Seed(mnemonic, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + self.controller.storage.set_pending_seed(seed) + + finally: + # Stop spinner even if an error occurs + self.loading_screen.stop() - # Add the mnemonic as an in-memory Seed - seed = Seed(mnemonic, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) - self.controller.storage.set_pending_seed(seed) - # Cannot return BACK to this View return Destination(SeedWordsWarningView, view_args={"seed_num": None}, clear_history=True) From f54003c915ec58a3cc18ee94c9235658735c2f3c Mon Sep 17 00:00:00 2001 From: Advait Gaur Date: Fri, 11 Apr 2025 22:53:04 +0530 Subject: [PATCH 11/60] fix: Update capitalization to Title Case for titles and Sentence case for buttons (#648) --- src/seedsigner/views/seed_views.py | 4 ++-- src/seedsigner/views/tools_views.py | 8 ++++---- src/seedsigner/views/view.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 6ce7e163..157ba1ea 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -268,7 +268,7 @@ class SeedMnemonicEntryView(View): class SeedMnemonicInvalidView(View): - EDIT = ButtonOption("Review & Edit") + EDIT = ButtonOption("Review & edit") DISCARD = ButtonOption("Discard", button_label_color="red") def __init__(self): @@ -454,7 +454,7 @@ class SeedReviewPassphraseView(View): class SeedDiscardView(View): - KEEP = ButtonOption("Keep Seed") + KEEP = ButtonOption("Keep seed") DISCARD = ButtonOption("Discard", button_label_color="red") def __init__(self, seed_num: int = None): diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index a6828fe7..464a2d5c 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -23,8 +23,8 @@ class ToolsMenuView(View): IMAGE = ButtonOption("New seed", FontAwesomeIconConstants.CAMERA) DICE = ButtonOption("New seed", FontAwesomeIconConstants.DICE) KEYBOARD = ButtonOption("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD) - ADDRESS_EXPLORER = ButtonOption("Address Explorer") - VERIFY_ADDRESS = ButtonOption("Verify Address") + ADDRESS_EXPLORER = ButtonOption("Address explorer") + VERIFY_ADDRESS = ButtonOption("Verify address") def run(self): button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.ADDRESS_EXPLORER, self.VERIFY_ADDRESS] @@ -516,10 +516,10 @@ class ToolsAddressExplorerSelectSourceView(View): class ToolsAddressExplorerAddressTypeView(View): # TRANSLATOR_NOTE: label for addresses where others send us incoming payments - RECEIVE = ButtonOption("Receive Addresses") + RECEIVE = ButtonOption("Receive addresses") # TRANSLATOR_NOTE: label for addresses that collect the change from our own outgoing payments - CHANGE = ButtonOption("Change Addresses") + CHANGE = ButtonOption("Change addresses") def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None): diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 66c3a229..bfebfa83 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -366,7 +366,7 @@ class UnhandledExceptionView(View): @dataclass class OptionDisabledView(View): - UPDATE_SETTING = ButtonOption("Update Setting") + UPDATE_SETTING = ButtonOption("Update setting") DONE = ButtonOption("Done") settings_attr: str From 12fdd2d9535caf857593ae70f5484308e0a41054 Mon Sep 17 00:00:00 2001 From: Advait Gaur Date: Sat, 12 Apr 2025 01:38:31 +0530 Subject: [PATCH 12/60] fix: Update UI to Sentence case for buttons. - Updated buttons to Sentence case, capitalizing only the first word. - Preserved proper nouns (SeedQR, BIP-39 Passphrase, Native Segwit, Nested Segwit, Taproot). - Ensured Xpub and Custom derivation are not always capitalized. --- src/seedsigner/gui/screens/psbt_screens.py | 4 +-- src/seedsigner/gui/screens/screen.py | 2 +- src/seedsigner/gui/screens/seed_screens.py | 4 +-- src/seedsigner/views/psbt_views.py | 10 +++---- src/seedsigner/views/seed_views.py | 32 +++++++++++----------- src/seedsigner/views/tools_views.py | 2 +- src/seedsigner/views/view.py | 4 +-- tests/test_flows.py | 4 +-- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index aa447ed7..21040000 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -31,7 +31,7 @@ class PSBTOverviewScreen(ButtonListScreen): # Customize defaults self.title = _("Review PSBT") self.is_bottom_list = True - self.button_data = [ButtonOption("Review Details")] + self.button_data = [ButtonOption("Review details")] # This screen can take a while to load while parsing the PSBT self.show_loading_screen = True @@ -479,7 +479,7 @@ class PSBTMathScreen(ButtonListScreen): def __post_init__(self): # Customize defaults self.title = _("PSBT Math") - self.button_data = [ButtonOption("Review Recipients")] + self.button_data = [ButtonOption("Review decipients")] self.is_bottom_list = True super().__post_init__() diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index 47f6645e..63355ba9 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1020,7 +1020,7 @@ class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen): status_icon_name: str = SeedSignerIconConstants.WARNING status_color: str = GUIConstants.WARNING_COLOR status_headline: str = _mft("Privacy Leak!") # The colored text under the alert icon - button_data: list = field(default_factory=lambda: [ButtonOption("I Understand")]) + button_data: list = field(default_factory=lambda: [ButtonOption("I understand")]) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 08d1966b..8a7976b0 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -605,7 +605,7 @@ class SeedExportXpubDetailsScreen(WarningEdgesMixin, ButtonListScreen): def __post_init__(self): # Programmatically set up other args - self.button_data = [ButtonOption("Export Xpub")] + self.button_data = [ButtonOption("Export xpub")] self.title = _("Xpub Details") # Initialize the base class @@ -1649,7 +1649,7 @@ class SeedSignMessageConfirmAddressScreen(ButtonListScreen): self.title = _("Confirm Address") self.is_bottom_list = True self.is_button_text_centered = True - self.button_data = [ButtonOption("Sign Message")] + self.button_data = [ButtonOption("Sign message")] super().__post_init__() derivation_path_display = IconTextLine( diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index b39c9367..57e0a4f3 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -12,7 +12,7 @@ class PSBTSelectSeedView(View): SCAN_SEED = ButtonOption("Scan a seed", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ButtonOption("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ButtonOption("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_ELECTRUM = ButtonOption("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ButtonOption("Enter electrum seed", FontAwesomeIconConstants.KEYBOARD) def run(self): @@ -265,7 +265,7 @@ class PSBTAddressDetailsView(View): button_data = [] if self.address_num < psbt_parser.num_destinations - 1: - button_data.append(ButtonOption("Next Recipient")) + button_data.append(ButtonOption("Next recipient")) else: # TRANSLATOR_NOTE: Short for "Next step" button_data.append(ButtonOption("Next")) @@ -300,8 +300,8 @@ class PSBTAddressDetailsView(View): class PSBTChangeDetailsView(View): NEXT = ButtonOption("Next") - SKIP_VERIFICATION = ButtonOption("Skip Verification") - VERIFY_MULTISIG = ButtonOption("Verify Multisig Change") + SKIP_VERIFICATION = ButtonOption("Skip verification") + VERIFY_MULTISIG = ButtonOption("Verify multisig change") def __init__(self, change_address_num): super().__init__() @@ -569,7 +569,7 @@ class PSBTSignedQRDisplayView(View): class PSBTSigningErrorView(View): - SELECT_DIFF_SEED = ButtonOption("Select Diff Seed") + SELECT_DIFF_SEED = ButtonOption("Select diff seed") def run(self): psbt_parser: PSBTParser = self.controller.psbt_parser diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 157ba1ea..526fc11c 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -74,7 +74,7 @@ class SeedSelectSeedView(View): SCAN_SEED = ButtonOption("Scan a seed", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ButtonOption("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ButtonOption("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_ELECTRUM = ButtonOption("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ButtonOption("Enter electrum seed", FontAwesomeIconConstants.KEYBOARD) def __init__(self, flow: str): @@ -162,7 +162,7 @@ class LoadSeedView(View): SEED_QR = ButtonOption("Scan a SeedQR", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ButtonOption("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) TYPE_24WORD = ButtonOption("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_ELECTRUM = ButtonOption("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ButtonOption("Enter electrum seed", FontAwesomeIconConstants.KEYBOARD) CREATE = ButtonOption("Create a seed", SeedSignerIconConstants.PLUS) def run(self): @@ -523,13 +523,13 @@ class SeedElectrumMnemonicStartView(View): ****************************************************************************""" class SeedOptionsView(View): SCAN_PSBT = ButtonOption("Scan PSBT", SeedSignerIconConstants.QRCODE) - VERIFY_ADDRESS = ButtonOption("Verify Addr") - EXPORT_XPUB = ButtonOption("Export Xpub") - EXPLORER = ButtonOption("Address Explorer") - SIGN_MESSAGE = ButtonOption("Sign Message") - BACKUP = ButtonOption("Backup Seed", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT) - BIP85_CHILD_SEED = ButtonOption("BIP-85 Child Seed") - DISCARD = ButtonOption("Discard Seed", button_label_color="red") + VERIFY_ADDRESS = ButtonOption("Verify addr") + EXPORT_XPUB = ButtonOption("Export xpub") + EXPLORER = ButtonOption("Address explorer") + SIGN_MESSAGE = ButtonOption("Sign message") + BACKUP = ButtonOption("Backup seed", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT) + BIP85_CHILD_SEED = ButtonOption("BIP-85 child seed") + DISCARD = ButtonOption("Discard seed", button_label_color="red") def __init__(self, seed_num: int): @@ -635,7 +635,7 @@ class SeedOptionsView(View): class SeedBackupView(View): - VIEW_WORDS = ButtonOption("View Seed Words") + VIEW_WORDS = ButtonOption("View seed words") EXPORT_SEEDQR = ButtonOption("Export as SeedQR") def __init__(self, seed_num): @@ -1210,7 +1210,7 @@ class SeedBIP85InvalidChildIndexView(View): status_icon_name=SeedSignerIconConstants.ERROR, status_headline=_("Invalid Child Index"), text=_("BIP-85 Child Index must be between 0 and 2^31-1."), - button_data=[ButtonOption("Try Again")] + button_data=[ButtonOption("Try again")] ).display() return Destination( @@ -1344,8 +1344,8 @@ class SeedWordsBackupTestView(View): class SeedWordsBackupTestMistakeView(View): - REVIEW = ButtonOption("Review Seed Words") - RETRY = ButtonOption("Try Again") + REVIEW = ButtonOption("Review seed words") + RETRY = ButtonOption("Try again") def __init__(self, seed_num: int, bip85_data: dict = None, cur_index: int = None, wrong_word: str = None, confirmed_list: list[bool] = None): super().__init__() @@ -2039,7 +2039,7 @@ class SeedAddressVerificationSuccessView(View): class LoadMultisigWalletDescriptorView(View): - SCAN = ButtonOption("Scan Descriptor", SeedSignerIconConstants.QRCODE) + SCAN = ButtonOption("Scan descriptor", SeedSignerIconConstants.QRCODE) CANCEL = ButtonOption("Cancel") def run(self): @@ -2065,8 +2065,8 @@ class LoadMultisigWalletDescriptorView(View): class MultisigWalletDescriptorView(View): RETURN = ButtonOption("Return to PSBT") - VERIFY_ADDR = ButtonOption("Verify Addr") - ADDRESS_EXPLORER = ButtonOption("Address Explorer") + VERIFY_ADDR = ButtonOption("Verify addr") + ADDRESS_EXPLORER = ButtonOption("Address explorer") OK = ButtonOption("OK") def run(self): diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 464a2d5c..1a1473c7 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -455,7 +455,7 @@ class ToolsAddressExplorerSelectSourceView(View): SCAN_DESCRIPTOR = ButtonOption("Scan wallet descriptor", SeedSignerIconConstants.QRCODE) TYPE_12WORD = ButtonOption("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD, return_data=12) TYPE_24WORD = ButtonOption("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD, return_data=24) - TYPE_ELECTRUM = ButtonOption("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = ButtonOption("Enter electrum seed", FontAwesomeIconConstants.KEYBOARD) def run(self): from seedsigner.controller import Controller diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index bfebfa83..15c32cbd 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -221,7 +221,7 @@ class MainMenuView(View): class PowerOptionsView(View): RESET = ButtonOption("Restart", SeedSignerIconConstants.RESTART) - POWER_OFF = ButtonOption("Power Off", SeedSignerIconConstants.POWER) + POWER_OFF = ButtonOption("Power off", SeedSignerIconConstants.POWER) def run(self): button_data = [self.RESET, self.POWER_OFF] @@ -291,7 +291,7 @@ class NotYetImplementedView(View): title=_("Work In Progress"), status_headline=_("Not Yet Implemented"), text=self.text, - button_data=[ButtonOption("Back to Main Menu")], + button_data=[ButtonOption("Back to main menu")], ) return Destination(MainMenuView) diff --git a/tests/test_flows.py b/tests/test_flows.py index 249f25be..2faafcfa 100644 --- a/tests/test_flows.py +++ b/tests/test_flows.py @@ -167,14 +167,14 @@ class TestFlowTest(FlowTest): def run(self): self.run_screen( ButtonListScreen, - button_data=[ButtonOption("this is fine"), "this is not"] + button_data=[ButtonOption("This is fine"), "This is not"] ) class MyGoodButtonDataTestView(View): def run(self): self.run_screen( ButtonListScreen, - button_data=[ButtonOption("this is fine"), ButtonOption("this is also fine")] + button_data=[ButtonOption("This is fine"), ButtonOption("This is also fine")] ) return Destination(MainMenuView) From 01a739afaa3bb811c685741b52af12735681bfa8 Mon Sep 17 00:00:00 2001 From: PROWLERx15 Date: Sun, 13 Apr 2025 18:24:20 +0530 Subject: [PATCH 13/60] update docs --- README.md | 50 ++++++++--------- docs/code_structure.md | 14 ++--- docs/debug_crash.md | 44 ++++++++------- docs/developer_tips.md | 35 ++++++------ docs/dice_verification.md | 37 ++++++------- docs/electrum.md | 8 +-- docs/feature_roadmap.md | 24 ++++----- docs/legacy_hardware.md | 10 ++-- docs/qr_formats.md | 16 +++--- docs/raspberry_pi_os_build_instructions.md | 53 +++++++++--------- docs/seed_qr/README.md | 12 ++--- docs/usb_relay.md | 62 +++++++++++----------- 12 files changed, 185 insertions(+), 180 deletions(-) diff --git a/README.md b/README.md index e483bc41..5c0329ed 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ If you have specific questions about the project, our [Telegram Group](https://t * Stateless, air-gapped operation: * Temporarily stores seeds in memory while the device is powered; all memory is wiped when power is removed. * SD card removable after boot to ensure no secret data can be written to it. - * No wifi or Bluetooth hardware onboard. + * No WiFi or Bluetooth hardware onboard. * Can only receive data via reading QR codes with its camera. * Can only send data by displaying QR codes on its screen. @@ -51,7 +51,7 @@ If you have specific questions about the project, our [Telegram Group](https://t * Import any existing seed phrase via an optimized seed word entry interface. * Partial support for Electrum Segwit seed phrases [(info)](docs/electrum.md). -* Wallet setup and transaction signing +* Wallet setup and transaction signing: * Script types: Taproot, native segwit, nested segwit, legacy (p2pkh). * Single sig and multisig xpub export. * Support for user-defined custom derivation paths. @@ -59,7 +59,7 @@ If you have specific questions about the project, our [Telegram Group](https://t * Verify the PSBT's single sig or multisig change outputs or self-transfer outputs. * Mainnet, testnet, and regtest. -* Additional utilities +* Additional utilities: * [SettingsQR](https://github.com/SeedSigner/seedsigner-settings-generator) to instantly reconfigure a SeedSigner for beginners, advanced users, or tailored to your preferences. * Scan a software wallet's receive or change address to verify that it's correct. * Address Explorer for single sig and multisig wallets. @@ -93,7 +93,7 @@ To build a SeedSigner, you will need: Notes: * You may need to solder the 40 GPIO pins (20 pins per row) to the Raspberry Pi Zero board. If you don't want to solder, most stores offer the board "with headers" already soldered on. -* The Pi Zero "W" or "2W" is often easier to find but has wifi/Bluetooth hardware. You can still use these boards and can optionally [disable the wifi/Bluetooth hardware](https://github.com/DesobedienteTecnologico/rpi_disable_wifi_and_bt_by_hardware). +* The Pi Zero "W" or "2W" is often easier to find but has WiFi/Bluetooth hardware. You can still use these boards and can optionally [disable the WiFi/Bluetooth hardware](https://github.com/DesobedienteTecnologico/rpi_disable_wifi_and_bt_by_hardware). * Other cameras with the above sensor module should work, but may not fit in the Orange Pill enclosure. * Choose the Waveshare screen carefully; they make a number of different boards that look very similar but ARE NOT COMPATIBLE! Make sure you purchase the model that has a resolution of 240x240 pixels. * Raspberry Pi 1 is also compatible, but will require a [hardware modification to the Waveshare LCD Hat](./docs/legacy_hardware.md). @@ -111,8 +111,8 @@ Instructions to build a SeedSigner OS image (using precisely the same process th ## Downloading the Software - -Download the current Version (0.8.5) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board. +Download the current Version (0.8.5) software image that is compatible with your Raspberry Pi Hardware. The Pi Zero 1.3 is the most common and recommended board. + | Board | Download Image Link/Name | | --------------------- | --------------------------------- | |**[Raspberry Pi Zero 1.3](https://www.raspberrypi.com/products/raspberry-pi-zero/)** |[`seedsigner_os.0.8.5.pi0.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi0.img) | @@ -124,9 +124,9 @@ Download the current Version (0.8.5) software image that is compatible with your |[Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) |[`seedsigner_os.0.8.5.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi4.img) | |[Raspberry Pi 400](https://www.raspberrypi.com/products/raspberry-pi-400-unit/) |[`seedsigner_os.0.8.5.pi4.img`](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner_os.0.8.5.pi4.img) | -Note: If you have physically removed the WiFi component from your board, you will still use the image file of the original(un-modified) hardware. (Our files are compiled/based on the *processor* architecture). Although it is better to spend a few minutes upfront to determine which specific Pi hardware/model you have, if you are still unsure which hardware you have, you can try using the pi0.img file. Making an incorrect choice here will not ruin your board, because this is software, not firmware. +Note: If you have physically removed the WiFi component from your board, you will still use the image file of the original (un-modified) hardware. (Our files are compiled/based on the *processor* architecture). Although it is better to spend a few minutes upfront to determine which specific Pi hardware/model you have, if you are still unsure which hardware you have, you can try using the pi0.img file. Making an incorrect choice here will not ruin your board, because this is software, not firmware. -**also download** these 2 signature verification files to the same folder +**Also download** these 2 signature verification files to the same folder: [The Plaintext manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner.0.8.5.sha256.txt) [The Signature of the manifest file](https://github.com/SeedSigner/seedsigner/releases/download/0.8.5/seedsigner.0.8.5.sha256.txt.sig) @@ -147,7 +147,7 @@ We assume you are running the commands from a computer where both [GPG](https:// ### Step 1. Verify that the signature (.sig) file is genuine: -Run GPG's *fetch-keys* command to import the SeedSigner projects public key from the popular online keyserver called *Keybase.io*, into your computers *keychain*. +Run GPG's *fetch-keys* command to import the SeedSigner project's public key from the popular online keyserver called *Keybase.io*, into your computer's *keychain*. ``` @@ -169,7 +169,7 @@ The result must display "**Good signature**". Ignore any email addresses - *onl
On the *last* output line, look at your *rightmost* 16 characters (the 4 blocks of 4). -**Crucially, we must now check WHO that Primary key fingerprint /ID belongs to.** We will start by looking at Keybase.io to see if it is the *SeedSigner project* 's public key or not. +**Crucially, we must now check WHO that Primary key fingerprint /ID belongs to.** We will start by looking at Keybase.io to see if it is the *SeedSigner project*'s public key or not.
About the warning message:

Since you are about to match the outputted fingerprint/ID against the proofs at Keybase.io/SeedSigner, and thereby confirm who the pubkey really belongs to-, you can safely ignore this warning message: @@ -184,16 +184,16 @@ On the *last* output line, look at your *rightmost* 16 characters (the 4 blocks

More about how the verify command works:

-The verify command will attempt to decrypt the signature file (sha256.sig) by trying each public key already imported into your computer. If the public key we just imported (via fetch-keys), manages to: (a) successfully decrypt the .sig file , and (b), that result matches exactly to the clear-text equivalent (.sha256) of the .sig file, then its "a good signature"! +The verify command will attempt to decrypt the signature file (sha256.sig) by trying each public key already imported into your computer. If the public key we just imported (via fetch-keys), manages to: (a) successfully decrypt the .sig file , and (b), that result matches exactly to the clear-text equivalent (.sha256) of the .sig file, then it's "a good signature"! -Crucially, we must still manually check who *exactly* owns the Key ID which gave us that "Good signature". Thats what the warning message means- Who does the matching key really belong to? We will start by looking at keybase.io to see if it is "The SeedSigner project"'s public Key or not. +Crucially, we must still manually check who *exactly* owns the Key ID which gave us that "Good signature". That's what the warning message means- Who does the matching key really belong to? We will start by looking at keybase.io to see if it is "The SeedSigner project"'s public Key or not. Note that it is the file hashes of .sig and .sha256 that *verify* compares, not their raw contents.


-Now to determine ***who*** the Public key ID belongs to: Goto [Keybase.io/SeedSigner](https://keybase.io/seedsigner) +Now to determine ***who*** the Public key ID belongs to: Go to [Keybase.io/SeedSigner](https://keybase.io/seedsigner)
![SS - Keybase Website PubKey visual matching1_Cropped-80pct](https://user-images.githubusercontent.com/91296549/215326193-97c84e35-5570-4e52-bf3f-e86d367c8908.jpg) @@ -207,13 +207,13 @@ Now to determine ***who*** the Public key ID belongs to: Goto [Keybase.io/SeedSi
Learn more about how keybase.io helps you check that someone (online) is who they say they are:

Keybase.io allows you to independently verify that the public key saved on Keybase.io, is both authentic and that it belongs to the organization it claims to represent. - Keybase has already checked the three pubkey file locations cryptographically when they were saved there. You can further verify the key publications if you would like: + Keybase has already checked the three pubkey file locations cryptographically when they were saved there. You can further verify the key publications if you would like: - *via Keybase*: By clicking on any of the three blue badges to see that the "proof" was published at that location. (The blue badge marked as tweet, is in the most human-readable form and it is also a bi-directional link on Twitter) or, - *without keybase (out-of-band)*: By using these 3 links directly: [Twitter](https://twitter.com/SeedSigner/status/1530555252373704707), [Github](https://gist.github.com/SeedSigner/5936fa1219b07e28a3672385b605b5d2) and [SeedSigner.com](https://seedsigner.com/keybase.txt). This method can be used if you would like to make an even deeper, independent inspection without relying on Keybase at all, or if the Keybase.io site is no longer valid or it is removed entirely. -Once you have used one of these methods, you will know if the Public Key stored on Keybase, is genuinely from the SeedSinger Project or not. +Once you have used one of these methods, you will know if the Public Key stored on Keybase, is genuinely from the SeedSigner Project or not.


@@ -224,8 +224,8 @@ If the two ID's do *not* match, then you must stop here immediately. Do not cont ### Step 2. Verifying that the *software images/binaries* are genuine -Now that you have confirmed that you do have the real SeedSigner Project's Public Key (ie the 16 characters match) - you can return to your terminal window. Running the *shasum* command, is the final verification step and will confirm (via file hashing) that the software code/image files, were also not altered since publication, or even during your download process. -(Prior to version 0.6.0 , your verify command will check the .zip file which contains the binary files.) +Now that you have confirmed that you do have the real SeedSigner Project's Public Key (ie the 16 characters match) - you can return to your terminal window. Running the *shasum* command, is the final verification step and will confirm (via file hashing) that the software code/image files, were also not altered since publication, or even during your download process. +(Prior to version 0.6.0, your verify command will check the .zip file which contains the binary files.) **On Linux or OSX:** Run this command ``` @@ -246,7 +246,7 @@ seedsigner_os.0.8.5.[Your_Pi_Model_For_Example:pi02w].img: OK **If you receive the "OK" message** for your **seedsigner_os.0.8.5.[Your_Pi_Model_For_Example:pi02w].img file**, as shown above, then your verification is fully complete! **All of your downloaded files have now been confirmed as both authentic and unaltered!** You can proceed to create/write your MicroSD card๐Ÿ˜„๐Ÿ˜„ !! -If your file result shows "FAILED", then you must stop here immediately. Do not continue. Contact us for assistance at the Telegram group address above. +If your file result shows "FAILED", then you must stop here immediately. Do not continue. Contact us for assistance at the Telegram group address above.
@@ -265,19 +265,19 @@ To write the SeedSigner software onto your MicroSD card, there are a few options | DD Command Line Utility | Built-in to Linux and MacOS, the DD (Data Duplicator) is a tool for advanced users. If not used carefully it can accidentally format the incorrect disk! | Built-in to Linux and MacOS | Be sure to download the software from the genuine publisher. -Either of the Etcher or Pi Imager software is recommended. Some SeedSigner users have reported a better experience with one or the other. So, if the one application doesnโ€™t work well for your particular machine, then please try the other one. +Either of the Etcher or Pi Imager software is recommended. Some SeedSigner users have reported a better experience with one or the other. So, if the one application doesn't work well for your particular machine, then please try the other one.
### **General Considerations:** The writing and verify steps are very quick from version 0.6.0 upwards, so please pay close attention to your screen. Make sure to set any write-protection physical slider on the MicroSD Card Adapter to UN-locked. -You also donโ€™t need to pre-format the MicroSD beforehand. You *dont* need to unzip any .zip file beforehand. -Current Etcher and Pi Imager software will perform a verify action (by default) to make sure the card was written successfully! Watching for that verify step to complete successfully, can save you a lot of headaches if you later need to troubleshoot issues where your SeedSigner device doesnโ€™t boot up at power on. -Writing the MicroSd card is also known as flashing. +You also don't need to pre-format the MicroSD beforehand. You *don't* need to unzip any .zip file beforehand. +Current Etcher and Pi Imager software will perform a verify action (by default) to make sure the card was written successfully! Watching for that verify step to complete successfully, can save you a lot of headaches if you later need to troubleshoot issues where your SeedSigner device doesn't boot up at power on. +Writing the MicroSD card is also known as flashing. It will overwrite everything on the MicroSD card. If the one application fails for you, then please try again using our other recommended application. -Advanced users may want to try the Linux/MacOS *DD* command instead of using Etcher or Pi Imager, however, a reminder is given that DD can overwrite the wrong disk if you are not careful ! +Advanced users may want to try the Linux/MacOS *DD* command instead of using Etcher or Pi Imager, however, a reminder is given that DD can overwrite the wrong disk if you are not careful! #### **Specific considerations for Windows users:** -Use the Pi imager software as your first choice on Windows. Windows can sometimes flag the writing of a MicroSD as risky behaviour and hence it may prevent this activity. If this happens, your writing/flashing will fail, hang or wont even begin, in which case you should to try to run the Etcher/Pi-Imager app "As administrator", (right-click and choose that option). It can also be blocked by windows security in some cases, so If you have the (non-default) *Controlled Folder Access* option set to active, try turning that *off* temporarily. +Use the Pi imager software as your first choice on Windows. Windows can sometimes flag the writing of a MicroSD as risky behaviour and hence it may prevent this activity. If this happens, your writing/flashing will fail, hang or won't even begin, in which case you should try to run the Etcher/Pi-Imager app "As administrator", (right-click and choose that option). It can also be blocked by windows security in some cases, so If you have the (non-default) *Controlled Folder Access* option set to active, try turning that *off* temporarily. @@ -288,7 +288,7 @@ Use the Pi imager software as your first choice on Windows. Windows can sometime ### Open Pill -The Open Pill enclosure design is all about quick, simple and inexpensive depoloyment of a SeedSigner device. The design does not require any additional hardware and can be printed using a standard FDM 3D printer in about 2 hours, no supports necessary. A video demonstrating the assembly process can be found [here](https://youtu.be/gXPFJygZobEa). To access the design file and printable model, click [here](https://github.com/SeedSigner/seedsigner/tree/main/enclosures/open_pill). +The Open Pill enclosure design is all about quick, simple and inexpensive deployment of a SeedSigner device. The design does not require any additional hardware and can be printed using a standard FDM 3D printer in about 2 hours, no supports necessary. A video demonstrating the assembly process can be found [here](https://youtu.be/gXPFJygZobEa). To access the design file and printable model, click [here](https://github.com/SeedSigner/seedsigner/tree/main/enclosures/open_pill). ### Orange Pill diff --git a/docs/code_structure.md b/docs/code_structure.md index 50b7d315..5a0f0276 100644 --- a/docs/code_structure.md +++ b/docs/code_structure.md @@ -1,21 +1,17 @@ # Code Structure -SeedSigner roughly follows a Model-View-Controller approach. Like in a typical web app (e.g. Flask) the `View`s can be called as needed like individual web urls. After completing display and interaction with the user, the `View` then decides where to route the user next, analogous to a web app returning a `response.redirect(url)`. - -The `Controller` then ends up being quite stripped down. For example, there's no need for a web app's `urls.py` since there are no mappings from url to `View` to maintain since we're not actually using a url/http routing approach. - -`View`s have to handle user interaction so there are `while True` loops that cycle between waiting for user input, gathering data, and then updating the UI components accordingly. You wouldn't find this kind of cycle in a web app because this sort of interactive user input is handled in the browser at the html/css/js level. +SeedSigner roughly follows a Model-View-Controller approach. Like in a typical web app (e.g. Flask), the `View`s can be called as needed like individual web URLs. After completing display and interaction with the user, the `View` then decides where to route the user next, analogous to a web app returning a `response.redirect(URL)`. +The `Controller` then ends up being quite stripped down. For example, there's no need for a web app's `urls.py` since there are no mappings from URL to `View` to maintain since we're not actually using a URL/HTTP routing approach. +`View`s have to handle user interaction, so there are `while True` loops that cycle between waiting for user input, gathering data, and then updating the UI components accordingly. You wouldn't find this kind of cycle in a web app because this sort of interactive user input is handled in the browser at the HTML/CSS/JS level. * `Model`s: Store the persistent settings, the in-memory seeds, current wallet information, etc. * `Controller`: Manages the state of the world and controls access to global resources. * `View`s: Implementation of each screen. Prepares relevant data for display. Must also instantiate the display objects that will actually render the UI. -* `gui.screens`: Re-usable formatted UI renderers. +* `gui.screens`: Reusable formatted UI renderers. * `gui.components`: Basic individual UI elements that are used by the `templates` such as the top nav, buttons, button lists, text displays. -In an typical webserver context the `View` would send data to an html template (e.g. Jinja) which would then dynamically populate the page with html elements like ``, `