From 55da96bfb0c78184c07eaedf5c8c517aea9b0e83 Mon Sep 17 00:00:00 2001 From: DesobedienteTecnologico <52879067+DesobedienteTecnologico@users.noreply.github.com> Date: Thu, 12 Aug 2021 14:10:40 +0300 Subject: [PATCH 01/40] Font changed to Free/Public domain, GPL, OFL --- README.md | 2 +- src/seedsigner/models/blue_wallet.py | 8 +- src/seedsigner/models/generic_ur2_wallet.py | 8 +- src/seedsigner/models/sparrow_wallet.py | 8 +- src/seedsigner/models/wallet.py | 8 +- src/seedsigner/resources/fonts/Anton.ttf | Bin 0 -> 42376 bytes src/seedsigner/resources/fonts/Cour.ttf | Bin 0 -> 303296 bytes src/seedsigner/views/io_test_view.py | 12 +- src/seedsigner/views/menu_view.py | 12 +- src/seedsigner/views/seed_tools_view.py | 80 +++++----- src/seedsigner/views/settings_tools_view.py | 12 +- src/seedsigner/views/signing_tools_view.py | 44 +++--- src/seedsigner/views/view.py | 154 ++++++++++---------- 13 files changed, 174 insertions(+), 174 deletions(-) create mode 100644 src/seedsigner/resources/fonts/Anton.ttf create mode 100644 src/seedsigner/resources/fonts/Cour.ttf diff --git a/README.md b/README.md index ed89842a..cb6813aa 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Exit and reboot when prompted within the raspi-config interface. Install these dependencies: ``` -sudo apt-get update && sudo apt-get install -y wiringpi python3-pip python3-numpy python-pil libopenjp2-7 ttf-mscorefonts-installer git python3-opencv libzbar0 python3-picamera +sudo apt-get update && sudo apt-get install -y wiringpi python3-pip python3-numpy python-pil libopenjp2-7 git python3-opencv libzbar0 python3-picamera ``` Install the [C library for Broadcom BCM 2835](http://www.airspayce.com/mikem/bcm2835/): diff --git a/src/seedsigner/models/blue_wallet.py b/src/seedsigner/models/blue_wallet.py index dad039b2..a2aa5e2c 100644 --- a/src/seedsigner/models/blue_wallet.py +++ b/src/seedsigner/models/blue_wallet.py @@ -122,10 +122,10 @@ class BlueWallet(Wallet): if not self.capture_complete() and self.scan_display_working == 0: self.scan_display_working = 1 View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize("Collecting QR Codes:", font=View.IMPACT25) - View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill=View.color, font=View.IMPACT25) - tw, th = View.draw.textsize(str(round(self.percentage_complete * 100)) + "% Complete", font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), str(round(self.percentage_complete * 100)) + "% Complete", fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize("Collecting QR Codes:", font=View.ANTON25) + View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill=View.color, font=View.ANTON25) + tw, th = View.draw.textsize(str(round(self.percentage_complete * 100)) + "% Complete", font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 125), str(round(self.percentage_complete * 100)) + "% Complete", fill=View.color, font=View.ANTON22) View.DispShowImage() self.scan_display_working = 0 diff --git a/src/seedsigner/models/generic_ur2_wallet.py b/src/seedsigner/models/generic_ur2_wallet.py index 9cdd87df..24b3709a 100644 --- a/src/seedsigner/models/generic_ur2_wallet.py +++ b/src/seedsigner/models/generic_ur2_wallet.py @@ -101,10 +101,10 @@ class GenericUR2Wallet(Wallet): # if all frames have not all been captured, display progress to screen/display if not self.capture_complete(): View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize("Collecting QR Codes:", font=View.IMPACT25) - View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill=View.color, font=View.IMPACT25) - tw, th = View.draw.textsize(str(round(self.percentage_complete * 100)) + "% Complete", font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), str(round(self.percentage_complete * 100)) + "% Complete", fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize("Collecting QR Codes:", font=View.ANTON25) + View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill=View.color, font=View.ANTON25) + tw, th = View.draw.textsize(str(round(self.percentage_complete * 100)) + "% Complete", font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 125), str(round(self.percentage_complete * 100)) + "% Complete", fill=View.color, font=View.ANTON22) View.DispShowImage() elif self.scan_started_ind == 0: diff --git a/src/seedsigner/models/sparrow_wallet.py b/src/seedsigner/models/sparrow_wallet.py index 6e9c77a9..3e762fa5 100644 --- a/src/seedsigner/models/sparrow_wallet.py +++ b/src/seedsigner/models/sparrow_wallet.py @@ -120,10 +120,10 @@ class SparrowWallet(Wallet): if not self.capture_complete() and self.scan_display_working == 0: self.scan_display_working = 1 View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize("Collecting QR Codes:", font=View.IMPACT25) - View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill=View.color, font=View.IMPACT25) - tw, th = View.draw.textsize(str(round(self.percentage_complete * 100)) + "% Complete", font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), str(round(self.percentage_complete * 100)) + "% Complete", fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize("Collecting QR Codes:", font=View.ANTON25) + View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill=View.color, font=View.ANTON25) + tw, th = View.draw.textsize(str(round(self.percentage_complete * 100)) + "% Complete", font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 125), str(round(self.percentage_complete * 100)) + "% Complete", fill=View.color, font=View.ANTON22) View.DispShowImage() self.scan_display_working = 0 diff --git a/src/seedsigner/models/wallet.py b/src/seedsigner/models/wallet.py index 47b7ccb6..23460c08 100644 --- a/src/seedsigner/models/wallet.py +++ b/src/seedsigner/models/wallet.py @@ -196,16 +196,16 @@ class Wallet: if not self.capture_complete() and self.scan_display_working == 0: self.scan_display_working = 1 View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize("Collecting QR Codes:", font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize("Collecting QR Codes:", font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 15), "Collecting QR Codes:", fill=View.color, font=View.ANTON22) lines = textwrap.wrap("".join(self.frame_display), width=11) yheight = 60 for line in lines: tw, th = View.draw.textsize(line, font=View.COURIERNEW30) View.draw.text(((240 - tw) / 2, yheight), line, fill=View.color, font=View.COURIERNEW30) yheight += 30 - tw, th = View.draw.textsize("Right to Exit", font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 215), "Right to Exit", fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize("Right to Exit", font=View.ANTON18) + View.draw.text(((240 - tw) / 2, 215), "Right to Exit", fill=View.color, font=View.ANTON18) View.DispShowImage() self.scan_display_working = 0 diff --git a/src/seedsigner/resources/fonts/Anton.ttf b/src/seedsigner/resources/fonts/Anton.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d8dad2574dbcf3651e5173f95f960f676c015098 GIT binary patch literal 42376 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%0Dvv`0tV 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%0Dg4Rz!L zv%IwZJ8T_&KdcW1%`s1^L$an@94%%e&gXKGBoav@R(e+e zMUGLcQbv|jIFj@WzC(x=zt$8T$ajKcu!zYc2O%g2%oaH)JWNGFD*`qV$8EX_i#fi@ z(Kv)G8p7;>zJ1<@Tt-Pl# zfB*7FFQ{E{*-7)h|D%5~b3u0b&aS_3|CiY5b859Y^*bNBoy}hBbIFD4Uc31x-=_)= ztQFSxrMi2XNEP9xH+}*;To&0r?*Xs4-n-em)r&$qK&@yV>PcQ*xMfK)b1uz4f%|pg z-)?-u-s1kEs1{chHy5QM#PJ`rPiUQR>~7FLfy;LC43lgp4>Tk^JkTQ9#$w$4OLBBa zCf+sit&_aTZzJqXb%=@)?&O~M5`H(Q4}LfGt8TD zHvP`zz32E9{ZH|?X+0Xqdv6tb)G)mF^AFA!56)-m{E_*0&zFSx^8Dm{X+B(jpVNNn zL@^}fxzTS0rMf_=z{j9=!|P9duL>3}i#^XAIV2ba5w*^=B8$9jS_9o+v{Eq6jL_T6 zM%b?fKfwrSoa{;>Sn5nC(xtSNPCN3VVpWXYN-3|@YQ|8>j7Y0P35lJZtT(Hmbo?tj z0av%fdGU(s^7JahZDa87ho?IgPpE&chJ&PTbkutr?e|bB@bL6^co#l&--99!vMo_A z1GT!L^aY-2I;BF_q~NzE)v7E|g;FuaPEU>Jtn{J?FyLk|Y^f4!N~_VYIXo%skt;OY)b*d)q=fpO|kkcyVx)VpA zch`}(wu@$rUsKN#txB6xL{WT46wT)HrR+df1acj{J`4tr!x8pm6JlNoOBTh0is?c+ zhMH4KDm|+tGpD=-5>E96D{{Cb7?hHqIsFNL$v@y<=ilNt`00{A)NwVSLRdPKyg@~0 zEFZmihtS=$Qrw|8K#YhI*GJ!SLF1!^>@0(wRs;AC+6Q`-7dLnsh0zGoswsjfh$bMi z3oS38@grI)xL!#6|FP%*-to_ezxD0AT)x)&-yZs9{dwm8_5ROd0a_%(uRd|w6E}<3 zwzPNNUw`>)zLw4(veWiF^mVr2Civ&ysCBQpY004rX&rd2LXH;cKGCVK$Yt7+DIs~}k61?g(~=C#Bf{YhAdnm}b8D8)D0HR~ za?iKZnIg^%V=~)?SY8Or0F#9C3B^;gD<-CNbeQC9wp~GtJTH3ClrW>X(6qXwAX}tM z*@Ap;6}@bA;og|C1>OW21VTL%`?82V$(ME!&j@d{#iX7a6iMJqiWek$MkfUHQI7IS z8f^b14b?^oPC)EkGFrr;79Qaud^|=qDd;V^c*?ir{@L!0_kH8sp`kN= zS^wL>8SY}fuVv(%4-XBA=MN2i`02|(t(#e2{Pc|%f2o7Ek!nVu^2)J0Dk z4k&cM^|tGXODeerT>HQk^9@vxhAp!C^9^8;mcQZMhZ+-aBXdKS$Tw0pj!yK@@VoQavH%#XwvH{GSXP9vfqk*dcjSP@-25M=L%lKe| zT>f3c$5dT!3Ss@bOSsmEAVB8|J-Rp8VWLM0V$PkZ6;f$J1r83!ALO_Ys zgipg`OZjQkJA+yrm8MD4rp*uyE>{S0TSR^TPFun>Bd2tV)0JwAQbcc`S9Z({DL`Dj zqI!#NG=No{hQ|z|A*>cd19JgCTuklhz%gi%-$B_PlG|L2H#r3^a4}w-J{qDI!G9yk zC$WvGOWjypT3Kql!yRx9k0kfN9jlD*2g-l;`+GkB;V)E&h7J!6ZMg8n*;k*7GB5R4 zCd+@${WlyF z*~W$**ShmqEyYG-9dT1NK``xJhIdj#*17I zN<{;@6&qqK;rF{-CdHN}Jf2=*1*jtU9i|A{^w`iwLJ$xOB)3lz6q-`*@;>Gjw~z(k zr4~K@4uewmsGhAJ$>~9a%(Kq3A8~H#&eJ~8oo55(xo!Y-pKL{t7Zp9?JV&!0OHD8= z$t(Q#ANWS5aG5ECsdPxfEX9N8&6LDra}=-{4gM-ludmWzk}Qk-FR=q&a(cyqldJMb zV9~h7d;aFJQ25253ObsC(TCJp3c3PDUx>g<^|WfeJ&wLP)8Y%_V&dr|uc21RYdcb5 ziueGL94hik-m!>`NxfH%yNf=z+v|(Tk`ys1*{o~~#3N$V8<$O3pE4*Nknp|7sWT8}$M*}~87Ss)X?SmE zkIm_{+hQ(?+Tgm>!=pAaJ-I*bRcD!cxaT8gU>3z-S6snR-0ecTgURC#0RiJqx<$b~ z;=bFx-+j=1$Zc?+XiHkzI_rL`* z)^?WPUt>{&n7UHzC*MW@GE%L68%ODGxG#Q$)E{k@>gX=U7Lx|F5Io@%n3XKNKVN?w zSyzx3ZjvAq$$^rrwgifeyew;^Rbio=tRVX(t;24%##>uOM=tJI1=g`kyjX1u=i{p) z>m%Y;^oolZxi~R8k+?dRSO@JV+&0WTYg>#uW2i_VJ(?284%BoTT|_bik^#v&l#laP zLrh?jP!S8$M5;vB2Tl`hNHXl8LiYBND~L<_NwS?^WG>m5a_KtWTVKq3qH{hy!5^&B^-&YiPFJ$X#GOux*t z!aL?WEpS@)THUq2YlGKjo*0aIO)d`sbN6od^Z;Q-hk|xET|1ym51m|%gIe_T0N>uz ziAM>K)9_e{#|jltjCN|fhsId;N{OE6BX{6PpGimh&>dz6RsG>(`gA(aKi7*3E9qUQ z(bEcgT6r2Y4Ar`#xNOCW)0V|nj-AfOtBbVn@wQU2t5oXj zinT*~imS7|syZ90NIVok$B@8DC!ZXjGvu8!XUG5qn9qZJ6_0W2p9kd)vw>7d7i}3-)49qZ=|6+2H&7W&nr)TA8C`yw z1zh1rT;xZupNTjE|`1n)KYW20W_bhB#Q9k>`OJ~lTH!JUVZd`Lo zeAc{qFQN)s{NgnmoxZ~Cp`n3GPh8tEI=esLmiMR6TE1xUEXA)54ZS#T9&E_fiypdn zX{313to{$N%TE4Id;hoUKQ9y<3QPZ`ZY7q${`!~tD>)}#DBr1-zj^(f4^2}qDn?HI z@>fn`chLt($cRtDpqU1KBfc$`n1w7$%@`Q zC@cXpPlQ#tg2G`jk3^)iateBNT&o6531#Li57Gi>!6`sCzA2;*HoQ07cq8GBX7QTi zt>~_8;O-kT?@3GNPgDqkULYbt27Hx7INWTbKoD+VIv$YC@@P1%3K!j2POgt9wNPcl z1J?+fYn8Fc-vdvaG>zF_efK-{pTzA>|EBw|edgKAc0V$0-n?mS`?EKG<(aT4(PBn} zASGqCL@n+cAO7ac;;MnyZu;SsMSs}7pyzWJopf2IQ2g@+mpyt_u_ZjaC1hN&l(kHo z;kBK;3OI8MTkvEdb$?3KG+PJS(SD$EOSl@O>j^IADsp5^%&BEZSk2uH zr)V=Ntk`gyW>CHRUUfyTDCBZ2LM)SV*zNIj%8Mcj092*X_^3t6c&t`YfeSx^>7h(K zl&f?AZH$W)%hBOme+76=L(%Qm0U_VW-TTJ7ZxjlUu6)h7?K2xo7!Wkur%m;;k=ot0y)|i7 zjSbYsYLB6A1zew6^F6@K#|v?zy$I7RFx{tNXKS zL40DA0pWTy*B@v5$FQZ25*j+!yy2xEFI{o&tsfm4x~$df`Rt-C7poV{n^*tw(9n73 z4GppOdGjt%myez?dd|jLuu#;^>c90BU)PU_b)!n2^0^fn@7Cz9zWRg*?_3dXUpE)< zJH|b?($+4QGQ&D-o6&)bY-vyx3g+n^+6 zZz9u;r)>goepl|XoVX=-0M%BhsW3pZ�jm2QX3bvZ#t%Mad}+i(}$Calfb+$yI^I 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 YT{-Kox#{eO9!q}ssT<-WUTT*9ALIy2IRF3v literal 0 HcmV?d00001 diff --git a/src/seedsigner/views/io_test_view.py b/src/seedsigner/views/io_test_view.py index f296d41a..f48b1626 100644 --- a/src/seedsigner/views/io_test_view.py +++ b/src/seedsigner/views/io_test_view.py @@ -69,7 +69,7 @@ class IOTestView(View): self.redraw_complete = False self.redraw = False self.draw.rectangle((0,0,View.canvas_width, View.canvas_height), outline=0, fill=0) - self.draw.text((45, 5), "Input/Output Check:", fill=View.color, font=View.IMPACT18) + self.draw.text((45, 5), "Input/Output Check:", fill=View.color, font=View.ANTON18) self.draw.polygon([(61, 89), (80, 46), (99, 89)], outline=View.color, fill=0) self.draw.polygon([(51, 100), (8, 119), (51, 138)], outline=View.color, fill=0) self.draw.polygon([(109, 100), (152, 119), (109, 138)], outline=View.color, fill=0) @@ -77,10 +77,10 @@ class IOTestView(View): self.draw.ellipse([(61, 99), (99, 141)], outline=View.color, fill=0) self.draw.ellipse([(198, 40), (238, 80)], outline=View.color, fill=0) self.draw.ellipse([(198, 95), (238, 135)], outline=View.color, fill=0) - self.draw.text((200, 160), "EXIT", fill=View.color, font=View.IMPACT18) + self.draw.text((200, 160), "EXIT", fill=View.color, font=View.ANTON18) self.draw.rectangle((30, 205, 210, 235), outline=View.color, fill="BLACK") - tw, th = self.draw.textsize(self.qr_text, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(self.qr_text, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill=View.color, font=View.ANTON22) View.DispShowImage() self.redraw_complete = True @@ -134,8 +134,8 @@ class IOTestView(View): self.qr_text = "QR Scanned" if self.redraw == False and self.redraw_complete == True: self.draw.rectangle((30, 205, 210, 235), outline=View.color, fill=View.color) - tw, th = self.draw.textsize(self.qr_text, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill="BLACK", font=View.IMPACT22) + tw, th = self.draw.textsize(self.qr_text, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill="BLACK", font=View.ANTON22) View.DispShowImage() self.redraw = True \ No newline at end of file diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index c5b9b974..23e6004f 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -306,8 +306,8 @@ class MenuView(View): #Menu has changed, redraw View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize(t, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 2), t, fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(t, font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 2), t, fill=View.color, font=View.ANTON22) num_of_lines = len(lines) @@ -345,8 +345,8 @@ class MenuView(View): if num_of_lines >= 15: self.draw_menu_text(15, 175, lines[14], (True if selected_menu_num == 15 else False)) - tw, th = View.draw.textsize(b, font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), b, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(b, font=View.ANTON18) + View.draw.text(((240 - tw) / 2, 210), b, fill=View.color, font=View.ANTON18) View.DispShowImage() # saved update menu lines and selection @@ -373,8 +373,8 @@ class MenuView(View): def draw_menu_text(self, x, y, line, selected) -> None: if selected == True: View.draw.rectangle((5, y-3, 235, y+28), outline=0, fill=View.color) - View.draw.text((x, y) , line, fill="BLACK", font=View.IMPACT20) + View.draw.text((x, y) , line, fill="BLACK", font=View.ANTON20) else: - View.draw.text((x, y) , line, fill=View.color, font=View.IMPACT20) + View.draw.text((x, y) , line, fill=View.color, font=View.ANTON20) return diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 33fba4b4..57438aa8 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -734,22 +734,22 @@ class SeedToolsView(View): def draw_gather_words(self): View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - View.draw.text((75, 2), "Seed Word: " + str(len(self.words)+1), fill=View.color, font=View.IMPACT18) - View.draw.text((15, 210), "(choose from words on right)", fill=View.color, font=View.IMPACT18) + View.draw.text((75, 2), "Seed Word: " + str(len(self.words)+1), fill=View.color, font=View.ANTON18) + View.draw.text((15, 210), "(choose from words on right)", fill=View.color, font=View.ANTON18) # draw possible words (3 at most) self.possible_words = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(self.letters))] if len(self.possible_words) >= 1: for idx, word in enumerate(self.possible_words, start=0): - word_offset = 223 - View.IMPACT25.getsize(word)[0] - View.draw.text((word_offset, 39 + (60*idx)), word + " -", fill=View.color, font=View.IMPACT25) + word_offset = 223 - View.ANTON25.getsize(word)[0] + View.draw.text((word_offset, 39 + (60*idx)), word + " -", fill=View.color, font=View.ANTON25) if idx >= 2: break # draw letter and arrows for idx, letter in enumerate(self.letters, start=0): - tw, th = View.draw.textsize(letter, font=View.IMPACT35) - View.draw.text((((idx*30)+((30-tw)/2)), 92), letter, fill=View.color, font=View.IMPACT35) + tw, th = View.draw.textsize(letter, font=View.ANTON35) + View.draw.text((((idx*30)+((30-tw)/2)), 92), letter, fill=View.color, font=View.ANTON35) if idx+1 == len(self.letters): # draw arrows only above last/active letter View.draw.polygon([(8 + (idx*30), 85) , (14 + (idx*30), 69) , (20 + (idx*30), 85 )], outline=View.color, fill="BLACK") @@ -802,17 +802,17 @@ class SeedToolsView(View): # Screen Title View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize("Add Passphrase", font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 2), "Add Passphrase", fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize("Add Passphrase", font=View.ANTON18) + View.draw.text(((240 - tw) / 2, 2), "Add Passphrase", fill=View.color, font=View.ANTON18) # Screen Botton - tw, th = View.draw.textsize("use joystick to add and", font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 197), "use joystick to add and", fill=View.color, font=View.IMPACT18) - tw, th = View.draw.textsize("remove passphrase characters", font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 217), "remove passphrase characters", fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize("use joystick to add and", font=View.ANTON18) + View.draw.text(((240 - tw) / 2, 197), "use joystick to add and", fill=View.color, font=View.ANTON18) + tw, th = View.draw.textsize("remove passphrase characters", font=View.ANTON18) + View.draw.text(((240 - tw) / 2, 217), "remove passphrase characters", fill=View.color, font=View.ANTON18) # Display passphrase selection in progress - View.draw.text((5,33), "Phrase:", fill=View.color, font=View.IMPACT20) + View.draw.text((5,33), "Phrase:", fill=View.color, font=View.ANTON20) View.draw.text((72,35), self.passphrase[:14] + "▒", fill=View.color, font=View.COURIERNEW20) if len(self.passphrase) >= 14: @@ -820,16 +820,16 @@ class SeedToolsView(View): # Display message when max passphrase length reached if len(self.passphrase) >= 28: - View.draw.text((50,70), "Max passphrase of 28", fill=View.color, font=View.IMPACT18) - View.draw.text((53,90), "characters reached", fill=View.color, font=View.IMPACT18) + View.draw.text((50,70), "Max passphrase of 28", fill=View.color, font=View.ANTON18) + View.draw.text((53,90), "characters reached", fill=View.color, font=View.ANTON18) # Save Button - c_x_offset = 240 - View.IMPACT25.getsize("Exit")[0] - View.draw.text((c_x_offset , 172), "Exit", fill=View.color, font=View.IMPACT22) + c_x_offset = 240 - View.ANTON25.getsize("Exit")[0] + View.draw.text((c_x_offset , 172), "Exit", fill=View.color, font=View.ANTON22) # Toglle Button - x = 240 - View.IMPACT20.getsize(self.pass_case_toggle)[0] - View.draw.text((x, 110), self.pass_case_toggle, fill=View.color, font=View.IMPACT20) + x = 240 - View.ANTON20.getsize(self.pass_case_toggle)[0] + View.draw.text((x, 110), self.pass_case_toggle, fill=View.color, font=View.ANTON20) # draw letter and arrows if self.pass_case_toggle == "lower": @@ -839,8 +839,8 @@ class SeedToolsView(View): else: pass_letter_disp = self.pass_letter - tw, th = View.draw.textsize(pass_letter_disp, font=View.IMPACT35) - View.draw.text((((30-tw)/2), 112), pass_letter_disp, fill=View.color, font=View.IMPACT35) + tw, th = View.draw.textsize(pass_letter_disp, font=View.ANTON35) + View.draw.text((((30-tw)/2), 112), pass_letter_disp, fill=View.color, font=View.ANTON35) View.draw.polygon([(8, 105) , (14, 89) , (20, 105 )], outline=View.color, fill="BLACK") View.draw.polygon([(8, 168), (14, 184), (20, 168)], outline=View.color, fill="BLACK") @@ -915,13 +915,13 @@ class SeedToolsView(View): last_word = splitseed[-1] self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = self.draw.textsize("The final word is :", font=View.IMPACT23) - self.draw.text(((240 - tw) / 2, 60), "The final word is :", fill=View.color, font=View.IMPACT23) - tw, th = self.draw.textsize(last_word, font=View.IMPACT50) - self.draw.text(((240 - tw) / 2, 90), last_word, fill=View.color, font=View.IMPACT50) + tw, th = self.draw.textsize("The final word is :", font=View.ANTON23) + self.draw.text(((240 - tw) / 2, 60), "The final word is :", fill=View.color, font=View.ANTON23) + tw, th = self.draw.textsize(last_word, font=View.ANTON50) + self.draw.text(((240 - tw) / 2, 90), last_word, fill=View.color, font=View.ANTON50) - tw, th = View.draw.textsize("Right to Continue", font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), "Right to Continue", fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize("Right to Continue", font=View.ANTON18) + View.draw.text(((240 - tw) / 2, 210), "Right to Continue", fill=View.color, font=View.ANTON18) View.DispShowImage() @@ -1053,7 +1053,7 @@ class SeedToolsView(View): def draw_dice(self, dice_selected): self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - self.draw.text((45, 5), "Dice roll: " + str(self.roll_number) + "/99", fill=View.color, font=View.IMPACT26) + self.draw.text((45, 5), "Dice roll: " + str(self.roll_number) + "/99", fill=View.color, font=View.ANTON26) # when dice is selected, rect fill will be orange and ellipse will be black, ellipse outline will be the black # when dice is not selected, rect will will be black and ellipse will be orange, ellipse outline will be orange @@ -1137,7 +1137,7 @@ class SeedToolsView(View): self.draw.ellipse([(208, 174), (220, 186)], outline=View.color, fill=View.color) # bottom text - self.draw.text((18, 210), "Press Control Stick to Select", fill=View.color, font=View.IMPACT18) + self.draw.text((18, 210), "Press Control Stick to Select", fill=View.color, font=View.ANTON18) View.DispShowImage() self.dice_selected = dice_selected @@ -1185,19 +1185,19 @@ class SeedToolsView(View): title = "Seed Phrase (2/2)" word_index_offset = 12 # Skip ahead one page worth of words - tw, th = View.draw.textsize(title, font=View.IMPACT18) - draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(title, font=View.ANTON18) + draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.ANTON18) for i in range(0, max_range): - draw.text(word_positions[i], f"{i + 1 + word_index_offset}: " + seed_phrase[i + word_index_offset] , fill=View.color, font=View.IMPACT22) + draw.text(word_positions[i], f"{i + 1 + word_index_offset}: " + seed_phrase[i + word_index_offset] , fill=View.color, font=View.ANTON22) if passphrase and ((len(seed_phrase) > 12 and page_num == 2) or (len(seed_phrase) <= 12 and page_num == 1)): disp_passphrase = "Passphrase: ************" - tw, th = View.draw.textsize(disp_passphrase, font=View.IMPACT18) - draw.text(((240 - tw) / 2, 185), disp_passphrase, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(disp_passphrase, font=View.ANTON18) + draw.text(((240 - tw) / 2, 185), disp_passphrase, fill=View.color, font=View.ANTON18) - tw, th = View.draw.textsize(bottom, font=View.IMPACT18) - draw.text(((240 - tw) / 2, 212), bottom, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(bottom, font=View.ANTON18) + draw.text(((240 - tw) / 2, 212), bottom, fill=View.color, font=View.ANTON18) View.DispShowImage() @@ -1267,7 +1267,7 @@ class SeedToolsView(View): qr = QR() image = qr.qrimage(data, width=240, height=240, border=3) - View.DispShowImageWithText(image, "click to zoom, right to exit", font=View.IMPACT18, text_color="BLACK", text_background="ORANGE") + View.DispShowImageWithText(image, "click to zoom, right to exit", font=View.ANTON18, text_color="BLACK", text_background="ORANGE") input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_PRESS]) if input == B.KEY_RIGHT: @@ -1310,8 +1310,8 @@ class SeedToolsView(View): draw.line((mask_width, View.canvas_height - mask_height, View.canvas_width - mask_width, View.canvas_height - mask_height), fill=View.color) msg = "click to exit" - tw, th = draw.textsize(msg, font=View.IMPACT18) - draw.text(((View.canvas_width - tw) / 2, View.canvas_height - th - 2), msg, fill=View.color, font=View.IMPACT18) + tw, th = draw.textsize(msg, font=View.ANTON18) + draw.text(((View.canvas_width - tw) / 2, View.canvas_height - th - 2), msg, fill=View.color, font=View.ANTON18) def draw_block_labels(cur_block_x, cur_block_y): # Create overlay for block labels (e.g. "D-5") @@ -1350,7 +1350,7 @@ class SeedToolsView(View): ) while True: - # View.draw_text_over_image("click to exit", font=View.IMPACT18, text_color="BLACK", text_background="ORANGE") + # View.draw_text_over_image("click to exit", font=View.ANTON18, text_color="BLACK", text_background="ORANGE") input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) if input == B.KEY_RIGHT: diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index a4e4dfc8..af5e1494 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -99,10 +99,10 @@ class SettingsToolsView(View): line3 = "(Joystick RIGHT to EXIT)" View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize(line1, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 20), line1, fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(line2, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 55), line2, fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(line3, font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), line3, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(line1, font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 20), line1, fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(line2, font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 55), line2, fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(line3, font=View.ANTON18) + View.draw.text(((240 - tw) / 2, 210), line3, fill=View.color, font=View.ANTON18) View.DispShowImage() diff --git a/src/seedsigner/views/signing_tools_view.py b/src/seedsigner/views/signing_tools_view.py index 08eda71e..1a078c90 100644 --- a/src/seedsigner/views/signing_tools_view.py +++ b/src/seedsigner/views/signing_tools_view.py @@ -108,15 +108,15 @@ class SigningToolsView(View): def display_transaction_information(self, wallet) -> None: self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = self.draw.textsize("Confirm Tx Details", font=View.IMPACT25) - self.draw.text(((240 - tw) / 2, 3), "Confirm Tx Details", fill=View.color, font=View.IMPACT25) + tw, th = self.draw.textsize("Confirm Tx Details", font=View.ANTON25) + self.draw.text(((240 - tw) / 2, 3), "Confirm Tx Details", fill=View.color, font=View.ANTON25) in_fee_outs_str = str(wallet.ins) in_fee_outs_str += " inputs - fee = " if wallet.ins > 1 else " input - fee = " in_fee_outs_str += str(wallet.outs) in_fee_outs_str += " outs" if wallet.outs > 1 else " out" - tw, th = self.draw.textsize(in_fee_outs_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 40), in_fee_outs_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(in_fee_outs_str, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 40), in_fee_outs_str, fill=View.color, font=View.ANTON22) receiving_addr_str1 = "" receiving_addr_str2 = "" @@ -129,41 +129,41 @@ class SigningToolsView(View): else: receiving_addr_str1 += "Self-Transfer" - tw, th = self.draw.textsize(receiving_addr_str1, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 75), receiving_addr_str1, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(receiving_addr_str1, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 75), receiving_addr_str1, fill=View.color, font=View.ANTON22) if len(receiving_addr_str2) > 0: - tw, th = self.draw.textsize(receiving_addr_str2, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 100), receiving_addr_str2, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(receiving_addr_str2, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 100), receiving_addr_str2, fill=View.color, font=View.ANTON22) if wallet.spend > 0: spending_str = "Spend: " + str(wallet.spend) + " sats" - tw, th = self.draw.textsize(spending_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 130), spending_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(spending_str, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 130), spending_str, fill=View.color, font=View.ANTON22) if wallet.change > 0 and wallet.dest_addr_cnt == 0: change_str = "Amount: " + str(int(wallet.change)) + " sats" - tw, th = self.draw.textsize(change_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(change_str, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.ANTON22) elif wallet.change > 0: change_str = "Change: " + str(int(wallet.change)) + " sats" - tw, th = self.draw.textsize(change_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(change_str, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.ANTON22) fee_str = "Fee: " + str(int(wallet.fee)) + " sats" - tw, th = self.draw.textsize(fee_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 180), fee_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(fee_str, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 180), fee_str, fill=View.color, font=View.ANTON22) - tw, th = self.draw.textsize("Left to Exit, Right to Continue", font=View.IMPACT18) - self.draw.text(((240 - tw) / 2, 215), "Left to Exit, Right to Continue", fill=View.color, font=View.IMPACT18) + tw, th = self.draw.textsize("Left to Exit, Right to Continue", font=View.ANTON18) + self.draw.text(((240 - tw) / 2, 215), "Left to Exit, Right to Continue", fill=View.color, font=View.ANTON18) View.DispShowImage() def qr_gen_status(percentage): View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize("QR Generation", font=View.IMPACT25) - View.draw.text(((240 - tw) / 2, 90), "QR Generation", fill=View.color, font=View.IMPACT25) - tw, th = View.draw.textsize(str(round(percentage)) + "% Complete", font=View.IMPACT25) - View.draw.text(((240 - tw) / 2, 125), str(round(percentage)) + "% Complete", fill=View.color, font=View.IMPACT25) + tw, th = View.draw.textsize("QR Generation", font=View.ANTON25) + View.draw.text(((240 - tw) / 2, 90), "QR Generation", fill=View.color, font=View.ANTON25) + tw, th = View.draw.textsize(str(round(percentage)) + "% Complete", font=View.ANTON25) + View.draw.text(((240 - tw) / 2, 125), str(round(percentage)) + "% Complete", fill=View.color, font=View.ANTON25) View.DispShowImage() diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index f11e4f30..d254fa80 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -18,21 +18,21 @@ class View: HEIGHT = 240 # Define necessary fonts - IMPACT16 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 16) - IMPACT18 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 18) - IMPACT20 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 20) - IMPACT21 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 21) - IMPACT22 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 22) - IMPACT23 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 23) - IMPACT25 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 25) - IMPACT26 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 26) - IMPACT35 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 35) - IMPACT50 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 50) - COURIERNEW14 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 14) - COURIERNEW24 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 24) - COURIERNEW38 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 38) - COURIERNEW30 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 30) - COURIERNEW20 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 20) + ANTON16 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 16) + ANTON18 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 18) + ANTON20 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 20) + ANTON21 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 21) + ANTON22 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 22) + ANTON23 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 23) + ANTON25 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 25) + ANTON26 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 26) + ANTON35 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 35) + ANTON50 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 50) + COURIERNEW14 = ImageFont.truetype(os.path.join(font_path, "Cour.ttf"), 14) + COURIERNEW24 = ImageFont.truetype(os.path.join(font_path, "Cour.ttf"), 24) + COURIERNEW38 = ImageFont.truetype(os.path.join(font_path, "Cour.ttf"), 38) + COURIERNEW30 = ImageFont.truetype(os.path.join(font_path, "Cour.ttf"), 30) + COURIERNEW20 = ImageFont.truetype(os.path.join(font_path, "Cour.ttf"), 20) font_path = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "fonts") @@ -146,37 +146,37 @@ class View: View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) if len(title) > 0: - tw, th = View.draw.textsize(title, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(title, font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.ANTON22) if len(bottom) > 0: - tw, th = View.draw.textsize(bottom, font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(bottom, font=View.ANTON18) + View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.ANTON18) if len(lines) == 1: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT26) - View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT26) + tw, th = View.draw.textsize(lines[0], font=View.ANTON26) + View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ANTON26) elif len(lines) == 2: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(lines[1], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.ANTON22) elif len(lines) == 3: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT26) - View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.IMPACT26) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[2], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ANTON26) + View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.ANTON26) + tw, th = View.draw.textsize(lines[1], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(lines[2], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ANTON22) elif len(lines) == 4: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[2], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[3], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(lines[1], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(lines[2], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(lines[3], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.ANTON22) View.DispShowImage() @@ -192,46 +192,46 @@ class View: View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) if len(title) > 0: - tw, th = View.draw.textsize(title, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(title, font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.ANTON22) if len(bottom) > 0: - tw, th = View.draw.textsize(bottom, font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(bottom, font=View.ANTON18) + View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.ANTON18) if len(lines) == 1: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT26) - View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT26) + tw, th = View.draw.textsize(lines[0], font=View.ANTON26) + View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ANTON26) elif len(lines) == 2: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(lines[1], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.ANTON22) elif len(lines) == 3: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT26) - View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.IMPACT26) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[2], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ANTON26) + View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.ANTON26) + tw, th = View.draw.textsize(lines[1], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(lines[2], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ANTON22) elif len(lines) == 4: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[2], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[3], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(lines[1], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(lines[2], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(lines[3], font=View.ANTON22) + View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.ANTON22) - a_x_offset = 240 - View.IMPACT25.getsize(a_txt)[0] - View.draw.text((a_x_offset, 39 + 0), a_txt, fill=View.color, font=View.IMPACT25) + a_x_offset = 240 - View.ANTON25.getsize(a_txt)[0] + View.draw.text((a_x_offset, 39 + 0), a_txt, fill=View.color, font=View.ANTON25) - b_x_offset = 240 - View.IMPACT25.getsize(b_txt)[0] - View.draw.text((b_x_offset , 39 + 60), b_txt, fill=View.color, font=View.IMPACT25) + b_x_offset = 240 - View.ANTON25.getsize(b_txt)[0] + View.draw.text((b_x_offset , 39 + 60), b_txt, fill=View.color, font=View.ANTON25) - c_x_offset = 240 - View.IMPACT25.getsize(c_txt)[0] - View.draw.text((c_x_offset , 39 + 120), c_txt, fill=View.color, font=View.IMPACT25) + c_x_offset = 240 - View.ANTON25.getsize(c_txt)[0] + View.draw.text((c_x_offset , 39 + 120), c_txt, fill=View.color, font=View.ANTON25) View.DispShowImage() @@ -251,14 +251,14 @@ class View: line3 = "30 seconds before" line4 = "disconnecting power." - tw, th = View.draw.textsize(line1, font=View.IMPACT22) - View.draw.text(((240-tw)/2, 45), line1, fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(line2, font=View.IMPACT20) - View.draw.text(((240-tw)/2, 100), line2, fill=View.color, font=View.IMPACT20) - tw, th = View.draw.textsize(line3, font=View.IMPACT20) - View.draw.text(((240-tw)/2, 130), line3, fill=View.color, font=View.IMPACT20) - tw, th = View.draw.textsize(line4, font=View.IMPACT20) - View.draw.text(((240-tw)/2, 160), line4, fill=View.color, font=View.IMPACT20) + tw, th = View.draw.textsize(line1, font=View.ANTON22) + View.draw.text(((240-tw)/2, 45), line1, fill=View.color, font=View.ANTON22) + tw, th = View.draw.textsize(line2, font=View.ANTON20) + View.draw.text(((240-tw)/2, 100), line2, fill=View.color, font=View.ANTON20) + tw, th = View.draw.textsize(line3, font=View.ANTON20) + View.draw.text(((240-tw)/2, 130), line3, fill=View.color, font=View.ANTON20) + tw, th = View.draw.textsize(line4, font=View.ANTON20) + View.draw.text(((240-tw)/2, 160), line4, fill=View.color, font=View.ANTON20) View.DispShowImage() From 4d0c3fb0ef989eb387e8676fb3f4fe9f26ccd1af Mon Sep 17 00:00:00 2001 From: Jaime Roque <52879067+DesobedienteTecnologico@users.noreply.github.com> Date: Mon, 16 Aug 2021 11:37:03 +0300 Subject: [PATCH 02/40] Define path before use in the new fonts --- src/seedsigner/views/view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index d254fa80..2fbb8a92 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -17,6 +17,8 @@ class View: WIDTH = 240 HEIGHT = 240 + font_path = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "fonts") + # Define necessary fonts ANTON16 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 16) ANTON18 = ImageFont.truetype(os.path.join(font_path, "Anton.ttf"), 18) @@ -34,7 +36,6 @@ class View: COURIERNEW30 = ImageFont.truetype(os.path.join(font_path, "Cour.ttf"), 30) COURIERNEW20 = ImageFont.truetype(os.path.join(font_path, "Cour.ttf"), 20) - font_path = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "fonts") ROBOTOCONDENSED_BOLD_16 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 16) ROBOTOCONDENSED_BOLD_18 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 18) From 0f0eda46d5d65ee03c27c9607ba10ff0dd28fe28 Mon Sep 17 00:00:00 2001 From: newtonick Date: Tue, 17 Aug 2021 21:32:12 +0000 Subject: [PATCH 03/40] add assistant and roboto mono fonts --- src/seedsigner/controller.py | 2 +- .../resources/fonts/Assistant-Bold.ttf | Bin 0 -> 49304 bytes .../resources/fonts/Assistant-Medium.ttf | Bin 0 -> 49292 bytes .../resources/fonts/Assistant-OFL.txt | 95 ++++++++ .../resources/fonts/RobotoMono-LICENSE.txt | 202 ++++++++++++++++++ .../resources/fonts/RobotoMono-Medium.ttf | Bin 0 -> 86820 bytes src/seedsigner/views/io_test_view.py | 12 +- src/seedsigner/views/menu_view.py | 12 +- src/seedsigner/views/seed_tools_view.py | 40 ++-- src/seedsigner/views/settings_tools_view.py | 12 +- src/seedsigner/views/signing_tools_view.py | 44 ++-- src/seedsigner/views/view.py | 170 ++++++++------- 12 files changed, 448 insertions(+), 141 deletions(-) create mode 100644 src/seedsigner/resources/fonts/Assistant-Bold.ttf create mode 100644 src/seedsigner/resources/fonts/Assistant-Medium.ttf create mode 100644 src/seedsigner/resources/fonts/Assistant-OFL.txt create mode 100644 src/seedsigner/resources/fonts/RobotoMono-LICENSE.txt create mode 100644 src/seedsigner/resources/fonts/RobotoMono-Medium.ttf diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 177e802a..1c8b2b0c 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -525,7 +525,7 @@ class Controller(Singleton): if frame is not None: if decoder.getPercentComplete() > 0 and decoder.isPSBT(): scan_text = str(decoder.getPercentComplete()) + "% Complete" - View.DispShowImageWithText(frame.resize((240,240)), scan_text, font=View.IMPACT22, text_color=View.color, text_background=(0,0,0,225)) + View.DispShowImageWithText(frame.resize((240,240)), scan_text, font=View.ASSISTANT22, text_color=View.color, text_background=(0,0,0,225)) time.sleep(0.1) # turn this up or down to tune performace while decoding psbt if camera._video_stream is None: break diff --git a/src/seedsigner/resources/fonts/Assistant-Bold.ttf b/src/seedsigner/resources/fonts/Assistant-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b231abef0503961a13a751d288580add927dc254 GIT binary patch literal 49304 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 Ce6P0v literal 0 HcmV?d00001 diff --git a/src/seedsigner/views/io_test_view.py b/src/seedsigner/views/io_test_view.py index d334f689..9b7d8da0 100644 --- a/src/seedsigner/views/io_test_view.py +++ b/src/seedsigner/views/io_test_view.py @@ -75,7 +75,7 @@ class IOTestView(View): self.redraw_complete = False self.redraw = False self.draw.rectangle((0,0,View.canvas_width, View.canvas_height), outline=0, fill=0) - self.draw.text((45, 5), "Input/Output Check:", fill=View.color, font=View.IMPACT18) + self.draw.text((45, 5), "Input/Output Check:", fill=View.color, font=View.ASSISTANT18) self.draw.polygon([(61, 89), (80, 46), (99, 89)], outline=View.color, fill=0) self.draw.polygon([(51, 100), (8, 119), (51, 138)], outline=View.color, fill=0) self.draw.polygon([(109, 100), (152, 119), (109, 138)], outline=View.color, fill=0) @@ -83,10 +83,10 @@ class IOTestView(View): self.draw.ellipse([(61, 99), (99, 141)], outline=View.color, fill=0) self.draw.ellipse([(198, 40), (238, 80)], outline=View.color, fill=0) self.draw.ellipse([(198, 95), (238, 135)], outline=View.color, fill=0) - self.draw.text((200, 160), "EXIT", fill=View.color, font=View.IMPACT18) + self.draw.text((200, 160), "EXIT", fill=View.color, font=View.ASSISTANT18) self.draw.rectangle((30, 205, 210, 235), outline=View.color, fill="BLACK") - tw, th = self.draw.textsize(self.qr_text, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(self.qr_text, font=View.ASSISTANT22) + self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill=View.color, font=View.ASSISTANT22) View.DispShowImage() self.redraw_complete = True @@ -141,8 +141,8 @@ class IOTestView(View): self.qr_text = "QR Scanned" if self.redraw == False and self.redraw_complete == True: self.draw.rectangle((30, 205, 210, 235), outline=View.color, fill=View.color) - tw, th = self.draw.textsize(self.qr_text, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill="BLACK", font=View.IMPACT22) + tw, th = self.draw.textsize(self.qr_text, font=View.ASSISTANT22) + self.draw.text(((240 - tw) / 2, 205), self.qr_text, fill="BLACK", font=View.ASSISTANT22) View.DispShowImage() self.redraw = True \ No newline at end of file diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index fcf0f0b9..6997842c 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -311,8 +311,8 @@ class MenuView(View): #Menu has changed, redraw View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize(t, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 2), t, fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(t, font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 2), t, fill=View.color, font=View.ASSISTANT22) num_of_lines = len(lines) @@ -350,8 +350,8 @@ class MenuView(View): if num_of_lines >= 15: self.draw_menu_text(15, 175, lines[14], (True if selected_menu_num == 15 else False)) - tw, th = View.draw.textsize(b, font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), b, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(b, font=View.ASSISTANT18) + View.draw.text(((240 - tw) / 2, 210), b, fill=View.color, font=View.ASSISTANT18) View.DispShowImage() # saved update menu lines and selection @@ -378,8 +378,8 @@ class MenuView(View): def draw_menu_text(self, x, y, line, selected) -> None: if selected == True: View.draw.rectangle((5, y-3, 235, y+28), outline=0, fill=View.color) - View.draw.text((x, y) , line, fill="BLACK", font=View.IMPACT20) + View.draw.text((x, y) , line, fill="BLACK", font=View.ASSISTANT20BOLD) else: - View.draw.text((x, y) , line, fill=View.color, font=View.IMPACT20) + View.draw.text((x, y) , line, fill=View.color, font=View.ASSISTANT20) return diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 4662ba77..cd540d30 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -675,13 +675,13 @@ class SeedToolsView(View): last_word = splitseed[-1] self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = self.draw.textsize("The final word is :", font=View.IMPACT23) - self.draw.text(((240 - tw) / 2, 60), "The final word is :", fill=View.color, font=View.IMPACT23) - tw, th = self.draw.textsize(last_word, font=View.IMPACT50) - self.draw.text(((240 - tw) / 2, 90), last_word, fill=View.color, font=View.IMPACT50) + tw, th = self.draw.textsize("The final word is :", font=View.ASSISTANT23) + self.draw.text(((240 - tw) / 2, 60), "The final word is :", fill=View.color, font=View.ASSISTANT23) + tw, th = self.draw.textsize(last_word, font=View.ASSISTANT50) + self.draw.text(((240 - tw) / 2, 90), last_word, fill=View.color, font=View.ASSISTANT50) - tw, th = View.draw.textsize("Right to Continue", font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), "Right to Continue", fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize("Right to Continue", font=View.ASSISTANT18) + View.draw.text(((240 - tw) / 2, 210), "Right to Continue", fill=View.color, font=View.ASSISTANT18) View.DispShowImage() @@ -813,7 +813,7 @@ class SeedToolsView(View): def draw_dice(self, dice_selected): self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - self.draw.text((45, 5), "Dice roll: " + str(self.roll_number) + "/99", fill=View.color, font=View.IMPACT26) + self.draw.text((45, 5), "Dice roll: " + str(self.roll_number) + "/99", fill=View.color, font=View.ASSISTANT26) # when dice is selected, rect fill will be orange and ellipse will be black, ellipse outline will be the black # when dice is not selected, rect will will be black and ellipse will be orange, ellipse outline will be orange @@ -897,7 +897,7 @@ class SeedToolsView(View): self.draw.ellipse([(208, 174), (220, 186)], outline=View.color, fill=View.color) # bottom text - self.draw.text((18, 210), "Press Control Stick to Select", fill=View.color, font=View.IMPACT18) + self.draw.text((18, 210), "Press Control Stick to Select", fill=View.color, font=View.ASSISTANT18) View.DispShowImage() self.dice_selected = dice_selected @@ -945,19 +945,19 @@ class SeedToolsView(View): title = "Seed Phrase (2/2)" word_index_offset = 12 # Skip ahead one page worth of words - tw, th = View.draw.textsize(title, font=View.IMPACT18) - draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(title, font=View.ASSISTANT18) + draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.ASSISTANT18) for i in range(0, max_range): - draw.text(word_positions[i], f"{i + 1 + word_index_offset}: " + seed_phrase[i + word_index_offset] , fill=View.color, font=View.IMPACT22) + draw.text(word_positions[i], f"{i + 1 + word_index_offset}: " + seed_phrase[i + word_index_offset] , fill=View.color, font=View.ASSISTANT22) if passphrase and ((len(seed_phrase) > 12 and page_num == 2) or (len(seed_phrase) <= 12 and page_num == 1)): disp_passphrase = "Passphrase: ************" - tw, th = View.draw.textsize(disp_passphrase, font=View.IMPACT18) - draw.text(((240 - tw) / 2, 185), disp_passphrase, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(disp_passphrase, font=View.ASSISTANT18) + draw.text(((240 - tw) / 2, 185), disp_passphrase, fill=View.color, font=View.ASSISTANT18) - tw, th = View.draw.textsize(bottom, font=View.IMPACT18) - draw.text(((240 - tw) / 2, 212), bottom, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(bottom, font=View.ASSISTANT18) + draw.text(((240 - tw) / 2, 212), bottom, fill=View.color, font=View.ASSISTANT18) View.DispShowImage() @@ -1022,7 +1022,7 @@ class SeedToolsView(View): def seed_phrase_as_qr(self, seed_phrase): e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEEDSSQR) image = e.nextPartImage(240, 240, 3) - View.DispShowImageWithText(image, "click to zoom, right to exit", font=View.IMPACT18, text_color="BLACK", text_background="ORANGE") + View.DispShowImageWithText(image, "click to zoom, right to exit", font=View.ASSISTANT18, text_color="BLACK", text_background="ORANGE") input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_PRESS]) if input == B.KEY_RIGHT: @@ -1067,8 +1067,8 @@ class SeedToolsView(View): draw.line((mask_width, View.canvas_height - mask_height, View.canvas_width - mask_width, View.canvas_height - mask_height), fill=View.color) msg = "click to exit" - tw, th = draw.textsize(msg, font=View.IMPACT18) - draw.text(((View.canvas_width - tw) / 2, View.canvas_height - th - 2), msg, fill=View.color, font=View.IMPACT18) + tw, th = draw.textsize(msg, font=View.ASSISTANT18) + draw.text(((View.canvas_width - tw) / 2, View.canvas_height - th - 2), msg, fill=View.color, font=View.ASSISTANT18) def draw_block_labels(cur_block_x, cur_block_y): # Create overlay for block labels (e.g. "D-5") @@ -1107,7 +1107,7 @@ class SeedToolsView(View): ) while True: - # View.draw_text_over_image("click to exit", font=View.IMPACT18, text_color="BLACK", text_background="ORANGE") + # View.draw_text_over_image("click to exit", font=View.ASSISTANT18, text_color="BLACK", text_background="ORANGE") input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) if input == B.KEY_RIGHT: @@ -1158,7 +1158,7 @@ class SeedToolsView(View): while True: frame = self.controller.camera.read_video_stream(as_image=True) if frame is not None: - View.DispShowImageWithText(frame.resize((240,240)), "Scan Seed QR", font=View.IMPACT22, text_color=View.color, text_background=(0,0,0,225)) + View.DispShowImageWithText(frame.resize((240,240)), "Scan Seed QR", font=View.ASSISTANT22, text_color=View.color, text_background=(0,0,0,225)) status = decoder.addImage(frame) if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID): diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index 8c16b47b..053ebc6b 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -109,10 +109,10 @@ class SettingsToolsView(View): line3 = "(Joystick RIGHT to EXIT)" View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize(line1, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 20), line1, fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(line2, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 55), line2, fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(line3, font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), line3, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(line1, font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 20), line1, fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(line2, font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 55), line2, fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(line3, font=View.ASSISTANT18) + View.draw.text(((240 - tw) / 2, 210), line3, fill=View.color, font=View.ASSISTANT18) View.DispShowImage() diff --git a/src/seedsigner/views/signing_tools_view.py b/src/seedsigner/views/signing_tools_view.py index 09c4dde0..ebdee6cf 100644 --- a/src/seedsigner/views/signing_tools_view.py +++ b/src/seedsigner/views/signing_tools_view.py @@ -28,15 +28,15 @@ class SigningToolsView(View): def display_transaction_information(self, p) -> None: self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = self.draw.textsize("Confirm Tx Details", font=View.IMPACT25) - self.draw.text(((240 - tw) / 2, 3), "Confirm Tx Details", fill=View.color, font=View.IMPACT25) + tw, th = self.draw.textsize("Confirm Tx Details", font=View.ASSISTANT25) + self.draw.text(((240 - tw) / 2, 3), "Confirm Tx Details", fill=View.color, font=View.ASSISTANT25) in_fee_outs_str = str(len(p.psbt.inputs)) in_fee_outs_str += " inputs - fee = " if len(p.psbt.inputs) > 1 else " input - fee = " in_fee_outs_str += str(len(p.psbt.outputs)) in_fee_outs_str += " outs" if len(p.psbt.outputs) > 1 else " out" - tw, th = self.draw.textsize(in_fee_outs_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 40), in_fee_outs_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(in_fee_outs_str, font=View.ASSISTANT22) + self.draw.text(((240 - tw) / 2, 40), in_fee_outs_str, fill=View.color, font=View.ASSISTANT22) receiving_addr_str1 = "" receiving_addr_str2 = "" @@ -49,41 +49,41 @@ class SigningToolsView(View): else: receiving_addr_str1 += "Self-Transfer" - tw, th = self.draw.textsize(receiving_addr_str1, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 75), receiving_addr_str1, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(receiving_addr_str1, font=View.ASSISTANT22) + self.draw.text(((240 - tw) / 2, 75), receiving_addr_str1, fill=View.color, font=View.ASSISTANT22) if len(receiving_addr_str2) > 0: - tw, th = self.draw.textsize(receiving_addr_str2, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 100), receiving_addr_str2, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(receiving_addr_str2, font=View.ASSISTANT22) + self.draw.text(((240 - tw) / 2, 100), receiving_addr_str2, fill=View.color, font=View.ASSISTANT22) if p.spend_amount > 0: spending_str = "Spend: " + str(p.spend_amount) + " sats" - tw, th = self.draw.textsize(spending_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 130), spending_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(spending_str, font=View.ASSISTANT22) + self.draw.text(((240 - tw) / 2, 130), spending_str, fill=View.color, font=View.ASSISTANT22) if p.change_amount > 0 and len(p.destination_addresses) == 0: change_str = "Amount: " + str(p.change_amount) + " sats" - tw, th = self.draw.textsize(change_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(change_str, font=View.ASSISTANT22) + self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.ASSISTANT22) elif p.change_amount > 0: change_str = "Change: " + str(p.change_amount) + " sats" - tw, th = self.draw.textsize(change_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(change_str, font=View.ASSISTANT22) + self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.ASSISTANT22) fee_str = "Fee: " + str(p.fee_amount) + " sats" - tw, th = self.draw.textsize(fee_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 180), fee_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(fee_str, font=View.ASSISTANT22) + self.draw.text(((240 - tw) / 2, 180), fee_str, fill=View.color, font=View.ASSISTANT22) - tw, th = self.draw.textsize("Left to Exit, Right to Continue", font=View.IMPACT18) - self.draw.text(((240 - tw) / 2, 215), "Left to Exit, Right to Continue", fill=View.color, font=View.IMPACT18) + tw, th = self.draw.textsize("Left to Exit, Right to Continue", font=View.ASSISTANT18) + self.draw.text(((240 - tw) / 2, 215), "Left to Exit, Right to Continue", fill=View.color, font=View.ASSISTANT18) View.DispShowImage() def qr_gen_status(percentage): View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) - tw, th = View.draw.textsize("QR Generation", font=View.IMPACT25) - View.draw.text(((240 - tw) / 2, 90), "QR Generation", fill=View.color, font=View.IMPACT25) - tw, th = View.draw.textsize(str(round(percentage)) + "% Complete", font=View.IMPACT25) - View.draw.text(((240 - tw) / 2, 125), str(round(percentage)) + "% Complete", fill=View.color, font=View.IMPACT25) + tw, th = View.draw.textsize("QR Generation", font=View.ASSISTANT25) + View.draw.text(((240 - tw) / 2, 90), "QR Generation", fill=View.color, font=View.ASSISTANT25) + tw, th = View.draw.textsize(str(round(percentage)) + "% Complete", font=View.ASSISTANT25) + View.draw.text(((240 - tw) / 2, 125), str(round(percentage)) + "% Complete", fill=View.color, font=View.ASSISTANT25) View.DispShowImage() diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index f11e4f30..8f0bf484 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -16,25 +16,35 @@ class View: WIDTH = 240 HEIGHT = 240 - - # Define necessary fonts - IMPACT16 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 16) - IMPACT18 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 18) - IMPACT20 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 20) - IMPACT21 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 21) - IMPACT22 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 22) - IMPACT23 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 23) - IMPACT25 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 25) - IMPACT26 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 26) - IMPACT35 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 35) - IMPACT50 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/Impact.ttf', 50) - COURIERNEW14 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 14) - COURIERNEW24 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 24) - COURIERNEW38 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 38) - COURIERNEW30 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 30) - COURIERNEW20 = ImageFont.truetype('/usr/share/fonts/truetype/msttcorefonts/courbd.ttf', 20) - + font_path = os.path.join(pathlib.Path(__file__).parent.resolve(), "..", "resources", "fonts") + + # Define necessary fonts + ASSISTANT16 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 16) + ASSISTANT18 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 18) + ASSISTANT20 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 20) + ASSISTANT21 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 21) + ASSISTANT22 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 22) + ASSISTANT23 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 23) + ASSISTANT25 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 25) + ASSISTANT26 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 26) + ASSISTANT35 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 35) + ASSISTANT50 = ImageFont.truetype(os.path.join(font_path, 'Assistant-Medium.ttf'), 50) + ASSISTANT16BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 15) + ASSISTANT18BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 17) + ASSISTANT20BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 19) + ASSISTANT21BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 20) + ASSISTANT22BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 21) + ASSISTANT23BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 22) + ASSISTANT25BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 25) + ASSISTANT26BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 26) + ASSISTANT35BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 35) + ASSISTANT50BOLD = ImageFont.truetype(os.path.join(font_path, 'Assistant-Bold.ttf'), 50) + ROBOTOMONO14 = ImageFont.truetype(os.path.join(font_path, 'RobotoMono-Medium.ttf'), 14) + ROBOTOMONO24 = ImageFont.truetype(os.path.join(font_path, 'RobotoMono-Medium.ttf'), 24) + ROBOTOMONO38 = ImageFont.truetype(os.path.join(font_path, 'RobotoMono-Medium.ttf'), 38) + ROBOTOMONO30 = ImageFont.truetype(os.path.join(font_path, 'RobotoMono-Medium.ttf'), 30) + ROBOTOMONO20 = ImageFont.truetype(os.path.join(font_path, 'RobotoMono-Medium.ttf'), 20) ROBOTOCONDENSED_BOLD_16 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 16) ROBOTOCONDENSED_BOLD_18 = ImageFont.truetype(os.path.join(font_path, "RobotoCondensed-Bold.ttf"), 18) @@ -146,37 +156,37 @@ class View: View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) if len(title) > 0: - tw, th = View.draw.textsize(title, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(title, font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.ASSISTANT22) if len(bottom) > 0: - tw, th = View.draw.textsize(bottom, font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(bottom, font=View.ASSISTANT18) + View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.ASSISTANT18) if len(lines) == 1: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT26) - View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT26) + tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT26) + View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ASSISTANT26) elif len(lines) == 2: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.ASSISTANT22) elif len(lines) == 3: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT26) - View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.IMPACT26) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[2], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT26) + View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.ASSISTANT26) + tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(lines[2], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ASSISTANT22) elif len(lines) == 4: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[2], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[3], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 55), lines[0], fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(lines[2], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(lines[3], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.ASSISTANT22) View.DispShowImage() @@ -192,46 +202,46 @@ class View: View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) if len(title) > 0: - tw, th = View.draw.textsize(title, font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(title, font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 2), title, fill=View.color, font=View.ASSISTANT22) if len(bottom) > 0: - tw, th = View.draw.textsize(bottom, font=View.IMPACT18) - View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.IMPACT18) + tw, th = View.draw.textsize(bottom, font=View.ASSISTANT18) + View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.ASSISTANT18) if len(lines) == 1: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT26) - View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT26) + tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT26) + View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ASSISTANT26) elif len(lines) == 2: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 90), lines[0], fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 125), lines[1], fill=View.color, font=View.ASSISTANT22) elif len(lines) == 3: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT26) - View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.IMPACT26) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[2], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT26) + View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.ASSISTANT26) + tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(lines[2], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ASSISTANT22) elif len(lines) == 4: - tw, th = View.draw.textsize(lines[0], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[1], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[2], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(lines[3], font=View.IMPACT22) - View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.IMPACT22) + tw, th = View.draw.textsize(lines[0], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 20), lines[0], fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(lines[1], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 90), lines[1], fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(lines[2], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 125), lines[2], fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(lines[3], font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 160), lines[3], fill=View.color, font=View.ASSISTANT22) - a_x_offset = 240 - View.IMPACT25.getsize(a_txt)[0] - View.draw.text((a_x_offset, 39 + 0), a_txt, fill=View.color, font=View.IMPACT25) + a_x_offset = 240 - View.ASSISTANT25.getsize(a_txt)[0] + View.draw.text((a_x_offset, 39 + 0), a_txt, fill=View.color, font=View.ASSISTANT25) - b_x_offset = 240 - View.IMPACT25.getsize(b_txt)[0] - View.draw.text((b_x_offset , 39 + 60), b_txt, fill=View.color, font=View.IMPACT25) + b_x_offset = 240 - View.ASSISTANT25.getsize(b_txt)[0] + View.draw.text((b_x_offset , 39 + 60), b_txt, fill=View.color, font=View.ASSISTANT25) - c_x_offset = 240 - View.IMPACT25.getsize(c_txt)[0] - View.draw.text((c_x_offset , 39 + 120), c_txt, fill=View.color, font=View.IMPACT25) + c_x_offset = 240 - View.ASSISTANT25.getsize(c_txt)[0] + View.draw.text((c_x_offset , 39 + 120), c_txt, fill=View.color, font=View.ASSISTANT25) View.DispShowImage() @@ -251,14 +261,14 @@ class View: line3 = "30 seconds before" line4 = "disconnecting power." - tw, th = View.draw.textsize(line1, font=View.IMPACT22) - View.draw.text(((240-tw)/2, 45), line1, fill=View.color, font=View.IMPACT22) - tw, th = View.draw.textsize(line2, font=View.IMPACT20) - View.draw.text(((240-tw)/2, 100), line2, fill=View.color, font=View.IMPACT20) - tw, th = View.draw.textsize(line3, font=View.IMPACT20) - View.draw.text(((240-tw)/2, 130), line3, fill=View.color, font=View.IMPACT20) - tw, th = View.draw.textsize(line4, font=View.IMPACT20) - View.draw.text(((240-tw)/2, 160), line4, fill=View.color, font=View.IMPACT20) + tw, th = View.draw.textsize(line1, font=View.ASSISTANT22) + View.draw.text(((240-tw)/2, 45), line1, fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(line2, font=View.ASSISTANT20) + View.draw.text(((240-tw)/2, 100), line2, fill=View.color, font=View.ASSISTANT20) + tw, th = View.draw.textsize(line3, font=View.ASSISTANT20) + View.draw.text(((240-tw)/2, 130), line3, fill=View.color, font=View.ASSISTANT20) + tw, th = View.draw.textsize(line4, font=View.ASSISTANT20) + View.draw.text(((240-tw)/2, 160), line4, fill=View.color, font=View.ASSISTANT20) View.DispShowImage() From 518290328850ced4c4bc8df2c6f59cd05b114050 Mon Sep 17 00:00:00 2001 From: newtonick Date: Fri, 20 Aug 2021 19:44:24 +0000 Subject: [PATCH 04/40] work in progress --- src/seedsigner/models/seed_storage.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py index 87f55273..ecec814c 100644 --- a/src/seedsigner/models/seed_storage.py +++ b/src/seedsigner/models/seed_storage.py @@ -128,16 +128,6 @@ class SeedStorage: count += 1 return count - - def get_first_seed_slot(self) -> int: - if len(self.saved_seeds[0]) > 0: - return 1 - elif len(self.saved_seeds[1]) > 0: - return 2 - elif len(self.saved_seeds[2]) > 0: - return 3 - else: - return None def num_of_free_slots(self) -> int: count = 0 From 8d8820dcf2cf36557c4ca07ccae110a785bd521c Mon Sep 17 00:00:00 2001 From: newtonick Date: Fri, 20 Aug 2021 19:46:53 +0000 Subject: [PATCH 05/40] still needs position information, work in progress --- src/seedsigner/models/seed_storage.py | 319 +++++++++++++++++--------- 1 file changed, 206 insertions(+), 113 deletions(-) diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py index ecec814c..356794df 100644 --- a/src/seedsigner/models/seed_storage.py +++ b/src/seedsigner/models/seed_storage.py @@ -1,141 +1,234 @@ # External Dependencies -from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes -from embit import bip39 +from embit import bip39, bip32 class SeedStorage: - SEEDWORDS = bip39.WORDLIST + SEEDWORDS = bip39.WORDLIST - def __init__(self) -> None: + def __init__(self) -> None: + self.seeds = [] + self.mnemonics = [] + self.passphrases = [] + + # to be removed once the concepts of slots is removed + # this is a list of 3 indexes to the actual seed/mnemonic/passphrase data + # -1 means the slot is empty + self.slots = [-1,-1,-1] + + # only adds a seed if it's valid and does not already exist + def add_seed(self, seed_phrase, passphrase: str = "", slot_num = -1) -> bool: + mnemonic = "" + if isinstance(seed_phrase, list): + mnemonic = " ".join(seed_phrase).strip() + elif isinstance(seed_phrase, str): + mnemonic = seed_phrase.strip() + else: + raise ValueError("Invalid mnemonic seed phrase format") + + if not isinstance(passphrase, str): + raise ValueError("Invalid passphrase format") - self.saved_seeds = [[],[],[]] - self.passphrase = ["","",""] + try: + seed = bip39.mnemonic_to_seed(mnemonic, passphrase, wordlist=SeedStorage.SEEDWORDS) + self.seeds.append(seed) + self.mnemonics.append(mnemonic) + self.passphrases.append(passphrase) + + # used to track virtual slot number, TODO: remove concept in UI of slots + if slot_num > 0: + self.slots[slot_num - 1] = len(self.seeds) - 1 - ### - ### Seed Related Methods - ### + return True + except Exception as e: + return False # return false if there is an unexpected exception + + def delete_seed(self, seed) -> bool: + if seed in self.seeds: + idx = self.seeds.index(seed) + self.seeds.pop(idx) + self.mnemonics.pop(idx) + self.passphrases.pop(idx) + self.slots[self.slots.index(idx)] = -1 + return True + else: + return False + + def add_passphrase_to_seed(self, seed, passphrase: str = ""): + if not isinstance(passphrase, str): + raise ValueError("Invalid passphrase format") + + # find existing seed to add passphrase to + if seed in self.seeds: + idx = self.seeds.index(seed) + new_seed = bip39.mnemonic_to_seed(self.mnemonics[idx], passphrase, wordlist=SeedStorage.SEEDWORDS) + self.seeds[idx] = new_seed + self.passphrases[idx] = passphrase + return True + else: + return False + + def remove_passphrase_from_seed(self, seed): + # find existing seed to add passphrase to + if seed in self.seeds: + idx = self.seeds.index(seed) + new_seed = bip39.mnemonic_to_seed(self.mnemonics[idx], "", wordlist=SeedStorage.SEEDWORDS) + self.seeds[idx] = new_seed + self.passphrases[idx] = "" + return True + else: + return False + + # validates mnemonic part of a seed in list or string format + def validate_mnemonic(self, seed_phrase) -> bool: + mnemonic = "" + if isinstance(seed_phrase, list): + mnemonic = " ".join(seed_phrase).strip() + elif isinstance(seed_phrase, str): + mnemonic = seed_phrase.strip() + else: + raise ValueError("Invalid mnemonic seed phrase format") + + return bip39.mnemonic_is_valid(mnemonic) - def save_seed_phrase(self, seed_phrase = [], slot_num = 0) -> bool: - slot_idx = slot_num - 1 - if slot_idx in (0,1,2): - self.saved_seeds[slot_idx] = seed_phrase[:] - self.passphrase[slot_idx] = "" - return True - else: - return False + def get_last_seed(self): + if len(self.seeds) >= 1: + return self.seeds[-1] + else: + return None - def save_passphrase(self, passphrase, slot_num = 0) -> bool: - slot_idx = slot_num - 1 - if slot_idx in (0,1,2): - self.passphrase[slot_idx] = passphrase - return True - else: - return False + def get_last_mnemonic(self): + if len(self.mnemonics) >= 1: + return self.mnemonics[-1] + else: + return None - def get_seed_phrase(self, slot_num) -> []: - slot_idx = slot_num - 1 - if len(self.saved_seeds[slot_idx]) > 0: - return self.saved_seeds[slot_idx] - else: - return [] + def get_last_passphrase(self): + if len(self.passphrases) >= 1: + return self.passphrases[-1] + else: + return None + + def seed_count(self): + return len(self.seeds) - def get_passphrase(self, slot_num) -> str: - slot_idx = slot_num - 1 - if len(self.passphrase[slot_idx]) > 0: - return self.passphrase[slot_idx] - else: - return "" + # + # Slot related methods to be removed once the concepts of slots is removed + # TODO: remove concept in UI of slots + # + + def get_mnemonic_from_slot(self, slot_num = 0): + if self.slots[slot_num - 1] == -1: + raise ValueError("Slot is unexpectedly empty") + + return self.mnemonics[self.slots[slot_num - 1]] - def delete_passphrase(self, slot_num) -> bool: - slot_idx = slot_num - 1 - self.passphrase[slot_idx] = "" + def get_passphrase_from_slot(self, slot_num = 0): + if self.slots[slot_num - 1] == -1: + raise ValueError("Slot is unexpectedly empty") + + return self.passphrases[self.slots[slot_num - 1]] + + def add_passphrase_to_slot(self, passphrase: str = "", slot_num = -1): + if self.slots[slot_num - 1] == -1: + raise ValueError("Slot is unexpectedly empty") + + return self.add_passphrase_to_seed(self.seeds[self.slots[slot_num - 1]], passphrase) - return True + def remove_passphrase_from_slot(self, slot_num = -1): + if self.slots[slot_num - 1] == -1: + raise ValueError("Slot is unexpectedly empty") + + return self.remove_passphrase_from_seed(self.seeds[self.slots[slot_num - 1]]) - def valid_seed_structure(self, seed_phrase) -> bool: - if isinstance(seed_phrase, list): - if len(seed_phrase) in (12,24): - if all(word in seed_phrase for word in SEEDWORDS): - return True - else: - return False - else: - return False # Required to be 12 or 24 word seed phrase - else: - return False # Not list, not valid seed_phrase + def get_first_seed_slot(self): + if self.slots[0] != -1: + return self.slots[0] + 1 + elif self.slots[1] != -1: + return self.slots[1] + 1 + elif self.slots[2] != -1: + return self.slots[2] + 1 - def check_slot_1(self) -> bool: - if len(self.saved_seeds[0]) > 0: - return True - else: - return False + def check_slot_1(self) -> bool: + if self.slots[0] != -1: + return True + else: + return False + + def check_slot_2(self) -> bool: + if self.slots[1] != -1: + return True + else: + return False + + def check_slot_3(self) -> bool: + if self.slots[2] != -1: + return True + else: + return False - def check_slot_2(self) -> bool: - if len(self.saved_seeds[1]) > 0: - return True - else: - return False + def check_slot(self, slot_num) -> bool: + if self.slots[slot_num-1] != -1: + return True + else: + return False - def check_slot_3(self) -> bool: - if len(self.saved_seeds[2]) > 0: - return True - else: - return False + def check_slot_passphrase(self, slot_num) -> bool: + if self.slots[slot_num-1] != -1: + if self.passphrases[self.slots[slot_num-1]] != "": + return True + return False + + def slot_avaliable(self) -> bool: + if self.slots[0] == -1 or self.slots[1] == -1 or self.slots[2] == -1: + return True + return True + + def num_of_saved_seeds(self) -> int: + count = 0 + if self.slots[0] != -1: + count += 1 + if self.slots[1] != -1: + count += 1 + if self.slots[2] != -1: + count += 1 - def check_slot(self, slot_num) -> bool: - if len(self.saved_seeds[slot_num-1]) > 0: - return True - else: - return False + return count - def check_slot_passphrase(self, slot_num) -> bool: - if len(self.passphrase[slot_num-1]) > 0: - return True - else: - return False + def num_of_free_slots(self) -> int: + count = 0 + if self.slots[0] == -1: + count += 1 + if self.slots[0] == -1: + count += 1 + if self.slots[0] == -1: + count += 1 - def check_if_seed_valid(self, seed_phrase) -> bool: - try: - bip39.mnemonic_to_seed((" ".join(seed_phrase)).strip()) - except ValueError: - return False + return count - return True + def save_seed_phrase(self, seed_phrase = [], slot_num = 0) -> bool: + return self.add_seed(seed_phrase,slot_num=slot_num) - def slot_avaliable(self) -> bool: - if len(self.saved_seeds[0]) > 0 and len(self.saved_seeds[1]) > 0 and len(self.saved_seeds[2]) > 0: - return False - return True + def save_passphrase(self, passphrase, slot_num = 0) -> bool: + return self.add_passphrase_to_slot(passphrase, slot_num=slot_num) - def num_of_saved_seeds(self) -> int: - count = 0 - if len(self.saved_seeds[0]) > 0: - count += 1 - if len(self.saved_seeds[1]) > 0: - count += 1 - if len(self.saved_seeds[2]) > 0: - count += 1 + def get_seed_phrase(self, slot_num) -> []: + if self.slots[slot_num - 1] == -1: + return [] - return count + return self.mnemonics[self.slots[slot_num - 1]].split() - def num_of_passphrase_seeds(self) -> int: - count = 0 - if len(self.saved_seeds[0]) > 0 and len(self.passphrase[0]) > 0: - count += 1 - if len(self.saved_seeds[1]) > 0 and len(self.passphrase[1]) > 0: - count += 1 - if len(self.saved_seeds[2]) > 0 and len(self.passphrase[2]) > 0: - count += 1 + def get_passphrase(self, slot_num) -> str: + if self.slots[slot_num - 1] == -1: + return "" - return count + return self.passphrases[self.slots[slot_num - 1]] - def num_of_free_slots(self) -> int: - count = 0 - if len(self.saved_seeds[0]) == 0: - count += 1 - if len(self.saved_seeds[1]) == 0: - count += 1 - if len(self.saved_seeds[2]) == 0: - count += 1 + def delete_passphrase(self, slot_num) -> bool: + if self.slots[slot_num - 1] == -1: + return False - return count \ No newline at end of file + return self.remove_passphrase_from_seed(self.seeds[self.slots[slot_num - 1]]) + + def check_if_seed_valid(self, seed_phrase) -> bool: + return self.validate_mnemonic(seed_phrase) + \ No newline at end of file From 8247d316079bd00bd93f4881e5f50718c0cb35b4 Mon Sep 17 00:00:00 2001 From: newtonick Date: Fri, 20 Aug 2021 20:52:35 +0000 Subject: [PATCH 06/40] add 0.4.5 to controller version --- src/seedsigner/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index cd9d766d..de0c5aa4 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -32,7 +32,7 @@ class Controller(Singleton): Note: In many/most cases you'll need to do the Controller import within a method rather than at the top in order avoid circular imports. """ - VERSION = "0.4.4" + VERSION = "0.4.5" @classmethod From ca3082dd6f7d9341203dddbd1790b25e649c0a89 Mon Sep 17 00:00:00 2001 From: newtonick Date: Fri, 20 Aug 2021 21:02:38 +0000 Subject: [PATCH 07/40] replace impact with anton --- src/seedsigner/controller.py | 2 +- src/seedsigner/views/seed_tools_view.py | 4 ++-- src/seedsigner/views/signing_tools_view.py | 20 ++++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index d23b37f0..2547cb8a 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -525,7 +525,7 @@ class Controller(Singleton): if frame is not None: if decoder.getPercentComplete() > 0 and decoder.isPSBT(): scan_text = str(decoder.getPercentComplete()) + "% Complete" - View.DispShowImageWithText(frame.resize((240,240)), scan_text, font=View.IMPACT22, text_color=View.color, text_background=(0,0,0,225)) + View.DispShowImageWithText(frame.resize((240,240)), scan_text, font=View.ANTON22, text_color=View.color, text_background=(0,0,0,225)) time.sleep(0.1) # turn this up or down to tune performace while decoding psbt if camera._video_stream is None: break diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 11190c82..abbedcbe 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -1022,7 +1022,7 @@ class SeedToolsView(View): def seed_phrase_as_qr(self, seed_phrase): e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEEDSSQR) image = e.nextPartImage(240, 240, 3) - View.DispShowImageWithText(image, "click to zoom, right to exit", font=View.IMPACT18, text_color="BLACK", text_background="ORANGE") + View.DispShowImageWithText(image, "click to zoom, right to exit", font=View.ANTON18, text_color="BLACK", text_background="ORANGE") input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_PRESS]) if input == B.KEY_RIGHT: @@ -1158,7 +1158,7 @@ class SeedToolsView(View): while True: frame = self.controller.camera.read_video_stream(as_image=True) if frame is not None: - View.DispShowImageWithText(frame.resize((240,240)), "Scan Seed QR", font=View.IMPACT22, text_color=View.color, text_background=(0,0,0,225)) + View.DispShowImageWithText(frame.resize((240,240)), "Scan Seed QR", font=View.ANTON22, text_color=View.color, text_background=(0,0,0,225)) status = decoder.addImage(frame) if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID): diff --git a/src/seedsigner/views/signing_tools_view.py b/src/seedsigner/views/signing_tools_view.py index c4d849b2..c549a972 100644 --- a/src/seedsigner/views/signing_tools_view.py +++ b/src/seedsigner/views/signing_tools_view.py @@ -34,8 +34,8 @@ class SigningToolsView(View): in_fee_outs_str += " inputs - fee = " if len(p.psbt.inputs) > 1 else " input - fee = " in_fee_outs_str += str(len(p.psbt.outputs)) in_fee_outs_str += " outs" if len(p.psbt.outputs) > 1 else " out" - tw, th = self.draw.textsize(in_fee_outs_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 40), in_fee_outs_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(in_fee_outs_str, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 40), in_fee_outs_str, fill=View.color, font=View.ANTON22) receiving_addr_str1 = "" receiving_addr_str2 = "" @@ -56,21 +56,21 @@ class SigningToolsView(View): if p.spend_amount > 0: spending_str = "Spend: " + str(p.spend_amount) + " sats" - tw, th = self.draw.textsize(spending_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 130), spending_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(spending_str, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 130), spending_str, fill=View.color, font=View.ANTON22) if p.change_amount > 0 and len(p.destination_addresses) == 0: change_str = "Amount: " + str(p.change_amount) + " sats" - tw, th = self.draw.textsize(change_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(change_str, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.ANTON22) elif p.change_amount > 0: change_str = "Change: " + str(p.change_amount) + " sats" - tw, th = self.draw.textsize(change_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(change_str, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 155), change_str, fill=View.color, font=View.ANTON22) fee_str = "Fee: " + str(p.fee_amount) + " sats" - tw, th = self.draw.textsize(fee_str, font=View.IMPACT22) - self.draw.text(((240 - tw) / 2, 180), fee_str, fill=View.color, font=View.IMPACT22) + tw, th = self.draw.textsize(fee_str, font=View.ANTON22) + self.draw.text(((240 - tw) / 2, 180), fee_str, fill=View.color, font=View.ANTON22) tw, th = self.draw.textsize("Left to Exit, Right to Continue", font=View.ANTON18) self.draw.text(((240 - tw) / 2, 215), "Left to Exit, Right to Continue", fill=View.color, font=View.ANTON18) From dfffeb30e9d9c3395be5acb073d916d30b7df16c Mon Sep 17 00:00:00 2001 From: newtonick Date: Tue, 24 Aug 2021 17:40:17 +0000 Subject: [PATCH 08/40] add qr formats doc --- docs/qr_formats.md | 34 ++++++++++++++++++++++++++++++++++ docs/recovery.md | 14 ++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 docs/recovery.md diff --git a/docs/qr_formats.md b/docs/qr_formats.md index e69de29b..549bd7a7 100644 --- a/docs/qr_formats.md +++ b/docs/qr_formats.md @@ -0,0 +1,34 @@ +# QR Formats + +### Scanning QR Codes + +SeedSigner supports scanning the following QR formats + +Animated QR Formats: +- PSBT + - Blockchain Commons [UR2 PSBT](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-006-urtypes.md) + - Specter Desktop base64 segments animated + - [Legacy UR](https://github.com/BlockchainCommons/Research/blob/d4d72417a1ff18f9422371b2f71bf2652adce41c/papers/bcr-2020-005-ur.md) Format that should no longer be used + +Static QR Formats: +- PSBT + - Base64 (if the PSBT byte size is too large, SS may have trouble scanning) +- Seed + - SeedSigner Seed QR format + - A 48 or 96 length string of numbers representing a bip39 wordlist (all wordlist languages supported). The numeric sequence is a concatination of four-digit zero padded segments. Each four-digit segment represents a bip39 word expressed by a zero-indexed position in the wordlist. For example "0000" is abandon in the english bip39 wordlist. + - English Bip39 Mnemonic words separated by a space. Currently only supports 12 and 24 word seeds. + - English Bip39 Mnemonic with only first 4 letters seperated by a space. Currently only supports 12 and 24 word seeds. + +### Displaying QR Codes + +SeedSigner supports displaying QR's in the following formats + +Animated QR Formats: +- PSBT + - Blockchain Commons [UR2 PSBT](https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-006-urtypes.md) + - Specter Desktop base64 segments animated + +Static QR Formats: +- Seed + - SeedSigner Seed QR format + - A 48 or 96 length string of numbers representing a bip39 wordlist (all wordlist languages supported). The numeric sequence is a concatination of four-digit zero padded segments. Each four-digit segment represents a bip39 word expressed by a zero-indexed position in the wordlist. For example "0000" is abandon in the english bip39 wordlist. \ No newline at end of file diff --git a/docs/recovery.md b/docs/recovery.md new file mode 100644 index 00000000..9128b044 --- /dev/null +++ b/docs/recovery.md @@ -0,0 +1,14 @@ +# SeedSigner Recovery Information + +SeedSigner creates extended public keys for the coordinator following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) to encode the extended public key. SeedSigner only supports native segwit for single sig (P2WPKH) and native segwit multisig (P2WSH). + +SeedSigners default settings for "Script Policy" (2021-08-21) is Multi Sig Native Segwit. The second option is Single Sig Native Segwit. + +- Single Sig Derivation Path: m/48'/0'/0'/2' +- Single Sig Public Key Encoding: 0x04b24746 - zpub +- Multisig Dervivation Path: m/84'/0'/0' +- Multisig Public Key Encoding: 0x02aa7ed3 - Zpub + +Changing the network settings from main to test will change the public key encoding and derivation path following bip-0084, bip-0048 and slip-0132 standards. + +Custom derivation paths will be optional in the future. From cdf532f3aafc0518b2eee041710a137d74beee95 Mon Sep 17 00:00:00 2001 From: newtonick Date: Tue, 31 Aug 2021 02:15:03 +0000 Subject: [PATCH 09/40] work in progress --- src/seedsigner/controller.py | 100 ++++++++++----------- src/seedsigner/models/__init__.py | 1 + src/seedsigner/models/seed.py | 77 ++++++++++++++++ src/seedsigner/models/seed_storage.py | 115 +++++++----------------- src/seedsigner/views/seed_tools_view.py | 3 +- 5 files changed, 161 insertions(+), 135 deletions(-) create mode 100644 src/seedsigner/models/seed.py diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 29944f6e..97463479 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -13,7 +13,7 @@ from .views import (View, MenuView, SeedToolsView,SigningToolsView, SettingsToolsView, IOTestView) from .helpers import Buttons, B, Path, Singleton from .models import (SeedStorage, Settings, DecodeQR, DecodeQRStatus, - EncodeQRDensity, EncodeQR, PSBTParser, QRType) + EncodeQRDensity, EncodeQR, PSBTParser, QRType, Seed) class Controller(Singleton): """ @@ -174,26 +174,26 @@ class Controller(Singleton): ### Generate Last Word 12 / 24 Menu def show_generate_last_word_tool(self) -> int: - seed_phrase = [] + seed = Seed() ret_val = 0 while True: # display menu to select 12 or 24 word seed for last word ret_val = self.menu_view.display_12_24_word_menu("... [ Return to Seed Tools ]") if ret_val == Path.SEED_WORD_12: - seed_phrase = self.seed_tools_view.display_manual_seed_entry(11) + seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(11) elif ret_val == Path.SEED_WORD_24: - seed_phrase = self.seed_tools_view.display_manual_seed_entry(23) + seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(23) else: return Path.SEED_TOOLS_SUB_MENU - if len(seed_phrase) > 0: - completed_seed_phrase = self.seed_tools_view.display_last_word(seed_phrase) + if len(seed.mnemonic_list) > 0: + seed.mnemonic = self.seed_tools_view.display_last_word(seed.mnemonic_list) break # display seed phrase while True: - ret_val = self.seed_tools_view.display_seed_phrase(completed_seed_phrase, show_qr_option=True) + ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic, show_qr_option=True) if ret_val == True: break else: @@ -206,7 +206,7 @@ class Controller(Singleton): 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.save_seed_phrase(completed_seed_phrase, slot_num) + self.storage.add_seed(seed, slot_num) self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") input = self.buttons.wait_for([B.KEY_RIGHT]) @@ -215,19 +215,19 @@ class Controller(Singleton): ### Create a Seed w/ Dice Screen def show_create_seed_with_dice_tool(self) -> int: - seed_phrase = [] + seed = Seed() ret_val = True while True: - seed_phrase = self.seed_tools_view.display_generate_seed_from_dice() - if len(seed_phrase) > 0: + seed.mnemonic = self.seed_tools_view.display_generate_seed_from_dice() + if seed: break else: return Path.SEED_TOOLS_SUB_MENU # display seed phrase (24 words) while True: - ret_val = self.seed_tools_view.display_seed_phrase(seed_phrase, show_qr_option=True) + ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=True) if ret_val == True: break else: @@ -240,30 +240,32 @@ class Controller(Singleton): 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.save_seed_phrase(seed_phrase, slot_num) + self.storage.add_seed(seed, slot_num) self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU + ### Create a Seed w/ Image + def show_create_seed_with_image_tool(self) -> int: - seed_phrase = [] + seed = Seed() ret_val = True while True: - (reshoot, seed_phrase) = self.seed_tools_view.seed_phrase_from_camera_image() + (reshoot, seed.mnemonic) = self.seed_tools_view.seed_phrase_from_camera_image() if reshoot: # Relaunch into another image capture cycle continue - if len(seed_phrase) > 0: + if seed: break else: return Path.SEED_TOOLS_SUB_MENU # display seed phrase (24 words) while True: - ret_val = self.seed_tools_view.display_seed_phrase(seed_phrase, show_qr_option=True) + ret_val = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=True) if ret_val == True: break else: @@ -276,15 +278,16 @@ class Controller(Singleton): 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.save_seed_phrase(seed_phrase, slot_num) + self.storage.add_seed(seed, slot_num) self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Main Menu") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU + ### Store a seed (temp) Menu def show_store_a_seed_tool(self): - seed_phrase = [] + seed = Seed() ret_val = 0 display_saved_seed = False ret_val = self.menu_view.display_saved_seed_menu(self.storage, 1, "... [ Return to Seed Tools ]") @@ -309,26 +312,25 @@ class Controller(Singleton): # 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 Seed Tools ]") if ret_val == Path.SEED_WORD_12: - seed_phrase = self.seed_tools_view.display_manual_seed_entry(12) + seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12) elif ret_val == Path.SEED_WORD_24: - seed_phrase = self.seed_tools_view.display_manual_seed_entry(24) + seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24) elif ret_val == Path.SEED_WORD_QR: - seed_phrase = self.seed_tools_view.read_seed_phrase_qr() + seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr() else: return Path.SEED_TOOLS_SUB_MENU - if len(seed_phrase) == 0: + if not seed: + # seed is not valid return Path.SEED_TOOLS_SUB_MENU - if ret_val == Path.SEED_WORD_QR and len(seed_phrase) > 0: + if ret_val == Path.SEED_WORD_QR and seed: show_qr_option = False else: show_qr_option = True - self.menu_view.draw_modal(["Validating ..."]) - is_valid = self.storage.check_if_seed_valid(seed_phrase) - if is_valid == False: - # Exit if not valid with message + if not seed: + # Seed is not valid, Exit if not valid with message 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.SEED_TOOLS_SUB_MENU @@ -337,15 +339,15 @@ class Controller(Singleton): input = self.buttons.wait_for([B.KEY_RIGHT]) while display_saved_seed == False: - r = self.seed_tools_view.display_seed_phrase(seed_phrase, show_qr_option=show_qr_option ) + r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, show_qr_option=show_qr_option ) if r == True: break else: # no-op; can't back out of the seed phrase view pass - if is_valid: - self.storage.save_seed_phrase(seed_phrase, slot_num) + if seed: + self.storage.add_seed(seed, slot_num) self.menu_view.draw_modal(["", "Saved to Slot #" + str(slot_num)], "", "Right to Exit") input = self.buttons.wait_for([B.KEY_RIGHT]) @@ -408,8 +410,7 @@ class Controller(Singleton): ### Generate XPUB def show_generate_xpub(self): - seed_phrase = [] - passphrase = "" + seed = Seed() # If there is a saved seed, ask to use saved seed if self.storage.num_of_saved_seeds() > 0: @@ -418,29 +419,26 @@ class Controller(Singleton): slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None) if slot_num not in (1,2,3): return Path.SEED_TOOLS_SUB_MENU - seed_phrase = self.storage.get_seed_phrase(slot_num) - passphrase = self.storage.get_passphrase(slot_num) + seed = self.storage.get_seed(slot_num) - if len(seed_phrase) == 0: - # gather seed phrase + if not seed: + # no valid seed, 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 Sign Tools ]") if ret_val == Path.SEED_WORD_12: - seed_phrase = self.seed_tools_view.display_manual_seed_entry(12) + seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12) elif ret_val == Path.SEED_WORD_24: - seed_phrase = self.seed_tools_view.display_manual_seed_entry(24) + seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24) elif ret_val == Path.SEED_WORD_QR: - seed_phrase = self.seed_tools_view.read_seed_phrase_qr() + seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr() else: return Path.SEED_TOOLS_SUB_MENU - if len(seed_phrase) == 0: + if not seed: return Path.SEED_TOOLS_SUB_MENU # check if seed phrase is valid - self.menu_view.draw_modal(["Validating ..."]) - is_valid = self.storage.check_if_seed_valid(seed_phrase) - if is_valid == False: + 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 @@ -448,20 +446,19 @@ class Controller(Singleton): 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 - passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry() - if len(passphrase) == 0 or passphrase == "-1": - 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", passphrase], "", "Right to Continue") + 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_phrase, passphrase, "Right to Continue") + r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue") if r == True: break else: @@ -470,8 +467,7 @@ class Controller(Singleton): self.signing_tools_view.draw_modal(["Loading xPub Info ..."]) - seed = bip39.mnemonic_to_seed((" ".join(seed_phrase)).strip(), passphrase) - root = bip32.HDKey.from_seed(seed, version=NETWORKS[self.settings.network]["xprv"]) + root = bip32.HDKey.from_seed(seed.seed, version=NETWORKS[self.settings.network]["xprv"]) fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8') bip48_xprv = root.derive(self.settings.derivation) bip48_xpub = bip48_xprv.to_public() @@ -491,7 +487,7 @@ class Controller(Singleton): qr_xpub_type = self.settings.qr_xpub_type self.signing_tools_view.draw_modal(["Generating xPub QR ..."]) - e = EncodeQR(seed_phrase=seed_phrase, passphrase=passphrase, derivation=self.settings.derivation, network=self.settings.network, policy=self.settings.script_policy, qr_type=qr_xpub_type, qr_density=self.settings.qr_density) + e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=self.settings.derivation, network=self.settings.network, policy=self.settings.script_policy, qr_type=qr_xpub_type, qr_density=self.settings.qr_density) while e.totalParts() > 1: image = e.nextPartImage(240,240,2) diff --git a/src/seedsigner/models/__init__.py b/src/seedsigner/models/__init__.py index f9a1945f..6ef31222 100644 --- a/src/seedsigner/models/__init__.py +++ b/src/seedsigner/models/__init__.py @@ -2,5 +2,6 @@ from .qr_type import * from .decode_qr import * from .encode_qr import * from .psbt_parser import * +from .seed import * from .seed_storage import * from .settings import * \ No newline at end of file diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py new file mode 100644 index 00000000..4b99a57b --- /dev/null +++ b/src/seedsigner/models/seed.py @@ -0,0 +1,77 @@ +# External Dependencies +from embit import bip39, bip32 +import unicodedata + +class Seed: + + def __init__(self, mnemonic = None, passphrase = "", wordlist=None) -> None: + self._init_complete = False + self._valid = False + self.passphrase = passphrase + self.mnemonic = mnemonic + if wordlist == None: + self._wordlist = bip39.WORDLIST + else: + self._wordlist = wordlist + + self._valid = self._generate_seed() + self._init_complete = True + + def _generate_seed(self) -> bool: + self.seed = None + try: + self.seed = bip39.mnemonic_to_seed(self.mnemonic, password=self.passphrase, wordlist=self._wordlist) + return True + except Exception as e: + return False + + + ### getters and setters + + @property + def mnemonic(self): + return self._mnemonic + + @property + def mnemonic_list(self): + return self._mnemonic.split() + + @property + def mnemonic_display(self): + return unicodedata.normalize("NFC", self._mnemonic) + + @mnemonic.setter + def mnemonic(self, value): + if isinstance(value, list): + self._mnemonic = unicodedata.normalize("NFKD", " ".join(value).strip()) + elif isinstance(value, str): + self._mnemonic = unicodedata.normalize("NFKD", value.strip()) + + if self._init_complete: + self._valid = self._generate_seed() + + @property + def passphrase(self): + return self._passphrase + + @property + def passphrase_display(self): + return unicodedata.normalize("NFC", self._passphrase) + + @passphrase.setter + def passphrase(self, value): + if isinstance(value, str): + self._passphrase = unicodedata.normalize("NFKD", value) + + if self._init_complete: + self._valid = self._generate_seed() + + ### override operators + + def __eq__(self, other): + if isinstance(other, Seed): + return self.seed == other.seed + return False + + def __bool__(self): + return self._valid \ No newline at end of file diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py index 356794df..68c093a0 100644 --- a/src/seedsigner/models/seed_storage.py +++ b/src/seedsigner/models/seed_storage.py @@ -1,5 +1,6 @@ # External Dependencies -from embit import bip39, bip32 +from embit import bip39 +from seedsigner.models import Seed class SeedStorage: @@ -7,8 +8,6 @@ class SeedStorage: def __init__(self) -> None: self.seeds = [] - self.mnemonics = [] - self.passphrases = [] # to be removed once the concepts of slots is removed # this is a list of 3 indexes to the actual seed/mnemonic/passphrase data @@ -16,42 +15,19 @@ class SeedStorage: self.slots = [-1,-1,-1] # only adds a seed if it's valid and does not already exist - def add_seed(self, seed_phrase, passphrase: str = "", slot_num = -1) -> bool: - mnemonic = "" - if isinstance(seed_phrase, list): - mnemonic = " ".join(seed_phrase).strip() - elif isinstance(seed_phrase, str): - mnemonic = seed_phrase.strip() - else: - raise ValueError("Invalid mnemonic seed phrase format") - - if not isinstance(passphrase, str): - raise ValueError("Invalid passphrase format") + def add_mnemonic(self, mnemonic, passphrase: str = "", slot_num = -1) -> bool: + self.seeds.append(Seed(mnemonic, passphrase)) - try: - seed = bip39.mnemonic_to_seed(mnemonic, passphrase, wordlist=SeedStorage.SEEDWORDS) - self.seeds.append(seed) - self.mnemonics.append(mnemonic) - self.passphrases.append(passphrase) + # used to track virtual slot number, TODO: remove concept in UI of slots + if slot_num > 0: + self.slots[slot_num - 1] = len(self.seeds) - 1 - # used to track virtual slot number, TODO: remove concept in UI of slots - if slot_num > 0: - self.slots[slot_num - 1] = len(self.seeds) - 1 - - return True - except Exception as e: - return False # return false if there is an unexpected exception - - def delete_seed(self, seed) -> bool: - if seed in self.seeds: - idx = self.seeds.index(seed) - self.seeds.pop(idx) - self.mnemonics.pop(idx) - self.passphrases.pop(idx) - self.slots[self.slots.index(idx)] = -1 - return True - else: - return False + def add_seed(self, seed: Seed, slot_num = -1): + self.seeds.append(seed) + + # used to track virtual slot number, TODO: remove concept in UI of slots + if slot_num > 0: + self.slots[slot_num - 1] = len(self.seeds) - 1 def add_passphrase_to_seed(self, seed, passphrase: str = ""): if not isinstance(passphrase, str): @@ -60,53 +36,22 @@ class SeedStorage: # find existing seed to add passphrase to if seed in self.seeds: idx = self.seeds.index(seed) - new_seed = bip39.mnemonic_to_seed(self.mnemonics[idx], passphrase, wordlist=SeedStorage.SEEDWORDS) - self.seeds[idx] = new_seed - self.passphrases[idx] = passphrase + seed.passphrase = passphrase return True else: return False def remove_passphrase_from_seed(self, seed): - # find existing seed to add passphrase to - if seed in self.seeds: - idx = self.seeds.index(seed) - new_seed = bip39.mnemonic_to_seed(self.mnemonics[idx], "", wordlist=SeedStorage.SEEDWORDS) - self.seeds[idx] = new_seed - self.passphrases[idx] = "" - return True - else: - return False + return self.add_passphrase_to_seed(seed, "") # validates mnemonic part of a seed in list or string format def validate_mnemonic(self, seed_phrase) -> bool: - mnemonic = "" - if isinstance(seed_phrase, list): - mnemonic = " ".join(seed_phrase).strip() - elif isinstance(seed_phrase, str): - mnemonic = seed_phrase.strip() - else: - raise ValueError("Invalid mnemonic seed phrase format") - - return bip39.mnemonic_is_valid(mnemonic) - - def get_last_seed(self): - if len(self.seeds) >= 1: - return self.seeds[-1] - else: - return None - - def get_last_mnemonic(self): - if len(self.mnemonics) >= 1: - return self.mnemonics[-1] - else: - return None - - def get_last_passphrase(self): - if len(self.passphrases) >= 1: - return self.passphrases[-1] - else: - return None + try: + seed = Seed(seed_phrase) + except Exception as e: + return False + + return True def seed_count(self): return len(self.seeds) @@ -120,13 +65,13 @@ class SeedStorage: if self.slots[slot_num - 1] == -1: raise ValueError("Slot is unexpectedly empty") - return self.mnemonics[self.slots[slot_num - 1]] + return self.seeds[self.slots[slot_num - 1]].mnemonic def get_passphrase_from_slot(self, slot_num = 0): if self.slots[slot_num - 1] == -1: raise ValueError("Slot is unexpectedly empty") - return self.passphrases[self.slots[slot_num - 1]] + return self.seeds[self.slots[slot_num - 1]].passphrase def add_passphrase_to_slot(self, passphrase: str = "", slot_num = -1): if self.slots[slot_num - 1] == -1: @@ -174,7 +119,7 @@ class SeedStorage: def check_slot_passphrase(self, slot_num) -> bool: if self.slots[slot_num-1] != -1: - if self.passphrases[self.slots[slot_num-1]] != "": + if self.seeds[self.slots[slot_num-1]].passphrase != "": return True return False @@ -206,7 +151,7 @@ class SeedStorage: return count def save_seed_phrase(self, seed_phrase = [], slot_num = 0) -> bool: - return self.add_seed(seed_phrase,slot_num=slot_num) + return self.add_mnemonic(seed_phrase,slot_num=slot_num) def save_passphrase(self, passphrase, slot_num = 0) -> bool: return self.add_passphrase_to_slot(passphrase, slot_num=slot_num) @@ -215,13 +160,19 @@ class SeedStorage: if self.slots[slot_num - 1] == -1: return [] - return self.mnemonics[self.slots[slot_num - 1]].split() + return self.seeds[self.slots[slot_num - 1]].mnemonic.split() def get_passphrase(self, slot_num) -> str: if self.slots[slot_num - 1] == -1: return "" - return self.passphrases[self.slots[slot_num - 1]] + return self.seeds[self.slots[slot_num - 1]].passphrase + + def get_seed(self, slot_num) -> Seed: + if self.slots[slot_num - 1] == -1: + return Seed() + + return self.seeds[self.slots[slot_num - 1]] def delete_passphrase(self, slot_num) -> bool: if self.slots[slot_num - 1] == -1: diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 29873f98..536ef49f 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -12,11 +12,12 @@ import time from . import View from seedsigner.helpers import B, QR, Keyboard, TextEntryDisplay, mnemonic_generation from seedsigner.models import DecodeQR, DecodeQRStatus, QRType, EncodeQR +from seedsigner.models import SeedStorage class SeedToolsView(View): - SEEDWORDS = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo"] + SEEDWORDS = SeedStorage.SEEDWORDS ALPHABET = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] From f9fc829920e024364bda0d0b50a0b82f5e866e97 Mon Sep 17 00:00:00 2001 From: newtonick Date: Tue, 31 Aug 2021 16:49:11 +0000 Subject: [PATCH 10/40] work in progress --- src/seedsigner/controller.py | 60 +++++++++++-------------- src/seedsigner/models/__init__.py | 4 +- src/seedsigner/models/seed.py | 4 +- src/seedsigner/models/seed_storage.py | 6 +-- src/seedsigner/models/settings.py | 8 +++- src/seedsigner/views/seed_tools_view.py | 7 +-- 6 files changed, 41 insertions(+), 48 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 97463479..7a212a24 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -506,8 +506,7 @@ class Controller(Singleton): ### Sign Transactions def show_sign_transaction(self): - seed_phrase = [] - passphrase = "" + seed = Seed() used_saved_seed = False # reusable qr scan function @@ -563,10 +562,9 @@ class Controller(Singleton): elif decoder.isComplete() and decoder.isSeed(): # first QR is Seed self.menu_view.draw_modal(["Validating Seed"]) - seed_phrase = decoder.getSeedPhrase() - is_valid = self.storage.check_if_seed_valid(seed_phrase) - if is_valid == False: - # Exit if not valid with message + seed.mnemonic = decoder.getSeedPhrase() + if not seed: + # seed is not valid, Exit if not valid with message 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 @@ -577,9 +575,8 @@ class Controller(Singleton): 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 - passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry() - if len(passphrase) == 0 or passphrase == "-1": - 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: @@ -594,14 +591,13 @@ class Controller(Singleton): 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.save_seed_phrase(seed_phrase, slot_num) - self.storage.save_passphrase(passphrase, slot_num) + 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]) # display seed phrase while True: - r = self.seed_tools_view.display_seed_phrase(seed_phrase, passphrase, "Right to Continue") + r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue") if r == True: break else: @@ -629,62 +625,57 @@ class Controller(Singleton): else: return Path.MAIN_MENU - - if len(seed_phrase) == 0: - - # If there is a saved seed, ask to use saved seed + if not 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_phrase = self.storage.get_seed_phrase(slot_num) - passphrase = self.storage.get_passphrase(slot_num) + seed = self.storage.get_seed(slot_num) used_saved_seed = True - if len(seed_phrase) == 0: - # gather seed phrase + 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_phrase = self.seed_tools_view.display_manual_seed_entry(12) + seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12) elif ret_val == Path.SEED_WORD_24: - seed_phrase = self.seed_tools_view.display_manual_seed_entry(24) + seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24) elif ret_val == Path.SEED_WORD_QR: - seed_phrase = self.seed_tools_view.read_seed_phrase_qr() + seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr() else: return Path.MAIN_MENU - if len(seed_phrase) == 0: + if not seed: return Path.MAIN_MENU # check if seed phrase is valid self.menu_view.draw_modal(["Validating Seed ..."]) - is_valid = self.storage.check_if_seed_valid(seed_phrase) - if is_valid == False: + 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(passphrase) == 0: + 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 - passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry() - if len(passphrase) == 0 or passphrase == "-1": - 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", passphrase], "", "Right to Continue") + 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_phrase, passphrase, "Right to Continue") + r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue") if r == True: break else: @@ -697,14 +688,13 @@ class Controller(Singleton): 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.save_seed_phrase(seed_phrase, slot_num) - self.storage.save_passphrase(passphrase, slot_num) + 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]) # show transaction information before sign self.menu_view.draw_modal(["Parsing PSBT"]) - p = PSBTParser(psbt,seed_phrase,passphrase,self.settings.network) + p = PSBTParser(psbt,seed.mnemonic_list,seed.passphrase,self.settings.network) self.signing_tools_view.display_transaction_information(p) input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT], False) if input == B.KEY_LEFT: diff --git a/src/seedsigner/models/__init__.py b/src/seedsigner/models/__init__.py index 6ef31222..47e5e70c 100644 --- a/src/seedsigner/models/__init__.py +++ b/src/seedsigner/models/__init__.py @@ -1,7 +1,7 @@ +from .settings import * from .qr_type import * from .decode_qr import * from .encode_qr import * from .psbt_parser import * from .seed import * -from .seed_storage import * -from .settings import * \ No newline at end of file +from .seed_storage import * \ No newline at end of file diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 4b99a57b..180f6f81 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -2,6 +2,8 @@ from embit import bip39, bip32 import unicodedata +from seedsigner.models import Settings + class Seed: def __init__(self, mnemonic = None, passphrase = "", wordlist=None) -> None: @@ -10,7 +12,7 @@ class Seed: self.passphrase = passphrase self.mnemonic = mnemonic if wordlist == None: - self._wordlist = bip39.WORDLIST + self._wordlist = Settings.get_instance().wordlist else: self._wordlist = wordlist diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py index 68c093a0..b35ad9f4 100644 --- a/src/seedsigner/models/seed_storage.py +++ b/src/seedsigner/models/seed_storage.py @@ -4,8 +4,6 @@ from seedsigner.models import Seed class SeedStorage: - SEEDWORDS = bip39.WORDLIST - def __init__(self) -> None: self.seeds = [] @@ -143,9 +141,9 @@ class SeedStorage: count = 0 if self.slots[0] == -1: count += 1 - if self.slots[0] == -1: + if self.slots[1] == -1: count += 1 - if self.slots[0] == -1: + if self.slots[2] == -1: count += 1 return count diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index b15f54b4..9aaae891 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -3,6 +3,7 @@ from .encode_qr import EncodeQRDensity from .qr_type import QRType import configparser +from embit import bip39 class Settings(Singleton): @@ -21,7 +22,8 @@ class Settings(Singleton): 'system': { 'debug': False, 'default_language': "en", - 'persistent_settings': False + 'persistent_settings': False, + 'wordlist': bip39.WORDLIST }, 'display': { 'text_color': "ORANGE", @@ -116,6 +118,10 @@ class Settings(Singleton): @property def language(self): return self._data["system"]["default_language"] + + @property + def wordlist(self): + return self._data["system"]["wordlist"] ### display diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 536ef49f..d7a67fcf 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -11,14 +11,11 @@ import time # Internal file class dependencies from . import View from seedsigner.helpers import B, QR, Keyboard, TextEntryDisplay, mnemonic_generation -from seedsigner.models import DecodeQR, DecodeQRStatus, QRType, EncodeQR +from seedsigner.models import DecodeQR, DecodeQRStatus, QRType, EncodeQR, Settings from seedsigner.models import SeedStorage - class SeedToolsView(View): - SEEDWORDS = SeedStorage.SEEDWORDS - ALPHABET = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] def __init__(self) -> None: @@ -1298,5 +1295,5 @@ class SeedToolsView(View): def calc_possible_words(self): - self.possible_words = [i for i in SeedToolsView.SEEDWORDS if i.startswith("".join(self.letters).strip())] + self.possible_words = [i for i in Settings.get_instance().wordlist if i.startswith("".join(self.letters).strip())] self.selected_possible_words_index = 0 From 710c67b29e5062d583f8259770198b847925e315 Mon Sep 17 00:00:00 2001 From: newtonick Date: Tue, 31 Aug 2021 19:07:30 +0000 Subject: [PATCH 11/40] work in progress --- src/seedsigner/controller.py | 2 +- src/seedsigner/helpers/mnemonic_generation.py | 5 +++- src/seedsigner/models/__init__.py | 2 +- src/seedsigner/models/decode_qr.py | 30 +++++++++++++------ src/seedsigner/models/encode_qr.py | 18 +++++------ src/seedsigner/models/qr_type.py | 7 ++++- src/seedsigner/models/seed.py | 7 ++++- src/seedsigner/models/settings.py | 3 +- src/seedsigner/views/seed_tools_view.py | 2 -- tests/test_controller.py | 20 +++++++++---- 10 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 7a212a24..3cdbe9b0 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -188,7 +188,7 @@ class Controller(Singleton): return Path.SEED_TOOLS_SUB_MENU if len(seed.mnemonic_list) > 0: - seed.mnemonic = self.seed_tools_view.display_last_word(seed.mnemonic_list) + seed.mnemonic = self.seed_tools_view.display_last_word(seed.mnemonic_display_list) break # display seed phrase diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 4a1c78cf..326a1ee9 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -1,5 +1,8 @@ from embit import bip39 from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes +import unicodedata + +from seedsigner.models import Settings @@ -13,7 +16,7 @@ def calculate_checksum(partial_mnemonic: list): mnemonic_copy.append("abandon") # Ignores the final checksum word and recalcs - mnemonic_bytes = bip39.mnemonic_to_bytes(" ".join(mnemonic_copy), ignore_checksum=True) + mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Settings.get_instance().wordlist) # Return as a list return bip39.mnemonic_from_bytes(mnemonic_bytes).split() diff --git a/src/seedsigner/models/__init__.py b/src/seedsigner/models/__init__.py index 47e5e70c..e378e13d 100644 --- a/src/seedsigner/models/__init__.py +++ b/src/seedsigner/models/__init__.py @@ -1,7 +1,7 @@ -from .settings import * from .qr_type import * from .decode_qr import * from .encode_qr import * +from .settings import * from .psbt_parser import * from .seed import * from .seed_storage import * \ No newline at end of file diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 31663996..033caea2 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -3,11 +3,13 @@ from pyzbar.pyzbar import ZBarSymbol from enum import IntEnum import re import base64 -from embit import bip39, psbt +from embit import psbt from binascii import a2b_base64, b2a_base64 from seedsigner.helpers.ur2.ur_decoder import URDecoder from seedsigner.helpers.bcur import (cbor_decode, bc32decode) from seedsigner.models.qr_type import QRType +from seedsigner.models.settings import Settings +from seedsigner.models.seed import Seed ### ### DecodeQR Class @@ -16,8 +18,6 @@ from seedsigner.models.qr_type import QRType class DecodeQR: - _4LETTER_WORDLIST = [word[:4].strip() for word in bip39.WORDLIST] - def __init__(self, **kwargs): self.complete = False self.qr_type = None @@ -189,6 +189,9 @@ class DecodeQR: @staticmethod def SegmentType(s): + + _WORDLIST = Settings.get_instance().wordlist + _4LETTER_WORDLIST = [word[:4].strip() for word in _WORDLIST] if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE): return QRType.PSBTUR2 @@ -198,10 +201,10 @@ class DecodeQR: return QRType.PSBTURLEGACY elif re.search(r'\d{48,96}', s): return QRType.SEEDSSQR - elif all(x in bip39.WORDLIST for x in s.strip().split(" ")): + 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 - elif all(x in DecodeQR._4LETTER_WORDLIST for x in s.strip().split(" ")): + elif all(x in _4LETTER_WORDLIST for x in s.strip().split(" ")): # checks if all 4 letter words are in list are in 4 letter bip39 word list return QRType.SEED4LETTERMNEMONIC elif DecodeQR.isBase64PSBT(s): @@ -427,6 +430,9 @@ class SeedQR: def add(self, segment, qr_type=QRType.SEEDSSQR): + _WORDLIST = Settings.get_instance().wordlist + _4LETTER_WORDLIST = [word[:4].strip() for word in _WORDLIST] + if qr_type == QRType.SEEDSSQR: try: @@ -436,7 +442,7 @@ class SeedQR: num_words = int(len(segment) / 4) for i in range(0, num_words): index = int(segment[i * 4: (i*4) + 4]) - word = bip39.WORDLIST[index] + word = _WORDLIST[index] self.seed_phrase.append(word) if len(self.seed_phrase) > 0: if self.is1224Phrase() == False: @@ -453,7 +459,10 @@ class SeedQR: try: # embit mnemonic code to validate - bip39.mnemonic_to_seed(segment.strip()) + seed = Seed(segment, passphrase="") + if not seed: + # seed is not valid, return invalid + return DecodeQRStatus.INVALID self.seed_phrase = segment.strip().split(" ") if self.is1224Phrase() == False: return DecodeQRStatus.INVALID @@ -469,10 +478,13 @@ class SeedQR: sl = segment.strip().split(" ") words = [] for s in sl: - words.append(bip39.WORDLIST[DecodeQR._4LETTER_WORDLIST.index(s)]) + words.append(_WORDLIST[_4LETTER_WORDLIST.index(s)]) # embit mnemonic code to validate - bip39.mnemonic_to_seed(" ".join(words).strip()) + seed = Seed(words, passphrase="") + if not seed: + # seed is not valid, return invalid + return DecodeQRStatus.INVALID self.seed_phrase = words if self.is1224Phrase() == False: return DecodeQRStatus.INVALID diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 6931aa13..63d02fd8 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -1,5 +1,5 @@ from enum import IntEnum -from embit import psbt, bip39, bip32 +from embit import psbt, bip32 from embit.networks import NETWORKS from binascii import b2a_base64, hexlify from seedsigner.helpers.ur2.ur_encoder import UREncoder @@ -8,6 +8,8 @@ from seedsigner.helpers.ur2.ur import UR from seedsigner.helpers.bcur import (bc32encode, cbor_encode, bcur_encode) from seedsigner.helpers.qr import QR from seedsigner.models.qr_type import QRType +from seedsigner.models.settings import Settings +from seedsigner.models.seed import Seed ### ### EncodeQR Class @@ -17,6 +19,7 @@ from seedsigner.models.qr_type import QRType class EncodeQR: def __init__(self, **kwargs): + self._WORDLIST = Settings.get_instance().wordlist self.psbt = None self.seed_phrase = None self.passphrase = None @@ -193,7 +196,7 @@ class SeedSSQR: def nextPart(self): data = "" for word in self.seed_phrase: - index = bip39.WORDLIST.index(word) + index = self._WORDLIST.index(word) data += str("%04d" % index) return data @@ -212,8 +215,8 @@ class XPubQR: self.sent_complete = False self.network = network - self.seed = bip39.mnemonic_to_seed((" ".join(self.seed_phrase)).strip(), self.passphrase) - self.root = bip32.HDKey.from_seed(self.seed, version=NETWORKS[self.network]["xprv"]) + self.seed = Seed(self.seed_phrase, self.passphrase) + self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[self.network]["xprv"]) self.fingerprint = self.root.child(0).fingerprint self.bip48_xprv = self.root.derive(self.derivation) self.bip48_xpub = self.bip48_xprv.to_public() @@ -303,9 +306,4 @@ class SpecterXPubQR(XPubQR): return len(self.parts) def isComplete(self): - return self.sent_complete - -class EncodeQRDensity(IntEnum): - LOW = 1 - MEDIUM = 2 - HIGH = 3 \ No newline at end of file + return self.sent_complete \ No newline at end of file diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py index a3eb52ca..f40ad65b 100644 --- a/src/seedsigner/models/qr_type.py +++ b/src/seedsigner/models/qr_type.py @@ -16,4 +16,9 @@ class QRType(IntEnum): SEED4LETTERMNEMONIC = 9 XPUBQR = 20 SPECTERXPUBQR = 21 - INVALID = 100 \ No newline at end of file + INVALID = 100 + +class EncodeQRDensity(IntEnum): + LOW = 1 + MEDIUM = 2 + HIGH = 3 \ No newline at end of file diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 180f6f81..0bd0df94 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -2,7 +2,7 @@ from embit import bip39, bip32 import unicodedata -from seedsigner.models import Settings +from seedsigner.models.settings import Settings class Seed: @@ -41,6 +41,10 @@ class Seed: @property def mnemonic_display(self): return unicodedata.normalize("NFC", self._mnemonic) + + @property + def mnemonic_display_list(self): + return unicodedata.normalize("NFC", self._mnemonic).split() @mnemonic.setter def mnemonic(self, value): @@ -52,6 +56,7 @@ class Seed: if self._init_complete: self._valid = self._generate_seed() + @property def passphrase(self): return self._passphrase diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 9aaae891..24aeea7b 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -1,6 +1,5 @@ from seedsigner.helpers import Singleton -from .encode_qr import EncodeQRDensity -from .qr_type import QRType +from .qr_type import QRType, EncodeQRDensity import configparser from embit import bip39 diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index d7a67fcf..5f6bb8c7 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -1,6 +1,4 @@ # External Dependencies -from embit import bip39 -from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes from PIL import ImageDraw, Image from PIL.ImageOps import autocontrast import hashlib diff --git a/tests/test_controller.py b/tests/test_controller.py index efafa713..27ae35a6 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -19,12 +19,20 @@ def test_singleton_get_instance_preserves_state(): """ Changes to the Controller singleton should be preserved across calls to get_instance() """ settings = """ [system] - DEBUG = True - DEFAULT_LANGUAGE = en - + debug = False + default_language = en + persistent_settings = False + [display] - BACKGROUND_COLOR = #000 - TEXT_COLOR = orange + text_color = ORANGE + + [wallet] + network = main + software = Prompt + qr_density = 2 + script_policy = PKWSH + custom_derivation_enabled = False + custom_derivation = m/0/0 """ config = configparser.ConfigParser() config.read_string(settings) @@ -32,7 +40,7 @@ def test_singleton_get_instance_preserves_state(): # Initialize the instance and verify that it read the config settings Controller.configure_instance(config) controller = Controller.get_instance() - assert controller.color == "orange" + assert controller.color == "ORANGE" # Change a value in the instance... controller.color = "purple" From cb7ada8543ded1b107d2f904fc7bb33ccb00ce96 Mon Sep 17 00:00:00 2001 From: newtonick Date: Tue, 31 Aug 2021 19:58:16 +0000 Subject: [PATCH 12/40] work in progress --- src/seedsigner/controller.py | 4 ++-- src/seedsigner/models/psbt_parser.py | 23 +++++++++++------------ src/seedsigner/models/seed.py | 3 +-- src/seedsigner/models/seed_storage.py | 1 - src/seedsigner/views/io_test_view.py | 3 +++ tests/test_decodepsbtqr.py | 16 +++++++++------- tests/test_encodepsbtqr.py | 3 ++- 7 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 3cdbe9b0..1de1ce16 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -3,7 +3,7 @@ import time from multiprocessing import Process, Queue from subprocess import call import os, sys -from embit import bip39, bip32 +from embit import bip32 from embit.networks import NETWORKS from binascii import hexlify from threading import Thread @@ -694,7 +694,7 @@ class Controller(Singleton): # show transaction information before sign self.menu_view.draw_modal(["Parsing PSBT"]) - p = PSBTParser(psbt,seed.mnemonic_list,seed.passphrase,self.settings.network) + p = PSBTParser(psbt,seed,self.settings.network) self.signing_tools_view.display_transaction_information(p) input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT], False) if input == B.KEY_LEFT: diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py index 52ea6926..b567c6a7 100644 --- a/src/seedsigner/models/psbt_parser.py +++ b/src/seedsigner/models/psbt_parser.py @@ -2,11 +2,12 @@ from embit import psbt, script, ec, bip32, bip39 from embit.networks import NETWORKS from io import BytesIO +from seedsigner.models.seed import Seed + class PSBTParser(): - def __init__(self, p, seed_phrase=[], passphrase="", network="main"): - self.seed_phrase = seed_phrase - self.passphrase = passphrase + def __init__(self, p, seed: Seed, network="main"): + self.seed = seed self.network = network self.psbt = p @@ -18,17 +19,15 @@ class PSBTParser(): self.destination_addresses = [] self.self_addresses = [] - self.seed = None self.root = None - if self.seed_phrase != None: - self.parse(self.psbt,self.seed_phrase,self.passphrase,self.network) + if self.seed: + self.parse(self.psbt,self.seed,self.network) - def __setSeedRoot(self, seed_phrase, passphrase, network): - self.seed = bip39.mnemonic_to_seed(" ".join(seed_phrase).strip(), passphrase) - self.root = bip32.HDKey.from_seed(self.seed, version=NETWORKS[network]["xprv"]) + def __setRoot(self, seed: Seed, network): + self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[network]["xprv"]) - def parse(self, p, seed_phrase=[], passphrase="", network="main"): + def parse(self, p, seed: Seed, network="main"): is_psbt_empty = False try: if p == None: @@ -39,10 +38,10 @@ class PSBTParser(): if is_psbt_empty: return False - if len(seed_phrase) == 0: + if not seed: return False - self.__setSeedRoot(seed_phrase,passphrase,network) + self.__setRoot(seed, network) rt = self.__parseInputs() if rt == False: diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 0bd0df94..c3a95ffa 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -2,8 +2,6 @@ from embit import bip39, bip32 import unicodedata -from seedsigner.models.settings import Settings - class Seed: def __init__(self, mnemonic = None, passphrase = "", wordlist=None) -> None: @@ -12,6 +10,7 @@ class Seed: self.passphrase = passphrase self.mnemonic = mnemonic if wordlist == None: + from seedsigner.models.settings import Settings self._wordlist = Settings.get_instance().wordlist else: self._wordlist = wordlist diff --git a/src/seedsigner/models/seed_storage.py b/src/seedsigner/models/seed_storage.py index b35ad9f4..0ffd654b 100644 --- a/src/seedsigner/models/seed_storage.py +++ b/src/seedsigner/models/seed_storage.py @@ -1,5 +1,4 @@ # External Dependencies -from embit import bip39 from seedsigner.models import Seed class SeedStorage: diff --git a/src/seedsigner/views/io_test_view.py b/src/seedsigner/views/io_test_view.py index d334f689..cb077950 100644 --- a/src/seedsigner/views/io_test_view.py +++ b/src/seedsigner/views/io_test_view.py @@ -23,6 +23,9 @@ class IOTestView(View): self.draw_modal(["Initializing I/O Test"]) print("Initializing I/O Test") self.qr_text = "Scan ANY QR Code" + self.redraw = False + self.redraw_complete = False + self.exit = False try: self.controller.get_instance().camera.start_video_stream_mode() diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index 45290756..d8b0781e 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -1,8 +1,9 @@ import pytest from mock import MagicMock +from seedsigner.models.seed import Seed from seedsigner.models.decode_qr import DecodeQR, QRType, DecodeQRStatus from seedsigner.models.psbt_parser import PSBTParser -from embit import psbt +from embit import psbt, bip39 # this is an of this bug: https://github.com/Foundation-Devices/foundation-ur-py/issues/3 @@ -80,7 +81,8 @@ def test_base64_single_frame_singlsig(): mnemonic = "height demise useless trap grow lion found off key clown transfer enroll" pw = "" - pp = PSBTParser(tx,mnemonic.split(" "),pw,"test") + seed = Seed(mnemonic, pw, wordlist=bip39.WORDLIST) + pp = PSBTParser(tx,seed,"test") assert tx.inputs[0].witness_utxo.value == 25000 # input amount in psbt @@ -130,7 +132,7 @@ def test_base64_2_input_p2wsh(): assert str(tx) == base64_psbt mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - pp = PSBTParser(tx,mnemonic.split(" "),"","test") + pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test") assert tx.inputs[0].witness_utxo.value == 10000000 # input amount 1 in psbt @@ -157,7 +159,7 @@ def test_base64_1_input_p2sh_p2wsh(): assert str(tx) == base64_psbt mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - pp = PSBTParser(tx,mnemonic.split(" "),"","test") + pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test") assert tx.inputs[0].witness_utxo.value == 100000000 # input amount 1 in psbt @@ -213,7 +215,7 @@ def test_ur_legacy(): tx = d.getPSBT() mnemonic = "zone zone zone zone zone abandon ability able abandon ability able abstract" - pp = PSBTParser(tx,mnemonic.split(" "),"","test") + pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test") assert pp.input_amount == 200000 @@ -247,7 +249,7 @@ def test_specter_multisig_animated_qr(): tx = d.getPSBT() mnemonic = "zone zone zone zone zone abandon ability able abandon ability able abstract" - pp = PSBTParser(tx,mnemonic.split(" "),"","test") + pp = PSBTParser(tx,Seed(mnemonic, wordlist=bip39.WORDLIST),"test") assert pp.input_amount == 1052818 @@ -285,7 +287,7 @@ def test_specter_multisig_animated_qr(): tx2 = d2.getPSBT() mnemonic2 = "able bacon cable able bacon cable abandon abandon abandon abandon abandon access" - pp2 = PSBTParser(tx2,mnemonic2.split(" "),"","test") + pp2 = PSBTParser(tx2,Seed(mnemonic2, wordlist=bip39.WORDLIST),"test") assert pp2.input_amount == 1052818 diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index b38239d1..da3e21c5 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -1,7 +1,8 @@ import pytest from mock import MagicMock from seedsigner.models.decode_qr import QRType -from seedsigner.models.encode_qr import EncodeQR, EncodeQRDensity +from seedsigner.models.encode_qr import EncodeQR +from seedsigner.models.qr_type import EncodeQRDensity from embit import psbt from binascii import a2b_base64 From fd21e953cb63ab006ebe54fdab15bf36a2defda4 Mon Sep 17 00:00:00 2001 From: newtonick Date: Fri, 3 Sep 2021 21:04:05 +0000 Subject: [PATCH 13/40] fix to qr encode for seed wordlist --- src/seedsigner/models/encode_qr.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 63d02fd8..2841202f 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -7,7 +7,7 @@ from seedsigner.helpers.ur2.cbor_lite import CBOREncoder from seedsigner.helpers.ur2.ur import UR from seedsigner.helpers.bcur import (bc32encode, cbor_encode, bcur_encode) from seedsigner.helpers.qr import QR -from seedsigner.models.qr_type import QRType +from seedsigner.models.qr_type import QRType, EncodeQRDensity from seedsigner.models.settings import Settings from seedsigner.models.seed import Seed @@ -18,8 +18,10 @@ from seedsigner.models.seed import Seed class EncodeQR: + WORDLIST = None + def __init__(self, **kwargs): - self._WORDLIST = Settings.get_instance().wordlist + EncodeQR.WORDLIST = Settings.get_instance().wordlist self.psbt = None self.seed_phrase = None self.passphrase = None @@ -196,7 +198,7 @@ class SeedSSQR: def nextPart(self): data = "" for word in self.seed_phrase: - index = self._WORDLIST.index(word) + index = EncodeQR.WORDLIST.index(word) data += str("%04d" % index) return data From 27a6c72a7daf412787bd774b841af1eae7d97b5b Mon Sep 17 00:00:00 2001 From: newtonick Date: Tue, 7 Sep 2021 02:26:49 +0000 Subject: [PATCH 14/40] work in progress --- setup.py | 2 +- src/seedsigner/controller.py | 18 +++---- src/seedsigner/helpers/mnemonic_generation.py | 6 +-- src/seedsigner/models/__init__.py | 6 +-- src/seedsigner/models/decode_qr.py | 47 ++++++++++++------- src/seedsigner/models/encode_qr.py | 10 ++-- src/seedsigner/models/psbt_parser.py | 2 - src/seedsigner/models/seed.py | 9 ++-- src/seedsigner/views/seed_tools_view.py | 2 +- tests/test_decodepsbtqr.py | 2 +- tests/test_mnemonic_generation.py | 4 +- tests/test_psbt_parser.py | 8 ++++ 12 files changed, 67 insertions(+), 49 deletions(-) create mode 100644 tests/test_psbt_parser.py diff --git a/setup.py b/setup.py index 66fa1c99..bfee1ee7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh: setuptools.setup( name="seedsigner", - version="0.4.1", + version="0.4.4", author="SeedSigner", author_email="author@example.com", description="Build an offline, airgapped Bitcoin signing device for less than $50!", diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 1de1ce16..87d9d870 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -174,7 +174,7 @@ class Controller(Singleton): ### Generate Last Word 12 / 24 Menu def show_generate_last_word_tool(self) -> int: - seed = Seed() + seed = Seed(wordlist=self.settings.wordlist) ret_val = 0 while True: @@ -215,7 +215,7 @@ class Controller(Singleton): ### Create a Seed w/ Dice Screen def show_create_seed_with_dice_tool(self) -> int: - seed = Seed() + seed = Seed(wordlist=self.settings.wordlist) ret_val = True while True: @@ -249,7 +249,7 @@ class Controller(Singleton): ### Create a Seed w/ Image def show_create_seed_with_image_tool(self) -> int: - seed = Seed() + seed = Seed(wordlist=self.settings.wordlist) ret_val = True while True: @@ -287,7 +287,7 @@ class Controller(Singleton): ### Store a seed (temp) Menu def show_store_a_seed_tool(self): - seed = Seed() + seed = Seed(wordlist=self.settings.wordlist) ret_val = 0 display_saved_seed = False ret_val = self.menu_view.display_saved_seed_menu(self.storage, 1, "... [ Return to Seed Tools ]") @@ -410,7 +410,7 @@ class Controller(Singleton): ### Generate XPUB def show_generate_xpub(self): - seed = Seed() + seed = Seed(wordlist=self.settings.wordlist) # If there is a saved seed, ask to use saved seed if self.storage.num_of_saved_seeds() > 0: @@ -487,7 +487,7 @@ class Controller(Singleton): qr_xpub_type = self.settings.qr_xpub_type self.signing_tools_view.draw_modal(["Generating xPub QR ..."]) - e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=self.settings.derivation, network=self.settings.network, policy=self.settings.script_policy, qr_type=qr_xpub_type, qr_density=self.settings.qr_density) + e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=self.settings.derivation, network=self.settings.network, policy=self.settings.script_policy, qr_type=qr_xpub_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) while e.totalParts() > 1: image = e.nextPartImage(240,240,2) @@ -506,7 +506,7 @@ class Controller(Singleton): ### Sign Transactions def show_sign_transaction(self): - seed = Seed() + seed = Seed(wordlist=self.settings.wordlist) used_saved_seed = False # reusable qr scan function @@ -514,7 +514,7 @@ class Controller(Singleton): # Scan QR using Camera self.menu_view.draw_modal(["Initializing Camera"]) self.camera.start_video_stream_mode(resolution=(480, 480), framerate=12, format="rgb") - decoder = DecodeQR() + decoder = DecodeQR(wordlist=self.settings.wordlist) def live_preview(camera, decoder, scan_text): while True: @@ -714,7 +714,7 @@ class Controller(Singleton): # Display Animated QR Code self.menu_view.draw_modal(["Generating PSBT QR ..."]) - e = EncodeQR(psbt=trimmed_psbt, qr_type=self.settings.qr_psbt_type, qr_density=self.settings.qr_density) + e = EncodeQR(psbt=trimmed_psbt, qr_type=self.settings.qr_psbt_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) while True: image = e.nextPartImage(240,240,1) View.DispShowImage(image) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 326a1ee9..5b3f8a44 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -2,11 +2,9 @@ from embit import bip39 from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes import unicodedata -from seedsigner.models import Settings - -def calculate_checksum(partial_mnemonic: list): +def calculate_checksum(partial_mnemonic: list, wordlist): # Provide 11- or 23-word mnemonic, returns complete mnemonic w/checksum if len(partial_mnemonic) not in [11, 23]: raise Exception("Pass in a 11- or 23-word mnemonic") @@ -16,7 +14,7 @@ def calculate_checksum(partial_mnemonic: list): mnemonic_copy.append("abandon") # Ignores the final checksum word and recalcs - mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Settings.get_instance().wordlist) + mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=wordlist) # Return as a list return bip39.mnemonic_from_bytes(mnemonic_bytes).split() diff --git a/src/seedsigner/models/__init__.py b/src/seedsigner/models/__init__.py index e378e13d..d82d803b 100644 --- a/src/seedsigner/models/__init__.py +++ b/src/seedsigner/models/__init__.py @@ -1,7 +1,7 @@ from .qr_type import * +from .seed import * from .decode_qr import * from .encode_qr import * -from .settings import * from .psbt_parser import * -from .seed import * -from .seed_storage import * \ No newline at end of file +from .seed_storage import * +from .settings import * \ No newline at end of file diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 033caea2..ef864844 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -8,7 +8,6 @@ from binascii import a2b_base64, b2a_base64 from seedsigner.helpers.ur2.ur_decoder import URDecoder from seedsigner.helpers.bcur import (cbor_decode, bc32decode) from seedsigner.models.qr_type import QRType -from seedsigner.models.settings import Settings from seedsigner.models.seed import Seed ### @@ -25,11 +24,18 @@ class DecodeQR: self.specter_qr = SpecterDecodePSBTQR() # Specter Desktop PSBT QR base64 decoder self.legacy_ur = LegacyURDecodeQR() # UR Legacy decoder self.base64_qr = Base64DecodeQR() # Single Segments Base64 - self.seedqr = SeedQR() + self.wordlist = None for key, value in kwargs.items(): if key == "qr_type": self.qr_type = value + if key == "wordlist": + self.wordlist = value + + if self.wordlist == None: + raise Exception('Wordlist Required') + + self.seedqr = SeedQR(wordlist=self.wordlist) def addImage(self, image): qr_str = DecodeQR.QR2Str(image) @@ -42,7 +48,7 @@ class DecodeQR: if qr_str == None: return DecodeQRStatus.FALSE - qr_type = DecodeQR.SegmentType(qr_str) + qr_type = DecodeQR.SegmentType(qr_str, wordlist=self.wordlist) if self.qr_type == None: self.qr_type = qr_type @@ -188,27 +194,29 @@ class DecodeQR: return barcode.data.decode("utf-8") @staticmethod - def SegmentType(s): - - _WORDLIST = Settings.get_instance().wordlist - _4LETTER_WORDLIST = [word[:4].strip() for word in _WORDLIST] + def SegmentType(s, wordlist=None): + # PSBT if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE): return QRType.PSBTUR2 elif re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE): return QRType.PSBTSPECTER elif re.search("^UR:BYTES/", s, re.IGNORECASE): return QRType.PSBTURLEGACY - elif re.search(r'\d{48,96}', s): + elif DecodeQR.isBase64PSBT(s): + return QRType.PSBTBASE64 + + _4LETTER_WORDLIST = [word[:4].strip() for word in wordlist] + + # Seed + if re.search(r'\d{48,96}', s): return QRType.SEEDSSQR - elif all(x in _WORDLIST for x in s.strip().split(" ")): + 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 elif all(x in _4LETTER_WORDLIST for x in s.strip().split(" ")): # checks if all 4 letter words are in list are in 4 letter bip39 word list return QRType.SEED4LETTERMNEMONIC - elif DecodeQR.isBase64PSBT(s): - return QRType.PSBTBASE64 else: return QRType.INVALID @@ -422,16 +430,19 @@ class Base64DecodeQR: class SeedQR: - def __init__(self): + def __init__(self, wordlist=None): self.total_segments = 1 self.collected_segments = 0 self.complete = False self.seed_phrase = [] + self.wordlist = wordlist + + if self.wordlist == None: + raise Exception('Wordlist Required') def add(self, segment, qr_type=QRType.SEEDSSQR): - _WORDLIST = Settings.get_instance().wordlist - _4LETTER_WORDLIST = [word[:4].strip() for word in _WORDLIST] + _4LETTER_WORDLIST = [word[:4].strip() for word in self.wordlist] if qr_type == QRType.SEEDSSQR: @@ -442,7 +453,7 @@ class SeedQR: num_words = int(len(segment) / 4) for i in range(0, num_words): index = int(segment[i * 4: (i*4) + 4]) - word = _WORDLIST[index] + word = self.wordlist[index] self.seed_phrase.append(word) if len(self.seed_phrase) > 0: if self.is1224Phrase() == False: @@ -459,7 +470,7 @@ class SeedQR: try: # embit mnemonic code to validate - seed = Seed(segment, passphrase="") + seed = Seed(segment, passphrase="", wordlist=self.wordlist) if not seed: # seed is not valid, return invalid return DecodeQRStatus.INVALID @@ -478,10 +489,10 @@ class SeedQR: sl = segment.strip().split(" ") words = [] for s in sl: - words.append(_WORDLIST[_4LETTER_WORDLIST.index(s)]) + words.append(self.wordlist[_4LETTER_WORDLIST.index(s)]) # embit mnemonic code to validate - seed = Seed(words, passphrase="") + seed = Seed(words, passphrase="", wordlist=self.wordlist) if not seed: # seed is not valid, return invalid return DecodeQRStatus.INVALID diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 2841202f..a272d7f5 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -8,7 +8,6 @@ from seedsigner.helpers.ur2.ur import UR from seedsigner.helpers.bcur import (bc32encode, cbor_encode, bcur_encode) from seedsigner.helpers.qr import QR from seedsigner.models.qr_type import QRType, EncodeQRDensity -from seedsigner.models.settings import Settings from seedsigner.models.seed import Seed ### @@ -21,7 +20,6 @@ class EncodeQR: WORDLIST = None def __init__(self, **kwargs): - EncodeQR.WORDLIST = Settings.get_instance().wordlist self.psbt = None self.seed_phrase = None self.passphrase = None @@ -31,6 +29,7 @@ class EncodeQR: self.qr_type = None self.qr_density = None self.qr = QR() + self.wordlist = None for key, value in kwargs.items(): if key == "psbt": @@ -49,6 +48,11 @@ class EncodeQR: self.qr_type = value elif key == "qr_density": self.qr_density = value + elif key == "wordlist": + self.wordlist = value + + if self.wordlist == None: + raise Exception('Wordlist Required') if self.qr_type == None: raise Exception('Encoder Type Required') @@ -217,7 +221,7 @@ class XPubQR: self.sent_complete = False self.network = network - self.seed = Seed(self.seed_phrase, self.passphrase) + self.seed = Seed(mnemonic=self.seed_phrase, passphrase=self.passphrase, wordlist=self.wordlist) self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[self.network]["xprv"]) self.fingerprint = self.root.child(0).fingerprint self.bip48_xprv = self.root.derive(self.derivation) diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py index b567c6a7..7406bc3b 100644 --- a/src/seedsigner/models/psbt_parser.py +++ b/src/seedsigner/models/psbt_parser.py @@ -2,8 +2,6 @@ from embit import psbt, script, ec, bip32, bip39 from embit.networks import NETWORKS from io import BytesIO -from seedsigner.models.seed import Seed - class PSBTParser(): def __init__(self, p, seed: Seed, network="main"): diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index c3a95ffa..6d1e990e 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -9,11 +9,10 @@ class Seed: self._valid = False self.passphrase = passphrase self.mnemonic = mnemonic - if wordlist == None: - from seedsigner.models.settings import Settings - self._wordlist = Settings.get_instance().wordlist - else: - self._wordlist = wordlist + self.wordlist = wordlist + + if self.wordlist == None: + raise Exception('Wordlist Required') self._valid = self._generate_seed() self._init_complete = True diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 5f6bb8c7..0fb27e8c 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -665,7 +665,7 @@ class SeedToolsView(View): ### def display_last_word(self, partial_seed_phrase) -> list: - finalseed = mnemonic_generation.calculate_checksum(partial_seed_phrase) + finalseed = mnemonic_generation.calculate_checksum(partial_seed_phrase, wordlist=self.controller.settings.wordlist) last_word = finalseed[-1] self.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index d8b0781e..d4892fa7 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -1,8 +1,8 @@ import pytest from mock import MagicMock -from seedsigner.models.seed import Seed from seedsigner.models.decode_qr import DecodeQR, QRType, DecodeQRStatus from seedsigner.models.psbt_parser import PSBTParser +from seedsigner.models.seed import Seed from embit import psbt, bip39 diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py index 65f002a8..67ecaf9a 100644 --- a/tests/test_mnemonic_generation.py +++ b/tests/test_mnemonic_generation.py @@ -23,10 +23,10 @@ def test_calculate_checksum(): """ # Test mnemonics from https://iancoleman.io/bip39/ partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day" - mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist=bip39.WORDLIST) assert bip39.mnemonic_is_valid(" ".join(mnemonic)) partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile" - mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist=bip39.WORDLIST) assert bip39.mnemonic_is_valid(" ".join(mnemonic)) diff --git a/tests/test_psbt_parser.py b/tests/test_psbt_parser.py new file mode 100644 index 00000000..195d07b2 --- /dev/null +++ b/tests/test_psbt_parser.py @@ -0,0 +1,8 @@ +import pytest +from mock import MagicMock +from seedsigner.models.psbt_parser import PSBTParser +from embit import psbt + +def test_sig_count(): + print("test") + \ No newline at end of file From e5a2c6c8bd731c7a1cbc8c84d604f156bbd5264a Mon Sep 17 00:00:00 2001 From: kdmukai Date: Tue, 7 Sep 2021 13:06:13 -0500 Subject: [PATCH 15/40] Updates test settings to match current Controller expectations; tab spacing changed to space chars --- tests/test_controller.py | 60 +++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/tests/test_controller.py b/tests/test_controller.py index efafa713..c71fb272 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -6,37 +6,45 @@ from seedsigner.controller import Controller def test_singleton_init_fails(): - """ The Controller should not allow any code to instantiate it via Controller() """ - with pytest.raises(Exception): - c = Controller() + """ The Controller should not allow any code to instantiate it via Controller() """ + with pytest.raises(Exception): + c = Controller() def test_singleton_get_instance_without_configure_fails(): - """ Calling get_instance() without first calling configure_instance() should fail """ - with pytest.raises(Exception): - c = Controller.get_instance() + """ Calling get_instance() without first calling configure_instance() should fail """ + with pytest.raises(Exception): + c = Controller.get_instance() def test_singleton_get_instance_preserves_state(): - """ Changes to the Controller singleton should be preserved across calls to get_instance() """ - settings = """ - [system] - DEBUG = True - DEFAULT_LANGUAGE = en + """ Changes to the Controller singleton should be preserved across calls to get_instance() """ + settings = """ + [system] + debug = False + default_language = en + persistent_settings = False - [display] - BACKGROUND_COLOR = #000 - TEXT_COLOR = orange - """ - config = configparser.ConfigParser() - config.read_string(settings) + [display] + text_color = ORANGE - # Initialize the instance and verify that it read the config settings - Controller.configure_instance(config) - controller = Controller.get_instance() - assert controller.color == "orange" + [wallet] + network = main + software = Prompt + qr_density = 2 + script_policy = PKWSH + custom_derivation_enabled = False + custom_derivation = m/0/0 + """ + config = configparser.ConfigParser() + config.read_string(settings) - # Change a value in the instance... - controller.color = "purple" + # Initialize the instance and verify that it read the config settings + Controller.configure_instance(config) + controller = Controller.get_instance() + assert controller.color == "ORANGE" - # ...get a new copy of the instance and confirm change - controller = Controller.get_instance() - assert controller.color == "purple" + # Change a value in the instance... + controller.color = "purple" + + # ...get a new copy of the instance and confirm change + controller = Controller.get_instance() + assert controller.color == "purple" From 616018930b9f689a4c0c7acf2637f63a46bd2d50 Mon Sep 17 00:00:00 2001 From: newtonick Date: Tue, 7 Sep 2021 18:46:59 +0000 Subject: [PATCH 16/40] work in progress --- .nova/Configuration.json | 4 +++ .nova/Tasks/Custom Task.json | 11 +++++++ src/seedsigner/controller.py | 6 ++-- src/seedsigner/models/__init__.py | 3 +- src/seedsigner/models/decode_qr.py | 3 +- src/seedsigner/models/encode_qr.py | 27 ++++++++++------ src/seedsigner/models/encode_qr_density.py | 11 +++++++ src/seedsigner/models/psbt_parser.py | 1 + src/seedsigner/models/qr_type.py | 7 +---- src/seedsigner/models/seed.py | 2 +- src/seedsigner/models/settings.py | 3 +- src/seedsigner/views/menu_view.py | 2 +- src/seedsigner/views/seed_tools_view.py | 2 +- src/seedsigner/views/settings_tools_view.py | 2 -- tests/test_decodepsbtqr.py | 35 +++++++++++---------- tests/test_encodepsbtqr.py | 16 +++++----- 16 files changed, 81 insertions(+), 54 deletions(-) create mode 100644 .nova/Configuration.json create mode 100644 .nova/Tasks/Custom Task.json create mode 100644 src/seedsigner/models/encode_qr_density.py diff --git a/.nova/Configuration.json b/.nova/Configuration.json new file mode 100644 index 00000000..943439e8 --- /dev/null +++ b/.nova/Configuration.json @@ -0,0 +1,4 @@ +{ + "workspace.color" : 2, + "workspace.name" : "seedsigner" +} diff --git a/.nova/Tasks/Custom Task.json b/.nova/Tasks/Custom Task.json new file mode 100644 index 00000000..02e6b4f0 --- /dev/null +++ b/.nova/Tasks/Custom Task.json @@ -0,0 +1,11 @@ +{ + "actions" : { + "run" : { + "enabled" : true, + "script" : "#!\/bin\/sh\n\n# stop systemd service if running\nsudo systemctl stop seedsigner.service\n\n# kill and existing processes\nps -ef | grep \"python3 main.py\" | grep -v grep | awk '{print $2}' | xargs kill\ncd \/home\/pi\/seedsigner\/src\n\/usr\/bin\/python3 main.py" + } + }, + "identifier" : "1B30DFE9-3294-4949-8ED8-67E47D15E10B", + "openLogOnRun" : "start", + "serverName" : "SeedSigner Wifi" +} diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 87d9d870..25c805ff 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -9,11 +9,9 @@ from binascii import hexlify from threading import Thread # Internal file class dependencies -from .views import (View, MenuView, SeedToolsView,SigningToolsView, - SettingsToolsView, IOTestView) +from .views import (View, MenuView, SeedToolsView,SigningToolsView, SettingsToolsView, IOTestView) from .helpers import Buttons, B, Path, Singleton -from .models import (SeedStorage, Settings, DecodeQR, DecodeQRStatus, - EncodeQRDensity, EncodeQR, PSBTParser, QRType, Seed) +from .models import (EncodeQRDensity, QRType, Seed, SeedStorage, Settings, DecodeQR, DecodeQRStatus, EncodeQR, PSBTParser) class Controller(Singleton): """ diff --git a/src/seedsigner/models/__init__.py b/src/seedsigner/models/__init__.py index d82d803b..7e691d33 100644 --- a/src/seedsigner/models/__init__.py +++ b/src/seedsigner/models/__init__.py @@ -1,5 +1,6 @@ -from .qr_type import * from .seed import * +from .qr_type import * +from .encode_qr_density import * from .decode_qr import * from .encode_qr import * from .psbt_parser import * diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index ef864844..0062cbae 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -7,8 +7,7 @@ from embit import psbt from binascii import a2b_base64, b2a_base64 from seedsigner.helpers.ur2.ur_decoder import URDecoder from seedsigner.helpers.bcur import (cbor_decode, bc32decode) -from seedsigner.models.qr_type import QRType -from seedsigner.models.seed import Seed +from seedsigner.models import QRType, Seed ### ### DecodeQR Class diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index a272d7f5..b11d3a1b 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -7,8 +7,7 @@ from seedsigner.helpers.ur2.cbor_lite import CBOREncoder from seedsigner.helpers.ur2.ur import UR from seedsigner.helpers.bcur import (bc32encode, cbor_encode, bcur_encode) from seedsigner.helpers.qr import QR -from seedsigner.models.qr_type import QRType, EncodeQRDensity -from seedsigner.models.seed import Seed +from seedsigner.models import Seed, QRType, EncodeQRDensity ### ### EncodeQR Class @@ -66,11 +65,11 @@ class EncodeQR: elif self.qr_type == QRType.PSBTUR2: self.encoder = UREncodePSBTQR(self.psbt, self.qr_density) elif self.qr_type == QRType.SEEDSSQR: - self.encoder = SeedSSQR(self.seed_phrase) + self.encoder = SeedSSQR(self.seed_phrase, self.wordlist) elif self.qr_type == QRType.XPUBQR: - self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.policy) + self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.policy, self.wordlist) elif self.qr_type == QRType.SPECTERXPUBQR: - self.encoder = SpecterXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.policy, self.qr_density) + self.encoder = SpecterXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.policy, self.qr_density, self.wordlist) else: raise Exception('Encoder Type not Supported') @@ -193,8 +192,12 @@ class SpecterEncodePSBTQR: class SeedSSQR: - def __init__(self, seed_phrase): + def __init__(self, seed_phrase, wordlist): self.seed_phrase = seed_phrase + self.wordlist = wordlist + + if self.wordlist == None: + raise Exception('Wordlist Required') def seqLen(self): return 1 @@ -202,7 +205,7 @@ class SeedSSQR: def nextPart(self): data = "" for word in self.seed_phrase: - index = EncodeQR.WORDLIST.index(word) + index = self.wordlist.index(word) data += str("%04d" % index) return data @@ -212,14 +215,18 @@ class SeedSSQR: class XPubQR: - def __init__(self, seed_phrase, passphrase, derivation, network, policy): + def __init__(self, seed_phrase, passphrase, derivation, network, policy, wordlist): self.seed_phrase = seed_phrase self.passphrase = passphrase self.derivation = derivation + self.wordlist = wordlist self.parts = [] self.part_num_sent = 0 self.sent_complete = False + if self.wordlist == None: + raise Exception('Wordlist Required') + self.network = network self.seed = Seed(mnemonic=self.seed_phrase, passphrase=self.passphrase, wordlist=self.wordlist) self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[self.network]["xprv"]) @@ -258,7 +265,7 @@ class XPubQR: class SpecterXPubQR(XPubQR): - def __init__(self, seed_phrase, passphrase, derivation, network, policy, qr_density): + def __init__(self, seed_phrase, passphrase, derivation, network, policy, qr_density, wordlist): self.qr_max_fragement_size = 65 if qr_density == EncodeQRDensity.LOW: self.qr_max_fragement_size = 40 @@ -267,7 +274,7 @@ class SpecterXPubQR(XPubQR): elif qr_density == EncodeQRDensity.HIGH: self.qr_max_fragement_size = 90 - XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, policy) + XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, policy, wordlist) self.__createParts() def __createParts(self): diff --git a/src/seedsigner/models/encode_qr_density.py b/src/seedsigner/models/encode_qr_density.py new file mode 100644 index 00000000..20da4a27 --- /dev/null +++ b/src/seedsigner/models/encode_qr_density.py @@ -0,0 +1,11 @@ +from enum import IntEnum + +### +### QRType Class IntEum +### Purpose: used with DecodeQR and EncodeQR to communicate qr encoding type +### + +class EncodeQRDensity(IntEnum): + LOW = 1 + MEDIUM = 2 + HIGH = 3 \ No newline at end of file diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py index 7406bc3b..179889fe 100644 --- a/src/seedsigner/models/psbt_parser.py +++ b/src/seedsigner/models/psbt_parser.py @@ -1,6 +1,7 @@ from embit import psbt, script, ec, bip32, bip39 from embit.networks import NETWORKS from io import BytesIO +from seedsigner.models import Seed class PSBTParser(): diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py index f40ad65b..a3eb52ca 100644 --- a/src/seedsigner/models/qr_type.py +++ b/src/seedsigner/models/qr_type.py @@ -16,9 +16,4 @@ class QRType(IntEnum): SEED4LETTERMNEMONIC = 9 XPUBQR = 20 SPECTERXPUBQR = 21 - INVALID = 100 - -class EncodeQRDensity(IntEnum): - LOW = 1 - MEDIUM = 2 - HIGH = 3 \ No newline at end of file + INVALID = 100 \ No newline at end of file diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index 6d1e990e..e0febe9b 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -20,7 +20,7 @@ class Seed: def _generate_seed(self) -> bool: self.seed = None try: - self.seed = bip39.mnemonic_to_seed(self.mnemonic, password=self.passphrase, wordlist=self._wordlist) + self.seed = bip39.mnemonic_to_seed(self.mnemonic, password=self.passphrase, wordlist=self.wordlist) return True except Exception as e: return False diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 24aeea7b..775c396c 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -1,5 +1,6 @@ from seedsigner.helpers import Singleton -from .qr_type import QRType, EncodeQRDensity +from .qr_type import QRType +from .encode_qr_density import EncodeQRDensity import configparser from embit import bip39 diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index fcf0f0b9..a9ddf4af 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -1,7 +1,7 @@ # Internal file class dependencies from . import View from seedsigner.helpers import B, Path -from seedsigner.models import SeedStorage, Settings +from seedsigner.models import SeedStorage, Settings, Seed # External Dependencies import time diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 0fb27e8c..8f679a03 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -9,7 +9,7 @@ import time # Internal file class dependencies from . import View from seedsigner.helpers import B, QR, Keyboard, TextEntryDisplay, mnemonic_generation -from seedsigner.models import DecodeQR, DecodeQRStatus, QRType, EncodeQR, Settings +from seedsigner.models import DecodeQR, DecodeQRStatus, QRType, EncodeQR, Settings, Seed from seedsigner.models import SeedStorage class SeedToolsView(View): diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index 18f33003..5ebdaab3 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -3,8 +3,6 @@ from . import View from seedsigner.helpers import B, QR from seedsigner.models import EncodeQRDensity - - class SettingsToolsView(View): def __init__(self) -> None: diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index d4892fa7..0b9d2840 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -1,8 +1,6 @@ import pytest from mock import MagicMock -from seedsigner.models.decode_qr import DecodeQR, QRType, DecodeQRStatus -from seedsigner.models.psbt_parser import PSBTParser -from seedsigner.models.seed import Seed +from seedsigner.models import Seed, DecodeQR, DecodeQRStatus, QRType, EncodeQRDensity, PSBTParser from embit import psbt, bip39 @@ -41,7 +39,7 @@ def test_ur_qr_decode_multisig(): , "UR:CRYPTO-PSBT/26-29/LPCSCYCSCACFCMCPCYBBRSPTSKHDSSKSKGDYBYDMPEYTWFDIROBZCHGWCEFMFECKZEDYAEAELAAEAEAELAAEAEAELAAOAEAELAADAEAEAEAEAEAEAECPAOAXREAMJNKKZCESYKSWJOJPTBRDEOKGBSHHDTIYPSBEPSVWKSYKJPCPDKUYIMPEOSFECEQZEOVTMDDYAEAELAAEAEAELAAEAEAELAAOAEAELAADAEAEAEAEAEAEAECPAOAXWTSOSKSKFNWYBYLBNSHTEEEEECTORFLRBYCWROTKVTEMBDPDQDTEVALECAWNWDVYCELPDNDYMYDYAEAELAAEAEAELAAEAEAELAAOAEAELAADAEAEAEAEAEAEAEAEADADTKGHCLAOOEWLYATYBAOYLKOLJESGHTGYJLCLRHSGLFKPBDCLSEHYMT" ] - d = DecodeQR() + d = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes: d.addString(i) assert d.qrType() == QRType.PSBTUR2 @@ -63,7 +61,7 @@ def test_base64_single_frame_singlsig(): "cHNidP8BAHICAAAAAQDo5ey+2HIrNUkExsFhsImv1OK1cYA9x/bRjYQD+0UaAQAAAAD9////Apg6AAAAAAAAF6kUVuVZEcdpQ2zgABa9dRUNYHD4VuaHgSYAAAAAAAAWABQaLE4t0JbDRg4pNnmcf+cAWIcyawAAAAAAAQEfqGEAAAAAAAAWABRyuw9od6yuS0yiZljV0X12wG9e5CIGA/ZlEZvQubb6PmcnK+vlnd8aftYnrQ8wHYSxsD8tDp61GIshjoFUAACAAQAAgAAAAIAAAAAAAAAAAAAAAA==" ] - d = DecodeQR() + d = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes: d.addString(i) assert d.qrType() == QRType.PSBTBASE64 @@ -81,7 +79,12 @@ def test_base64_single_frame_singlsig(): mnemonic = "height demise useless trap grow lion found off key clown transfer enroll" pw = "" - seed = Seed(mnemonic, pw, wordlist=bip39.WORDLIST) + seed = Seed(mnemonic, passphrase=pw, wordlist=bip39.WORDLIST) + + assert seed.mnemonic == mnemonic + + assert seed.seed != None + pp = PSBTParser(tx,seed,"test") assert tx.inputs[0].witness_utxo.value == 25000 # input amount in psbt @@ -103,7 +106,7 @@ def test_ur2_sparrow_singlesig_to_self(): , "UR:CRYPTO-PSBT/443-3/LPCFADRKAXCFAOOECYMSTBHDCSHDVYAXADCTPDIARTHKYLIDWSLTAASGSARSUECLSANEPDFZEHDLRNWSGLNNHTYTBNGRYLVYWDCPCLTDAEAEAEAEADDKAXTBCHOSVEHPAHTESNYAUYOYCKRNDRTBGWIHIYTLWFUERDNYWTMSLSROHKRPKELPLONDKOUEJSKBKGIHKNDNAYHLWENSMEGSSSHNAEEEFZSRNSKEGSVYWNEEAATAPYIMPASNHPFWMETEAYBYHFRSBBZCDSRDTTLNADLBEMFTTSSOAHENPFNTVSVEBDHNDTWYKBBDEYTSLPFRJSLBBTVADPDTBEDAKIZEZMZMAOPKPEAOAEAECPAOAXWKGRNLJEVSZMWSIDSOTTEHHETYFTFXCASBMKGTURBWJLIMTLLUFWSTSSKIZMLAFXBALUECZTFTHPISKTDWPEGRGSCPIYHDTLGYKIKORTJLHLVEMNKPCTAERKWPCMJN" ] - d = DecodeQR() + d = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes: d.addString(i) assert d.qrType() == QRType.PSBTUR2 @@ -123,7 +126,7 @@ def test_base64_2_input_p2wsh(): base64_psbt = "cHNidP8BALICAAAAAq1DhxRK+mUH4T6uUNob8bUaZ7MP+44MW4+Y9bOxpjhZAAAAAAD9////aWclWQ+45HKrI07r878E2UrAupT2paT4QurbmtNjYNQBAAAAAP3///8CQEIPAAAAAAAiACCpkDPDhmIzPlkJrjw9A71xjbIUWf3VUB7ooFJhTVm04tjSIQEAAAAAIgAgjQKFDauIXsV5u23LBdYgOwX1FwGGrLiQfWzBtFKZ7dIAAAAATwEENYfPBD5i336AAAACQStJhNVJul7vHKbo83VdmuAW2m0WaXLKDlFANn7dUNoCNbhLMdw4Knz7Q7o6exdL6UFhQegW9nJb0SUStbLEpawUAgjLdzAAAIABAACAAAAAgAIAAIBPAQQ1h88EnbHQAIAAAAI/2Nc7x7iMpJNapTe/OJTV4oifqzQcYY9KV2+PGRjCdQJoww1WnSNqfcxXGyux0q1PqfmzUqgJNqKJCpmqI9t47BQmu4PEMAAAgAEAAIAAAACAAgAAgE8BBDWHzwS6wUg5gAAAAh1Pvr3ZZ+GvcUwJl9OPz2cLXOnTAcBEC7zDtqIOt3IcA1aOofNgUZFu0baQw54SqOcGA7KAvTDOXygfKRilU2OqFHPF2gowAACAAQAAgAAAAIACAACAAAEBK4CWmAAAAAAAIgAgiYAxcG7dnrEiZ4VHFVHOo18XCalvhZYuMqBr9n7HESQBBWlSIQJOjQgMfX26XEf+trHIEk3rYkEX5Y2NfrFKQARPcd2X8iEDBWHUgq25PfHvE+hlcBryJG7wo2y8jKUSPY7sd85OOMchA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHU64iBgJOjQgMfX26XEf+trHIEk3rYkEX5Y2NfrFKQARPcd2X8hwmu4PEMAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgYDBWHUgq25PfHvE+hlcBryJG7wo2y8jKUSPY7sd85OOMccAgjLdzAAAIABAACAAAAAgAIAAIAAAAAAAQAAACIGA2iVcuKLD+2p1pgcAjfZ5d7b/sFt5xQ/aAoC7V0Vn3WHHHPF2gowAACAAQAAgAAAAIACAACAAAAAAAEAAAAAAQErgJaYAAAAAAAiACAzd60wM9EFnPHSNbsSJfyipL8myVLVP2/vwzotVUSNxQEFaVIhAiKCMRLlzIhLkRbLIUIMx5KYJM0v6LcjW/mS6K7eFGwiIQKDzUflU23LeecRgzDo5IBCEvaWGfHW7JkNxzXvuc7FdCEDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgFTriIGAiKCMRLlzIhLkRbLIUIMx5KYJM0v6LcjW/mS6K7eFGwiHAIIy3cwAACAAQAAgAAAAIACAACAAAAAAAAAAAAiBgKDzUflU23LeecRgzDo5IBCEvaWGfHW7JkNxzXvuc7FdBwmu4PEMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIgYDC5DtLoa61/Kk/pdpu0F9e6nKoRJIB9v7Ni377rZefgEcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAAAAAAABAWlSIQKtIdmtKKuZrH7f2R4iIU8RWVOrCdHVWBCS+0e9pZJy/iEDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYhA6GoQ/otQdk71nUpYZFfbkSKdBkkSj4CuPTPYrzGp6JrU64iAgKtIdmtKKuZrH7f2R4iIU8RWVOrCdHVWBCS+0e9pZJy/hwCCMt3MAAAgAEAAIAAAACAAgAAgAEAAAAAAAAAIgIDoH074LrWPIA10hyXtBCJDT06GdLkA6+z/PxoJqomPHYcc8XaCjAAAIABAACAAAAAgAIAAIABAAAAAAAAACICA6GoQ/otQdk71nUpYZFfbkSKdBkkSj4CuPTPYrzGp6JrHCa7g8QwAACAAQAAgAAAAIACAACAAQAAAAAAAAAAAA==" - d = DecodeQR() + d = DecodeQR(wordlist=bip39.WORDLIST) d.addString(base64_psbt) assert d.qrType() == QRType.PSBTBASE64 @@ -150,7 +153,7 @@ def test_base64_1_input_p2sh_p2wsh(): base64_psbt = "cHNidP8BAHICAAAAAR30J629i3Y/R8woRpLQ9JUa31rKxyM+Ny4NEsme48GWAAAAAAD9////Atw5XQUAAAAAF6kUdSESczdYagEyToVUSXyT8VTNz+OHgJaYAAAAAAAWABTmav7/w4OOcfCiewfjsA7eaujhYAAAAABPAQQ1h88EPmLffoAAAAHdXEj2dn8EYJ+rRdXEYu5laq6lJI5Mp+3t63ckwty05QKrJBNPewhwQaGPYRif6+XaxozFXvTXn7pU24H6fRy1FxQCCMt3MAAAgAEAAIAAAACAAQAAgE8BBDWHzwSdsdAAgAAAAeeOv56oeaaFTrNonMKDHk1C8brbWGFvdlecVue+v0/RAn/g4yI3oYsyen7OOcT7caYl4Mn7nQbyonHcusUR+GhzFCa7g8QwAACAAQAAgAAAAIABAACATwEENYfPBLrBSDmAAAABpzrb4oeEh2NNy/w/fr3osfyZTx7AaGDPAcP+LqeR3bYC5ioqmXPuazp69HwimlvecLylm9BLuyl/VpPXqiVrl20Uc8XaCjAAAIABAACAAAAAgAEAAIAAAQEgAOH1BQAAAAAXqRSv3gkn8731qcPbSDu4TJOlJJZ/PocBBCIAIOeiFBX5x0vX6CacrAUVovrs1DDCcKJS5qptFS3sjpDpAQVpUiECZ+pFYkOTVjB+eG+vQFA3MNjZWiA6DjRcs1Wl36A/zgMhA2Ygckuwjah29wiVRgA6wFx51+6ayrzeCIQ2eE4zfxPtIQOnUlBn22cn2CPCZkMSI6cDaZK2SlLV20rT6pqMoQCJsFOuIgYCZ+pFYkOTVjB+eG+vQFA3MNjZWiA6DjRcs1Wl36A/zgMcc8XaCjAAAIABAACAAAAAgAEAAIAAAAAAAAAAACIGA2Ygckuwjah29wiVRgA6wFx51+6ayrzeCIQ2eE4zfxPtHCa7g8QwAACAAQAAgAAAAIABAACAAAAAAAAAAAAiBgOnUlBn22cn2CPCZkMSI6cDaZK2SlLV20rT6pqMoQCJsBwCCMt3MAAAgAEAAIAAAACAAQAAgAAAAAAAAAAAAAEAIgAgZBwTq05RkpqKv6FV6LQjuM07Qv0/bYfWVc9NUQOFvwQBAWlSIQI32jVSdTgeu7+YZKrWfgOZ2J/LV36c5rBoApTzhrNlDCEC+hEqm3XmRt862AFFeyJ7p1m8A+V7czj6OajUNgCfg4EhA/MUVfxGh4k3Po3LB8CmMRsvIHcGNO0elUgETaITZA3UU64iAgI32jVSdTgeu7+YZKrWfgOZ2J/LV36c5rBoApTzhrNlDBwCCMt3MAAAgAEAAIAAAACAAQAAgAEAAAAAAAAAIgIC+hEqm3XmRt862AFFeyJ7p1m8A+V7czj6OajUNgCfg4EcJruDxDAAAIABAACAAAAAgAEAAIABAAAAAAAAACICA/MUVfxGh4k3Po3LB8CmMRsvIHcGNO0elUgETaITZA3UHHPF2gowAACAAQAAgAAAAIABAACAAQAAAAAAAAAAAA==" - d = DecodeQR() + d = DecodeQR(wordlist=bip39.WORDLIST) d.addString(base64_psbt) assert d.qrType() == QRType.PSBTBASE64 @@ -203,7 +206,7 @@ def test_ur_legacy(): , "UR:BYTES/8OF8/MMH5LDF9RK3QKCUL3VM26M6270FFJU50CU6YT5WM65783GL5EJMQPG3WV3/E0DKKUXEJDPX589HZPQ6SVQQQZQQZQQQSQQQQQYQQGQQPQQPQQQQQPQQQQQZYQSZ56D86Z6EZJEJALSG9KYCEJD4ES9TQR59AR7VDQQ9FKL44LSKUNL3E38ZAMFNQQQQSQQSQQYQQQQQPQQZQQQGQQGQQQQQGQQQQQQQ0P6SEH" ] - d = DecodeQR() + d = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes: if d.addString(i) == DecodeQRStatus.COMPLETE: break @@ -237,7 +240,7 @@ def test_specter_multisig_animated_qr(): , "p1of5 cHNidP8BAKQCAAAAA6DLc9RAdKwWQb/7Nrq1FyAtDQ3e0w5E4LkLpcrwBL0aAAAAAAD9////78NG2vqjywKaa0QHfo6C44o1+odXeWZ43FfHy4bLdxABAAAAAP3///8rhueJha90HzgJ6alnJ1uvi0Zbq0JQoXnwDb0Cjn79DwAAAAAA/f///wHWCBAAAAAAABYAFDraKFKubXzlSvRQy8V4X+TwKhLXAAAAAE8BBDWHzwQJeplqgAAAAqmHnnoRR+XJd7gJt/PhLazKeWAfCBLJ4/OF/s0PqqF1AzK8fctt" ] - d = DecodeQR() + d = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes: if d.addString(i) == DecodeQRStatus.COMPLETE: break @@ -275,7 +278,7 @@ def test_specter_multisig_animated_qr(): , "p2of5 OUsU2k9a6Valj6kLb/Bh2gB7YuxenNz7LY5PFJkKc60wAACAAQAAgAAAAIACAACATwEENYfPBEgFouKAAAACOeERUyCmIJuSdQNDB1mxkahw4tc2UCtjxJQoQGnRCtQCZUSXwQF+nA8dl/RG6U1b9ViWJxkzNrDCCagYEWch72cUrnhJ4jAAAIABAACAAAAAgAIAAIAAAQErckUPAAAAAAAiACDwHjagvyTvVDVn+RqxW0xaMLODBm2IVCwDBQm5uK8rDQEFR1IhAi/hk/Tz0aHcNwsVyv86bqQj30sUexPl" ] - d2 = DecodeQR() + d2 = DecodeQR(wordlist=bip39.WORDLIST) for i in qrcodes2: if d2.addString(i) == DecodeQRStatus.COMPLETE: break @@ -308,23 +311,23 @@ def test_specter_multisig_animated_qr(): def test_mnemonic_qr(): mnemonic = "height demise useless trap grow lion found off key clown transfer enroll" - d = DecodeQR() + d = DecodeQR(wordlist=bip39.WORDLIST) assert d.addString(mnemonic) == DecodeQRStatus.COMPLETE assert d.getSeedPhrase() == mnemonic.split() bad_mnemonic = "height useless trap grow lion found off key clown transfer enroll" - d2 = DecodeQR() + d2 = DecodeQR(wordlist=bip39.WORDLIST) assert d2.addString(bad_mnemonic) == DecodeQRStatus.INVALID bad2_mnemonic = "words not in the list of bip39" - d3 = DecodeQR() + d3 = DecodeQR(wordlist=bip39.WORDLIST) assert d3.addString(bad2_mnemonic) == DecodeQRStatus.INVALID def test_short_4_letter_mnemonic_qr(): short_nm = "heig demi usel trap grow lion foun off key clow tran enro" - d = DecodeQR() + d = DecodeQR(wordlist=bip39.WORDLIST) d.addString(short_nm) assert d.isComplete() == True diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index da3e21c5..6b13a407 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -1,9 +1,7 @@ import pytest from mock import MagicMock -from seedsigner.models.decode_qr import QRType -from seedsigner.models.encode_qr import EncodeQR -from seedsigner.models.qr_type import EncodeQRDensity -from embit import psbt +from seedsigner.models import EncodeQR, QRType, EncodeQRDensity +from embit import psbt, bip39 from binascii import a2b_base64 def test_ur_qr_encode(): @@ -12,7 +10,7 @@ def test_ur_qr_encode(): tx = psbt.PSBT.parse(a2b_base64(base64_psbt)) - e = EncodeQR(psbt=tx, qr_type=QRType.PSBTUR2) + e = EncodeQR(psbt=tx, qr_type=QRType.PSBTUR2, wordlist=bip39.WORDLIST) cnt = 0 while cnt <= 10: @@ -27,7 +25,7 @@ def test_specter_qr_encode(): tx = psbt.PSBT.parse(a2b_base64(base64_psbt)) - e = EncodeQR(psbt=tx, qr_type=QRType.PSBTSPECTER) + e = EncodeQR(psbt=tx, qr_type=QRType.PSBTSPECTER, wordlist=bip39.WORDLIST) cnt = 0 while cnt <= 10: @@ -62,7 +60,7 @@ def test_seedsigner_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(" "), qr_type=QRType.SEEDSSQR) + e = EncodeQR(seed_phrase=mnemonic.split(" "), qr_type=QRType.SEEDSSQR, wordlist=bip39.WORDLIST) print(e.nextPart()) @@ -72,7 +70,7 @@ def test_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUBQR, network="test", policy="PKWPKH", derivation="m/48h/1h/0h/2h") + e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUBQR, network="test", policy="PKWPKH", derivation="m/48h/1h/0h/2h", wordlist=bip39.WORDLIST) assert e.nextPart() == "[c49122a5/48h/1h/0h/2h]vpub5adb6xr5X1yqx2v7qoo5uR32BBCQsj9tRe9zbMRsH8X6tYGE6CzuiF6k3njCyHrU5JWPMsoPwNZd9Bk9Kaqy29izB8PbuU6smtv8HLcTEAv" @@ -80,7 +78,7 @@ def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", policy="PKWPKH", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.LOW) + e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", policy="PKWPKH", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.LOW, wordlist=bip39.WORDLIST) assert e.nextPart() == "p1of4 [c49122a5/48h/1h/0h/2h]vpub5adb6xr5X1yqx" assert e.nextPart() == "p2of4 2v7qoo5uR32BBCQsj9tRe9zbMRsH8X6tYGE6Czui" From 3c65c0bfcff82b1cf34b54dd85d8301615e6b645 Mon Sep 17 00:00:00 2001 From: newtonick Date: Tue, 7 Sep 2021 18:48:48 +0000 Subject: [PATCH 17/40] remove nova files, add git ignore for .nova files --- .gitignore | 1 + .nova/Configuration.json | 4 ---- .nova/Tasks/Custom Task.json | 11 ----------- 3 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 .nova/Configuration.json delete mode 100644 .nova/Tasks/Custom Task.json diff --git a/.gitignore b/.gitignore index e5df4958..f2651158 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store __pycache__/ src/seedsigner.egg-info/ +.nova diff --git a/.nova/Configuration.json b/.nova/Configuration.json deleted file mode 100644 index 943439e8..00000000 --- a/.nova/Configuration.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "workspace.color" : 2, - "workspace.name" : "seedsigner" -} diff --git a/.nova/Tasks/Custom Task.json b/.nova/Tasks/Custom Task.json deleted file mode 100644 index 02e6b4f0..00000000 --- a/.nova/Tasks/Custom Task.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "actions" : { - "run" : { - "enabled" : true, - "script" : "#!\/bin\/sh\n\n# stop systemd service if running\nsudo systemctl stop seedsigner.service\n\n# kill and existing processes\nps -ef | grep \"python3 main.py\" | grep -v grep | awk '{print $2}' | xargs kill\ncd \/home\/pi\/seedsigner\/src\n\/usr\/bin\/python3 main.py" - } - }, - "identifier" : "1B30DFE9-3294-4949-8ED8-67E47D15E10B", - "openLogOnRun" : "start", - "serverName" : "SeedSigner Wifi" -} From f6bcdcf1ac1a6312af3f2d6bb27fe01296357bbb Mon Sep 17 00:00:00 2001 From: newtonick Date: Tue, 7 Sep 2021 18:52:18 +0000 Subject: [PATCH 18/40] add psbt_parser test --- tests/test_psbt_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_psbt_parser.py b/tests/test_psbt_parser.py index 195d07b2..7fb84c60 100644 --- a/tests/test_psbt_parser.py +++ b/tests/test_psbt_parser.py @@ -1,6 +1,6 @@ import pytest from mock import MagicMock -from seedsigner.models.psbt_parser import PSBTParser +from seedsigner.models import PSBTParser from embit import psbt def test_sig_count(): From 168014dc531a62838b53284979104b637c2b5d72 Mon Sep 17 00:00:00 2001 From: newtonick Date: Wed, 8 Sep 2021 22:48:30 -0400 Subject: [PATCH 19/40] electrum qr scan working --- src/seedsigner/models/decode_qr.py | 110 ++++++++++++++++++++++-- src/seedsigner/models/psbt_parser.py | 17 ++-- src/seedsigner/models/qr_type.py | 1 + src/seedsigner/views/seed_tools_view.py | 4 +- 4 files changed, 117 insertions(+), 15 deletions(-) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 0062cbae..ecb6a5af 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -23,6 +23,7 @@ class DecodeQR: self.specter_qr = SpecterDecodePSBTQR() # Specter Desktop PSBT QR base64 decoder self.legacy_ur = LegacyURDecodeQR() # UR Legacy decoder self.base64_qr = Base64DecodeQR() # Single Segments Base64 + self.base43_qr = Base43DecodeQR() # Single Segment Base43 self.wordlist = None for key, value in kwargs.items(): @@ -82,6 +83,13 @@ class DecodeQR: if rt == DecodeQRStatus.COMPLETE: self.complete = True return rt + + elif self.qr_type == QRType.PSBTBASE43: + + rt = self.base43_qr.add(qr_str) + if rt == DecodeQRStatus.COMPLETE: + self.complete = True + return rt elif self.qr_type == QRType.SEEDSSQR: rt = self.seedqr.add(qr_str, QRType.SEEDSSQR) @@ -125,6 +133,8 @@ class DecodeQR: return self.legacy_ur.getData() elif self.qr_type == QRType.PSBTBASE64: return self.base64_qr.getData() + elif self.qr_type == QRType.PSBTBASE43: + return self.base43_qr.getData() return None def getBase64PSBT(self): @@ -157,6 +167,11 @@ class DecodeQR: return 100 else: return 0 + elif self.qr_type == QRType.PSBTBASE43: + if self.base43_qr.complete: + return 100 + else: + return 0 else: return 0 @@ -169,7 +184,7 @@ class DecodeQR: return False def isPSBT(self) -> bool: - if self.qr_type in (QRType.PSBTUR2, QRType.PSBTSPECTER, QRType.PSBTURLEGACY, QRType.PSBTBASE64): + if self.qr_type in (QRType.PSBTUR2, QRType.PSBTSPECTER, QRType.PSBTURLEGACY, QRType.PSBTBASE64, QRType.PSBTBASE43): return True return False @@ -216,6 +231,8 @@ class DecodeQR: elif all(x in _4LETTER_WORDLIST for x in s.strip().split(" ")): # checks if all 4 letter words are in list are in 4 letter bip39 word list return QRType.SEED4LETTERMNEMONIC + elif DecodeQR.isBase43PSBT(s): + return QRType.PSBTBASE43 else: return QRType.INVALID @@ -236,6 +253,51 @@ class DecodeQR: return False return False + @staticmethod + def isBase43PSBT(s): + try: + psbt.PSBT.parse(DecodeQR.base43Decode(s)) + return True + except Exception: + return False + return False + + @staticmethod + def base43Decode(s): + chars = b'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:' #base43 chars + + if isinstance(s, bytes): + v = s + if isinstance(s, str): + v = s.encode('ascii') + elif isinstance(s, bytearray): + v = bytes(s) + + long_value = 0 + power_of_base = 1 + for c in v[::-1]: + digit = chars.find(bytes([c])) + if digit == -1: + raise BaseDecodeError('Forbidden character {} for base {}'.format(c, 43)) + # naive but slow variant: long_value += digit * (base**i) + long_value += digit * power_of_base + power_of_base *= 43 + result = bytearray() + while long_value >= 256: + div, mod = divmod(long_value, 256) + result.append(mod) + long_value = div + result.append(long_value) + nPad = 0 + for c in v: + if c == chars[0]: + nPad += 1 + else: + break + result.extend(b'\x00' * nPad) + result.reverse() + return bytes(result) + ### ### SpecterDecodePSBTQR Class ### Purpose: used in DecodePSBTQR to decode Specter Desktop Animated QR PSBT encoding @@ -377,7 +439,7 @@ class LegacyURDecodeQR: ### ### Base64DecodeQR Class ### Purpose: used in DecodeQR to decode single frame base64 encoded qr image -### does not support animated qr because no idicator or segments or thier order +### does not support animated qr because no indicator or segments or their order ### class Base64DecodeQR: @@ -419,12 +481,50 @@ class Base64DecodeQR: def parseSegment(segment) -> str: return segment +### +### Base43DecodeQR Class +### Purpose: used in DecodeQR to decode single frame base43 encoded qr image +### does not support animated qr because no indicator or segments or their order +### + +class Base43DecodeQR: + + def __init__(self): + self.total_segments = 1 + self.collected_segments = 0 + self.complete = False + self.data = None + + def add(self, segment): + if DecodeQR.isBase43PSBT(segment): + self.complete = True + self.data = DecodeQR.base43Decode(segment) + self.collected_segments = 1 + return DecodeQRStatus.COMPLETE + + return DecodeQRStatus.INVALID + + def getData(self): + return self.data + + @staticmethod + def currentSegmentNum(segment) -> int: + return self.collected_segments + + @staticmethod + def totalSegmentNum(segment) -> int: + return self.total_segments + + @staticmethod + def parseSegment(segment) -> str: + return segment + ### ### SeedQR Class ### Purpose: used in DecodeQR to decode single frame representing a seed -### SeedSigner numeric respresentation of a seed as a qr is supported -### Mnemonic seed phrase respresentation of a seed as a qr is supported -### does not support animated qr because no idicator or segments or thier order +### SeedSigner numeric representation of a seed as a qr is supported +### Mnemonic seed phrase representation of a seed as a qr is supported +### does not support animated qr because no indicator or segments or their order ### class SeedQR: diff --git a/src/seedsigner/models/psbt_parser.py b/src/seedsigner/models/psbt_parser.py index 179889fe..8e6f8382 100644 --- a/src/seedsigner/models/psbt_parser.py +++ b/src/seedsigner/models/psbt_parser.py @@ -24,7 +24,7 @@ class PSBTParser(): self.parse(self.psbt,self.seed,self.network) def __setRoot(self, seed: Seed, network): - self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[network]["xprv"]) + self.root = bip32.HDKey.from_seed(seed.seed, version=NETWORKS[network]["xprv"]) def parse(self, p, seed: Seed, network="main"): is_psbt_empty = False @@ -55,13 +55,14 @@ class PSBTParser(): def __parseInputs(self): self.input_amount = 0 for inp in self.psbt.inputs: - self.input_amount += inp.witness_utxo.value - inp_policy = PSBTParser.__get_policy(inp, inp.witness_utxo.script_pubkey, self.psbt.xpubs) - if self.policy == None: - self.policy = inp_policy - else: - if self.policy != inp_policy: - raise RuntimeError("Mixed inputs in the transaction") + if inp.witness_utxo: + self.input_amount += inp.witness_utxo.value + inp_policy = PSBTParser.__get_policy(inp, inp.witness_utxo.script_pubkey, self.psbt.xpubs) + if self.policy == None: + self.policy = inp_policy + else: + if self.policy != inp_policy: + raise RuntimeError("Mixed inputs in the transaction") def __parseOutputs(self): self.spend_amount = 0 diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py index a3eb52ca..a3c2490d 100644 --- a/src/seedsigner/models/qr_type.py +++ b/src/seedsigner/models/qr_type.py @@ -9,6 +9,7 @@ class QRType(IntEnum): PSBTBASE64 = 1 PSBTSPECTER = 2 PSBTURLEGACY = 3 + PSBTBASE43 = 4 PSBTUR2 = 5 SEEDSSQR = 6 SEEDUR2 = 7 diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 8f872fc2..c89432c0 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -1003,7 +1003,7 @@ class SeedToolsView(View): def seed_phrase_as_qr(self, seed_phrase): - e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEEDSSQR) + e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEEDSSQR, wordlist=self.controller.settings.wordlist) image = e.nextPartImage(240, 240, 3) View.DispShowImageWithText(image, "click to zoom, right to exit", font=View.ANTON18, text_color="BLACK", text_background="ORANGE") @@ -1137,7 +1137,7 @@ class SeedToolsView(View): self.draw_modal(["Scanning..."], "Seed QR" ,"Right to Exit") try: self.controller.camera.start_video_stream_mode(resolution=(480, 480), framerate=12, format="rgb") - decoder = DecodeQR() + decoder = DecodeQR(wordlist=self.controller.settings.wordlist) while True: frame = self.controller.camera.read_video_stream(as_image=True) if frame is not None: From f0a10a45fb0032c874f7343c56b5e1fe7011dbe3 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 10 Sep 2021 20:39:07 -0500 Subject: [PATCH 20/40] Fades in logo as startup opening splash screen --- docs/img/logo.svg | 203 ++++++++++++++++++++ src/seedsigner/controller.py | 6 +- src/seedsigner/resources/logo_black_240.png | Bin 0 -> 8440 bytes src/seedsigner/views/__init__.py | 1 + src/seedsigner/views/screensaver.py | 45 +++++ 5 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 docs/img/logo.svg create mode 100644 src/seedsigner/resources/logo_black_240.png create mode 100644 src/seedsigner/views/screensaver.py diff --git a/docs/img/logo.svg b/docs/img/logo.svg new file mode 100644 index 00000000..1b94c144 --- /dev/null +++ b/docs/img/logo.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index d1f04c7a..2d2edf6b 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -10,7 +10,7 @@ from threading import Thread # Internal file class dependencies from .views import (View, MenuView, SeedToolsView,SigningToolsView, - SettingsToolsView, IOTestView) + SettingsToolsView, IOTestView, OpeningSplashView, ScreensaverView) from .helpers import Buttons, B, Path, Singleton from .models import (SeedStorage, Settings, DecodeQR, DecodeQRStatus, EncodeQRDensity, EncodeQR, PSBTParser, QRType) @@ -72,6 +72,7 @@ class Controller(Singleton): controller.io_test_view = IOTestView() controller.signing_tools_view = SigningToolsView(controller.storage) controller.settings_tools_view = SettingsToolsView() + controller.screensaver = ScreensaverView() @property def camera(self): @@ -80,6 +81,9 @@ class Controller(Singleton): def start(self) -> None: + opening_splash = OpeningSplashView() + opening_splash.start() + if self.DEBUG: # Let Exceptions halt execution try: diff --git a/src/seedsigner/resources/logo_black_240.png b/src/seedsigner/resources/logo_black_240.png new file mode 100644 index 0000000000000000000000000000000000000000..68e3043f099e24ea712f40e08006b6d707275e47 GIT binary patch literal 8440 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 Date: Sat, 11 Sep 2021 14:39:54 -0500 Subject: [PATCH 21/40] Adds fully resumable screensaver --- src/seedsigner/controller.py | 8 +- src/seedsigner/helpers/buttons.py | 44 ++++++++++- src/seedsigner/views/screensaver.py | 100 ++++++++++++++++++++++-- src/seedsigner/views/seed_tools_view.py | 4 + src/seedsigner/views/view.py | 17 ++++ 5 files changed, 165 insertions(+), 8 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 2d2edf6b..977c69dc 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -72,7 +72,9 @@ class Controller(Singleton): controller.io_test_view = IOTestView() controller.signing_tools_view = SigningToolsView(controller.storage) controller.settings_tools_view = SettingsToolsView() - controller.screensaver = ScreensaverView() + controller.screensaver = ScreensaverView(controller.buttons) + + controller.screensaver_activation_ms = 60 * 1000 @property def camera(self): @@ -111,6 +113,10 @@ class Controller(Singleton): self.menu_view.draw_modal(["Crashed ..."], "", "requires hard restart") + def start_screensaver(self): + self.screensaver.start() + + ### Menu ### Menu View handles navigation within the menu ### Sub Menu's like Seed Tools, Signing Tools, Settings are all in the Menu View diff --git a/src/seedsigner/helpers/buttons.py b/src/seedsigner/helpers/buttons.py index 80dd9d03..d7f6fea3 100644 --- a/src/seedsigner/helpers/buttons.py +++ b/src/seedsigner/helpers/buttons.py @@ -40,10 +40,30 @@ class Buttons: def wait_for(self, keys=[], check_release=True, release_keys=[]) -> int: + # TODO: Refactor to keep control in the Controller and not here + from seedsigner.controller import Controller + controller = Controller.get_instance() + if not release_keys: release_keys = keys self.override_ind = False + while True: + cur_time = int(time.time() * 1000) + if cur_time - self.last_input_time > controller.screensaver_activation_ms and not controller.screensaver.is_running: + # Start the screensaver. Will block execution until input detected. + controller.start_screensaver() + + # We're back. Update last_input_time to now. + self.update_last_input_time() + + # Freeze any further processing for a moment to avoid having the wakeup + # input register in the resumed UI. + time.sleep(self.next_repeat_threshold / 1000.0) + + # Resume from a fresh loop + continue + for key in keys: if not check_release or ((check_release and key in release_keys and B.release_lock) or check_release and key not in release_keys): # when check release is False or the release lock is released (True) @@ -61,8 +81,6 @@ class Buttons: else: # Still pressing the same input - cur_time = int(time.time() * 1000) - if cur_time - self.last_input_time > self.next_repeat_threshold: # Too much time has elapsed to consider this the same # continuous input. Treat as a new separate press. @@ -92,6 +110,10 @@ class Buttons: time.sleep(0.01) # wait 10 ms to give CPU chance to do other things + def update_last_input_time(self): + self.last_input_time = int(time.time() * 1000) + + def add_events(self, keys=[]): for key in keys: GPIO.add_event_detect(key, self.GPIO.RISING, callback=Buttons.rising_callback) @@ -117,6 +139,13 @@ class Buttons: def check_for_low(self, key) -> bool: return self.GPIO.input(key) == self.GPIO.LOW + def has_any_input(self) -> bool: + for key in B.ALL_KEYS: + if self.GPIO.input(key) == GPIO.LOW: + return True + return False + + # class used as short hand for static button/channel lookup values @@ -133,4 +162,15 @@ class B: KEY3 = 16 OVERRIDE = 1000 + ALL_KEYS = [ + KEY_UP, + KEY_DOWN, + KEY_LEFT, + KEY_RIGHT, + KEY_PRESS, + KEY1, + KEY2, + KEY3, + ] + release_lock = True # released when True, locked when False diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py index 88700edf..3dd0712d 100644 --- a/src/seedsigner/views/screensaver.py +++ b/src/seedsigner/views/screensaver.py @@ -1,17 +1,22 @@ import os +import random import time + from PIL import Image, ImageDraw + from . import View +from seedsigner.helpers import B -class OpeningSplashView(View): - +class LogoView(View): def __init__(self): logo_url = os.path.join("seedsigner", "resources", "logo_black_240.png") self.logo = Image.open(logo_url) + +class OpeningSplashView(LogoView): def start(self): from seedsigner.controller import Controller controller = Controller.get_instance() @@ -36,10 +41,95 @@ class OpeningSplashView(View): -class ScreensaverView(OpeningSplashView): +class ScreensaverView(LogoView): + def __init__(self, buttons): + super().__init__() + + self.buttons = buttons + + # Paste the logo in a bigger image that is 2x the size of the logo + self.image = Image.new("RGB", (2 * self.logo.size[0], 2 * self.logo.size[1]), (0,0,0)) + self.image.paste(self.logo, (int(self.logo.size[0] / 2), int(self.logo.size[1] / 2))) + + self.min_coords = (0, 0) + self.max_coords = (self.logo.size[0], self.logo.size[1]) + + max_increment = 25 + self.increment_x = self.rand_increment() + self.increment_y = self.rand_increment() + self.cur_x = int(self.logo.size[0] / 2) + self.cur_y = int(self.logo.size[1] / 2) + + self._is_running = False + self.last_screen = None + + + + @property + def is_running(self): + return self._is_running + + + def rand_increment(self): + max_increment = 10.0 + min_increment = 1.0 + increment = random.uniform(min_increment, max_increment) + if random.uniform(-1.0, 1.0) < 0.0: + return -1.0 * increment + return increment + + def start(self): - pass + if self.is_running: + return + + self._is_running = True + + # Store the current screen in order to restore it later + self.last_screen = View.canvas.copy() + + screensaver_start = int(time.time() * 1000) + + while True: + if self.buttons.has_any_input(): + return self.stop() + + # Must crop the image to the exact display size + crop = self.image.crop(( + self.cur_x, self.cur_y, + self.cur_x + View.canvas_width, self.cur_y + View.canvas_height)) + View.disp.ShowImage(crop, 0, 0) + + self.cur_x += self.increment_x + self.cur_y += self.increment_y + + if self.cur_x < self.min_coords[0]: + self.cur_x = self.min_coords[0] + self.increment_x = self.rand_increment() + if self.increment_x < 0.0: + self.increment_x *= -1.0 + elif self.cur_x > self.max_coords[0]: + self.cur_x = self.max_coords[0] + self.increment_x = self.rand_increment() + if self.increment_x > 0.0: + self.increment_x *= -1.0 + + if self.cur_y < self.min_coords[1]: + self.cur_y = self.min_coords[1] + self.increment_y = self.rand_increment() + if self.increment_y < 0.0: + self.increment_y *= -1.0 + elif self.cur_y > self.max_coords[1]: + self.cur_y = self.max_coords[1] + self.increment_y = self.rand_increment() + if self.increment_y > 0.0: + self.increment_y *= -1.0 def stop(self): - pass \ No newline at end of file + # Restore the original screen + View.DispShowImage(self.last_screen) + + self._is_running = False + + diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index 23cc2c87..b4c0d180 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -1189,11 +1189,15 @@ class SeedToolsView(View): preview_images.append(frame) if self.buttons.check_for_low(B.KEY_LEFT): + # Have to manually update last input time since we're not in a wait_for loop + self.buttons.update_last_input_time() self.words = [] self.controller.camera.stop_video_stream_mode() return (reshoot, self.words) elif self.buttons.check_for_low(B.KEY_PRESS): + # Have to manually update last input time since we're not in a wait_for loop + self.buttons.update_last_input_time() self.controller.camera.stop_video_stream_mode() break diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 2fbb8a92..03722f87 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -68,6 +68,7 @@ class View: disp = None previous_button_width = None + def __init__(self) -> None: # Import here to avoid circular imports from seedsigner.controller import Controller @@ -89,13 +90,23 @@ class View: self.queue = Queue() + def DispShowImage(image=None, alpha_overlay=None): if image == None: image = View.canvas + else: + # Always keep a copy of the current display in the canvas + View.canvas.paste(image) + if alpha_overlay: image = Image.alpha_composite(image, alpha_overlay) + + # Always keep a copy of the current display in the canvas + View.canvas.paste(image) + View.disp.ShowImage(image, 0, 0) + def disp_show_image_pan(image, start_x, start_y, end_x, end_y, rate, alpha_overlay=None): cur_x = start_x cur_y = start_y @@ -124,6 +135,9 @@ class View: if alpha_overlay: crop = Image.alpha_composite(crop, alpha_overlay) + # Always keep a copy of the current display in the canvas + View.canvas.paste(crop) + View.disp.ShowImage(crop, 0, 0) @@ -140,6 +154,7 @@ class View: if text_background: text_overlay_draw.rectangle(((240 - tw) / 2 - 3, 240 - th, (240 - tw) / 2 + tw + 3, 240), fill=text_background) text_overlay_draw.text(((240 - tw) / 2, 240 - th - 1), text, fill=text_color, font=font) + View.DispShowImage(image_copy, alpha_overlay=text_overlay) @@ -183,11 +198,13 @@ class View: return + def draw_prompt_yes_no(self, lines = [], title = "", bottom = "") -> None: self.draw_prompt_custom("", "Yes ", "No ", lines, title, bottom) return + def draw_prompt_custom(self, a_txt, b_txt, c_txt, lines = [], title = "", bottom = "") -> None: View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) From e1b22b99ff6bd62a4fdf9684ac9e269f56c01fe9 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Mon, 13 Sep 2021 15:58:47 +0000 Subject: [PATCH 22/40] working, additional testing needed --- requirements.txt | 4 +- src/seedsigner/controller.py | 58 ++++++-- src/seedsigner/models/encode_qr.py | 22 +-- src/seedsigner/models/settings.py | 41 +++++- src/seedsigner/views/settings_tools_view.py | 155 +++++++++++++++++++- 5 files changed, 247 insertions(+), 33 deletions(-) diff --git a/requirements.txt b/requirements.txt index c9156ae1..bce39d1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -embit==0.4.5 +embit==0.4.8 numpy==1.21.1 picamera==1.13 Pillow==8.2.0 @@ -6,4 +6,4 @@ pyzbar==0.1.8 qrcode==6.1 RPi.GPIO==0.7.0 six==1.16.0 -spidev==3.5 \ No newline at end of file +spidev==3.5 diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 6f94b2b1..bf5160f1 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -473,21 +473,36 @@ class Controller(Singleton): else: # Cancel return Path.SEED_TOOLS_SUB_MENU - - self.signing_tools_view.draw_modal(["Loading xPub Info ..."]) - - root = bip32.HDKey.from_seed(seed.seed, version=NETWORKS[self.settings.network]["xprv"]) - fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8') - bip48_xprv = root.derive(self.settings.derivation) - bip48_xpub = bip48_xprv.to_public() - if self.settings.script_policy == "PKWPKH": - xpub = bip48_xpub.to_base58(NETWORKS[self.settings.network]["zpub"]) + + # choose single sig or multisig wallet type + wallet_type = "multisig" + script_type = "native segwit" + derivation = "m/0" + r = self.menu_view.display_generic_selection_menu(["Single Sig", "Multisig"], "Wallet Type?") + if r == 1: + wallet_type = "single sig" + elif r == 2: + wallet_type = "multisig" + + # 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() else: - xpub = bip48_xpub.to_base58(NETWORKS[self.settings.network]["Zpub"]) - - self.signing_tools_view.display_xpub_info(fingerprint, self.settings.derivation, xpub) - self.buttons.wait_for([B.KEY_RIGHT]) - + derivation = Settings.calc_derivation(self.settings.network, wallet_type, 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 + if self.settings.software == "Prompt": lines = ["Specter Desktop", "Blue Wallet", "Sparrow"] r = self.menu_view.display_generic_selection_menu(lines, "Which Wallet?") @@ -495,8 +510,21 @@ class Controller(Singleton): else: qr_xpub_type = self.settings.qr_xpub_type + self.signing_tools_view.draw_modal(["Loading xPub Info ..."]) + + version = bip32.detect_version(derivation, default="xpub", network=NETWORKS[self.settings.network]) + root = bip32.HDKey.from_seed(seed.seed, version=NETWORKS[self.settings.network]["xprv"]) + 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) + self.buttons.wait_for([B.KEY_RIGHT]) + self.signing_tools_view.draw_modal(["Generating xPub QR ..."]) - e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=self.settings.derivation, network=self.settings.network, policy=self.settings.script_policy, qr_type=qr_xpub_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) + e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=derivation, network=self.settings.network, policy=self.settings.script_policy, qr_type=qr_xpub_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) while e.totalParts() > 1: image = e.nextPartImage(240,240,2) diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index b11d3a1b..39083200 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -226,22 +226,16 @@ class XPubQR: if self.wordlist == None: raise Exception('Wordlist Required') - - self.network = network + + version = bip32.detect_version(self.derivation, default="xpub", network=NETWORKS[network]) + print(self.derivation) self.seed = Seed(mnemonic=self.seed_phrase, passphrase=self.passphrase, wordlist=self.wordlist) - self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[self.network]["xprv"]) + self.root = bip32.HDKey.from_seed(self.seed.seed, version=NETWORKS[network]["xprv"]) self.fingerprint = self.root.child(0).fingerprint - self.bip48_xprv = self.root.derive(self.derivation) - self.bip48_xpub = self.bip48_xprv.to_public() - - xpub_base58 = "" - - if policy == "PKWPKH": - self.xpub_base58 = self.bip48_xpub.to_base58(NETWORKS[self.network]["zpub"]) - elif policy == "PKWSH": - self.xpub_base58 = self.bip48_xpub.to_base58(NETWORKS[self.network]["Zpub"]) - else: - raise Exception('Policy Type not Supported') + self.xprv = self.root.derive(self.derivation) + self.xpub = self.xprv.to_public() + self.xpub_base58 = self.xpub.to_string(version=version) + self.network = network self.xpubstring = "[%s%s]%s" % (hexlify(self.fingerprint).decode('utf-8'),self.derivation[1:],self.xpub_base58) diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 775c396c..76ec57bc 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -251,4 +251,43 @@ class Settings(Singleton): if type(value) == bool: self._data["wallet"]["custom_derivation_enabled"] = value else: - raise Exception("Unexpected wallet.custom_derivation_enabled settings.ini value") \ No newline at end of file + raise Exception("Unexpected wallet.custom_derivation_enabled settings.ini value") + + @staticmethod + def calc_derivation(network, wallet_type, script_type): + if network == "main": + if wallet_type.lower() == "single sig": + if script_type.lower() == "native segwit": + return "m/84'/0'/0'" + elif script_type.lower() == "nested segwit": + return "m/49'/0'/0'" + else: + raise Exception("Unexpected script type") + elif wallet_type.lower() == "multisig": + if script_type.lower() == "native segwit": + return "m/48'/0'/0'/2'" + elif script_type.lower() == "nested segwit": + return "m/48'/0'/0'/1'" + else: + raise Exception("Unexpected script type") + else: + raise Exception("Unexpected wallet type") + elif network == "test": + if wallet_type.lower() == "single sig": + if script_type.lower() == "native segwit": + return "m/84'/1'/0'" + elif script_type.lower() == "nested segwit": + return "m/49'/1'/0'" + else: + raise Exception("Unexpected script type") + elif wallet_type.lower() == "multisig": + if script_type.lower() == "native segwit": + return "m/48'/1'/0'/2'" + elif script_type.lower() == "nested segwit": + return "m/48'/1'/0'/1'" + else: + raise Exception("Unexpected script type") + else: + raise Exception("Unexpected wallet type") + else: + raise Exception("Unexpected network type") \ No newline at end of file diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index d95e32cd..ce51a54e 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -1,6 +1,6 @@ # SeedSigner file class dependencies from . import View -from seedsigner.helpers import B, QR +from seedsigner.helpers import B, QR, Keyboard, TextEntryDisplay from seedsigner.models import EncodeQRDensity class SettingsToolsView(View): @@ -114,3 +114,156 @@ class SettingsToolsView(View): tw, th = View.draw.textsize(line3, font=View.ANTON18) View.draw.text(((240 - tw) / 2, 210), line3, fill=View.color, font=View.ANTON18) View.DispShowImage() + + ### + ### Custom Derivation Path + ### + + def draw_derivation_keyboard_entry(self, existing_derivation = "m/"): + def render_right_panel(): + row_height = 28 + right_button_left_margin = 10 + right_button_width = 60 + font_padding_right = 2 + font_padding_top = 1 + key_x = View.canvas_width - right_button_width + key_y = int(View.canvas_height - row_height) / 2 - 1 - 60 + font = View.ROBOTOCONDENSED_BOLD_24 + background_color = "#111" + font_color = View.color + button3_text = "Save" + tw, th = font.getsize(button3_text) + key_y = int(View.canvas_height - row_height) / 2 - 1 + 60 + View.draw.rounded_rectangle((key_x, key_y, 250, key_y + row_height), outline=View.color, fill=background_color, radius=5, width=1) + View.draw.text((View.canvas_width - tw - font_padding_right, key_y + font_padding_top), font=font, text=button3_text, fill=font_color) + + # Clear the screen + View.draw.rectangle((0,0, View.canvas_width,View.canvas_height), fill="black") + + self.render_previous_button() + previous_button_is_active = False + + # Have to ensure that we don't carry any effects from a previous run + # TODO: This shouldn't be a member var + if existing_derivation: + self.derivation = existing_derivation + else: + self.derivation = "m/" + + # Set up the keyboard params + right_panel_buttons_width = 60 + + # render top title banner + font = View.ROBOTOCONDENSED_REGULAR_20 + title = "Enter Derivation" + title_top_padding = 0 + title_bottom_padding = 10 + tw, th = font.getsize(title) + View.draw.text((int(View.canvas_width - tw) / 2, title_top_padding), text=title, font=font, fill=View.color) + title_height = th + title_top_padding + title_bottom_padding + + # Render the live text entry display + font = View.ROBOTOCONDENSED_REGULAR_28 + tw, th = font.getsize("m/1234567890") # All possible chars for max range + text_entry_side_padding = 0 + text_entry_top_padding = 1 + text_entry_bottom_padding = 10 + text_entry_top_y = title_height + text_entry_top_padding + text_entry_bottom_y = text_entry_top_y + 3 + th + 3 + text_entry_display = TextEntryDisplay( + View.draw, + rect=(text_entry_side_padding,text_entry_top_y, View.canvas_width - right_panel_buttons_width - 1, text_entry_bottom_y), + font=font, + font_color=View.color, + cursor_mode=TextEntryDisplay.CURSOR_MODE__BLOCK, + is_centered=False, + has_outline=True, + cur_text=''.join(self.derivation) + ) + text_entry_display.render() + cursor_position = len(self.derivation) + + keyboard_start_y = text_entry_bottom_y + text_entry_bottom_padding + keyboard_digits = Keyboard( + View.draw, + charset="0123456789/'", + rows=3, + cols=6, + rect=(0, keyboard_start_y, View.canvas_width - right_panel_buttons_width, View.canvas_height), + auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], + render_now=False + ) + keyboard_digits.set_selected_key(selected_letter="0") + keyboard_digits.render_keys() + render_right_panel() + View.DispShowImage() + + # Start the interactive update loop + while True: + input = View.buttons.wait_for( + [B.KEY_UP, B.KEY_DOWN, B.KEY_RIGHT, B.KEY_LEFT, B.KEY_PRESS, B.KEY3], + check_release=True, + release_keys=[B.KEY_PRESS, B.KEY3] + ) + + # Check our two possible exit conditions + if input == B.KEY3: + # Save! + if len(self.derivation) > 0: + return self.derivation.strip() + + elif input == B.KEY_PRESS and previous_button_is_active: + # Prev button clicked; return empty string to signal cancel. + return "" + + # Process normal input + if input in [B.KEY_UP, B.KEY_DOWN] and previous_button_is_active: + # We're navigating off the previous button + previous_button_is_active = False + self.render_previous_button(highlight=False) + + # Override the actual input w/an ENTER signal for the Keyboard + if input == B.KEY_DOWN: + input = Keyboard.ENTER_TOP + else: + input = Keyboard.ENTER_BOTTOM + elif input in [B.KEY_LEFT, B.KEY_RIGHT] and previous_button_is_active: + # ignore + continue + + ret_val = keyboard_digits.update_from_input(input) + + # Now process the result from the keyboard + if ret_val in Keyboard.EXIT_DIRECTIONS: + self.render_previous_button(highlight=True) + previous_button_is_active = True + + elif ret_val in Keyboard.ADDITIONAL_KEYS and input == B.KEY_PRESS: + if ret_val == Keyboard.KEY_BACKSPACE["code"]: + if len(self.derivation) <= 2: + pass + elif cursor_position == len(self.derivation): + self.derivation = self.derivation[:-1] + cursor_position -= 1 + else: + self.derivation = self.derivation[:cursor_position - 1] + self.derivation[cursor_position:] + cursor_position -= 1 + + elif input == B.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS: + # User has locked in the current letter + if cursor_position == len(self.derivation): + self.derivation += ret_val + else: + self.derivation = self.derivation[:cursor_position] + ret_val + self.derivation[cursor_position:] + cursor_position += 1 + + elif input in [B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN]: + # Live joystick movement; haven't locked this new letter in yet. + # Leave current spot blank for now. Only update the active keyboard keys + # when a selection has been locked in (KEY_PRESS) or removed ("del"). + pass + + # Render the text entry display and cursor block + text_entry_display.render(self.derivation) + + View.DispShowImage() \ No newline at end of file From c11e56636976ec1eacdff66e265a8fe171663d70 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Mon, 13 Sep 2021 16:57:39 +0000 Subject: [PATCH 23/40] =?UTF-8?q?update=20settings=20to=20remove=20script?= =?UTF-8?q?=20type=20since=20it=E2=80=99s=20only=20in=20the=20xpub=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/default_settings.ini | 2 - src/seedsigner/controller.py | 13 +---- src/seedsigner/helpers/path.py | 1 - src/seedsigner/models/encode_qr.py | 13 ++--- src/seedsigner/models/settings.py | 54 +-------------------- src/seedsigner/views/menu_view.py | 23 ++++----- src/seedsigner/views/settings_tools_view.py | 18 ------- src/settings.ini | 4 +- tests/test_controller.py | 2 - tests/test_encodepsbtqr.py | 4 +- 10 files changed, 20 insertions(+), 114 deletions(-) diff --git a/src/default_settings.ini b/src/default_settings.ini index 48d3e892..5d1a99ee 100644 --- a/src/default_settings.ini +++ b/src/default_settings.ini @@ -10,6 +10,4 @@ text_color = ORANGE network = main software = Prompt qr_density = 2 -script_policy = PKWSH -custom_derivation_enabled = False custom_derivation = m/0/0 \ No newline at end of file diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index bf5160f1..1d270d2b 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -151,8 +151,6 @@ class Controller(Singleton): ret_val = self.show_wallet_tool() elif ret_val == Path.QR_DENSITY_SETTING: ret_val = self.show_qr_density_tool() - elif ret_val == Path.WALLET_POLICY: - ret_val = self.show_wallet_policy_tool() elif ret_val == Path.PERSISTENT_SETTINGS: ret_val = self.show_persistent_settings_tool() elif ret_val == Path.DONATE: @@ -524,7 +522,7 @@ class Controller(Singleton): self.buttons.wait_for([B.KEY_RIGHT]) self.signing_tools_view.draw_modal(["Generating xPub QR ..."]) - e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=derivation, network=self.settings.network, policy=self.settings.script_policy, qr_type=qr_xpub_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) + e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=derivation, network=self.settings.network, qr_type=qr_xpub_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) while e.totalParts() > 1: image = e.nextPartImage(240,240,2) @@ -803,15 +801,6 @@ class Controller(Singleton): return Path.SETTINGS_SUB_MENU - ### Show Wallet Policy Tool - - def show_wallet_policy_tool(self): - r = self.settings_tools_view.display_wallet_policy_selection() - if r is not None: - self.settings.script_policy = r - - return Path.SETTINGS_SUB_MENU - ### Show Version Info def show_version_info(self): diff --git a/src/seedsigner/helpers/path.py b/src/seedsigner/helpers/path.py index 8658f3d3..2873aed4 100644 --- a/src/seedsigner/helpers/path.py +++ b/src/seedsigner/helpers/path.py @@ -27,7 +27,6 @@ class Path: DONATE = 63 WALLET = 64 QR_DENSITY_SETTING = 65 - WALLET_POLICY = 66 PERSISTENT_SETTINGS = 67 RESET = 68 diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 39083200..842811d9 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -24,7 +24,6 @@ class EncodeQR: self.passphrase = None self.derivation = None self.network = None - self.policy = None self.qr_type = None self.qr_density = None self.qr = QR() @@ -41,8 +40,6 @@ class EncodeQR: self.derivation = value elif key == "network": self.network = value - elif key == "policy": - self.policy = value elif key == "qr_type": self.qr_type = value elif key == "qr_density": @@ -67,9 +64,9 @@ class EncodeQR: elif self.qr_type == QRType.SEEDSSQR: self.encoder = SeedSSQR(self.seed_phrase, self.wordlist) elif self.qr_type == QRType.XPUBQR: - self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.policy, self.wordlist) + self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.wordlist) elif self.qr_type == QRType.SPECTERXPUBQR: - self.encoder = SpecterXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.policy, self.qr_density, self.wordlist) + self.encoder = SpecterXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.qr_density, self.wordlist) else: raise Exception('Encoder Type not Supported') @@ -215,7 +212,7 @@ class SeedSSQR: class XPubQR: - def __init__(self, seed_phrase, passphrase, derivation, network, policy, wordlist): + def __init__(self, seed_phrase, passphrase, derivation, network, wordlist): self.seed_phrase = seed_phrase self.passphrase = passphrase self.derivation = derivation @@ -259,7 +256,7 @@ class XPubQR: class SpecterXPubQR(XPubQR): - def __init__(self, seed_phrase, passphrase, derivation, network, policy, qr_density, wordlist): + def __init__(self, seed_phrase, passphrase, derivation, network, qr_density, wordlist): self.qr_max_fragement_size = 65 if qr_density == EncodeQRDensity.LOW: self.qr_max_fragement_size = 40 @@ -268,7 +265,7 @@ class SpecterXPubQR(XPubQR): elif qr_density == EncodeQRDensity.HIGH: self.qr_max_fragement_size = 90 - XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, policy, wordlist) + XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, wordlist) self.__createParts() def __createParts(self): diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 76ec57bc..0fcf9ad4 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -32,8 +32,6 @@ class Settings(Singleton): 'network': "main", 'software': "Specter Desktop", 'qr_density': EncodeQRDensity.MEDIUM, - 'script_policy': "PKWSH", - 'custom_derivation_enabled': False, 'custom_derivation': 'm/0/0' } } @@ -54,8 +52,6 @@ class Settings(Singleton): self.network = config["wallet"]["network"] self.software = config["wallet"]["software"] self.qr_density = int(config["wallet"]["qr_density"]) - self.script_policy = config["wallet"]["script_policy"] - self.custom_derivation_enabled = config.getboolean("wallet", "custom_derivation_enabled") self.custom_derivation = config["wallet"]["custom_derivation"] ### persistent settings handling @@ -196,62 +192,14 @@ class Settings(Singleton): else: return "Unknown" - @property - def script_policy(self): - return self._data["wallet"]["script_policy"] - - @script_policy.setter - def script_policy(self, value): - if value in ("PKWSH", "PKWPKH"): - self._data["wallet"]["script_policy"] = value - self.__writeConfig() - else: - raise Exception("Unexpected wallet.script_policy settings.ini value") - - @property - def policy_name(self): - if self.script_policy == "PKWSH": - return "Multi Sig" - elif self.script_policy == "PKWPKH": - return "Single Sig" - else: - raise Exception("Unexpected Wallet policy value") - @property def custom_derivation(self): - if self._data["wallet"]["custom_derivation_enabled"]: - return self._data["wallet"]["custom_derivation"] - else: - return None - - @property - def derivation(self): - if self.script_policy == "PKWSH" and self.network == "main": - return "m/48'/0'/0'/2'" - elif self.script_policy == "PKWSH" and self.network == "test": - return "m/48'/1'/0'/2'" - elif self.script_policy == "PKWPKH" and self.network == "main": - return "m/84'/0'/0'" - elif self.script_policy == "PKWPKH" and self.network == "test": - return "m/84'/1'/0'" - else: - raise Exception("Unsupported Derivation Path or Policy") + return self._data["wallet"]["custom_derivation"] @custom_derivation.setter def custom_derivation(self, value): # TODO: parse and validate custom derivation path self._data["wallet"]["custom_derivation"] = value - - @property - def custom_derivation_enabled(self): - return self._data["wallet"]["custom_derivation_enabled"] - - @custom_derivation_enabled.setter - def custom_derivation_enabled(self, value): - if type(value) == bool: - self._data["wallet"]["custom_derivation_enabled"] = value - else: - raise Exception("Unexpected wallet.custom_derivation_enabled settings.ini value") @staticmethod def calc_derivation(network, wallet_type, script_type): diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index 301b04a0..60c185be 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -124,14 +124,13 @@ class MenuView(View): ### Settings Menu def display_settings_menu(self) -> int: - lines = ["... [ Return to Main ]", "Wallet: ", "Script Policy: ", "Network: ", "QR Density: ", "Input / Output Tests", "Persistent Settings: ", "Version Info", "Donate to SeedSigner", "Reset SeedSigner"] + lines = ["... [ Return to Main ]", "Wallet: ", "Network: ", "QR Density: ", "Input / Output Tests", "Persistent Settings: ", "Version Info", "Donate to SeedSigner", "Reset SeedSigner"] input = 0 lines[1] = lines[1].replace("", Settings.get_instance().software) - lines[2] = lines[2].replace("", Settings.get_instance().policy_name) - lines[3] = lines[3].replace("", Settings.get_instance().network) - lines[4] = lines[4].replace("", Settings.get_instance().qr_density_name) - lines[6] = lines[6].replace("", Settings.get_instance().persistent_display) + lines[2] = lines[2].replace("", Settings.get_instance().network) + lines[3] = lines[3].replace("", Settings.get_instance().qr_density_name) + lines[5] = lines[5].replace("", Settings.get_instance().persistent_display) # Draw Menu self.selected_menu_num = 1 @@ -150,20 +149,18 @@ class MenuView(View): elif self.selected_menu_num == 2: return Path.WALLET elif self.selected_menu_num == 3: - return Path.WALLET_POLICY - elif self.selected_menu_num == 4: return Path.CURRENT_NETWORK - elif self.selected_menu_num == 5: + elif self.selected_menu_num == 4: return Path.QR_DENSITY_SETTING - elif self.selected_menu_num == 6: + elif self.selected_menu_num == 5: return Path.IO_TEST_TOOL - elif self.selected_menu_num == 7: + elif self.selected_menu_num == 6: return Path.PERSISTENT_SETTINGS - elif self.selected_menu_num == 8: + elif self.selected_menu_num == 7: return Path.VERSION_INFO - elif self.selected_menu_num == 9: + elif self.selected_menu_num == 8: return Path.DONATE - elif self.selected_menu_num == 10: + elif self.selected_menu_num == 9: return Path.RESET raise Exception("Unhandled case") diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index ce51a54e..9931fbf2 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -62,24 +62,6 @@ class SettingsToolsView(View): else: return None - ### Display Wallet Policy Selection - - def display_wallet_policy_selection(self) -> str: - - lines = ["... [ Return to Settings ]"] - lines.append("Multi Sig Native Segwit") - lines.append("Single Sig Native Segwit") - - r = self.controller.menu_view.display_generic_selection_menu(lines, "Which Wallet Policy?") - if r == 1: - return None - elif lines[r-1] == "Multi Sig Native Segwit": - return "PKWSH" - elif lines[r-1] == "Single Sig Native Segwit": - return "PKWPKH" - else: - return None - def display_persistent_settings(self) -> bool: lines = ["... [ Return to Settings ]"] diff --git a/src/settings.ini b/src/settings.ini index 48d3e892..17101ca6 100644 --- a/src/settings.ini +++ b/src/settings.ini @@ -10,6 +10,4 @@ text_color = ORANGE network = main software = Prompt qr_density = 2 -script_policy = PKWSH -custom_derivation_enabled = False -custom_derivation = m/0/0 \ No newline at end of file +custom_derivation = m/0 \ No newline at end of file diff --git a/tests/test_controller.py b/tests/test_controller.py index c71fb272..96757eff 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -30,8 +30,6 @@ def test_singleton_get_instance_preserves_state(): network = main software = Prompt qr_density = 2 - script_policy = PKWSH - custom_derivation_enabled = False custom_derivation = m/0/0 """ config = configparser.ConfigParser() diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 6b13a407..3de257c7 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -70,7 +70,7 @@ def test_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUBQR, network="test", policy="PKWPKH", derivation="m/48h/1h/0h/2h", wordlist=bip39.WORDLIST) + e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUBQR, network="test", derivation="m/48h/1h/0h/2h", wordlist=bip39.WORDLIST) assert e.nextPart() == "[c49122a5/48h/1h/0h/2h]vpub5adb6xr5X1yqx2v7qoo5uR32BBCQsj9tRe9zbMRsH8X6tYGE6CzuiF6k3njCyHrU5JWPMsoPwNZd9Bk9Kaqy29izB8PbuU6smtv8HLcTEAv" @@ -78,7 +78,7 @@ def test_specter_xpub_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", policy="PKWPKH", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.LOW, wordlist=bip39.WORDLIST) + e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.LOW, wordlist=bip39.WORDLIST) assert e.nextPart() == "p1of4 [c49122a5/48h/1h/0h/2h]vpub5adb6xr5X1yqx" assert e.nextPart() == "p2of4 2v7qoo5uR32BBCQsj9tRe9zbMRsH8X6tYGE6Czui" From fe8d6a4157f4b596c56b6b35b540cab7388f0b01 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Mon, 13 Sep 2021 17:17:02 +0000 Subject: [PATCH 24/40] fix keyboard issue --- src/seedsigner/controller.py | 5 +++-- src/seedsigner/views/settings_tools_view.py | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index dab0571f..085bd2ef 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -475,7 +475,7 @@ class Controller(Singleton): # choose single sig or multisig wallet type wallet_type = "multisig" script_type = "native segwit" - derivation = "m/0" + derivation = self.settings.custom_derivation r = self.menu_view.display_generic_selection_menu(["Single Sig", "Multisig"], "Wallet Type?") if r == 1: wallet_type = "single sig" @@ -493,7 +493,8 @@ class Controller(Singleton): # calculated derivation or get custom from keyboard entry if script_type == "custom": - derivation = self.settings_tools_view.draw_derivation_keyboard_entry() + 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(self.settings.network, wallet_type, script_type) diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index cf106fd8..1b533716 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -10,6 +10,7 @@ class SettingsToolsView(View): self.qr = QR() self.donate_image = None + self.derivation = None ### Donate Menu Item @@ -168,16 +169,19 @@ class SettingsToolsView(View): keyboard_start_y = text_entry_bottom_y + text_entry_bottom_padding keyboard_digits = Keyboard( View.draw, - charset="0123456789/'", + charset="/'0123456789", rows=3, cols=6, rect=(0, keyboard_start_y, View.canvas_width - right_panel_buttons_width, View.canvas_height), auto_wrap=[Keyboard.WRAP_LEFT, Keyboard.WRAP_RIGHT], render_now=False ) - keyboard_digits.set_selected_key(selected_letter="0") + keyboard_digits.set_selected_key(selected_letter="/") keyboard_digits.render_keys() render_right_panel() + + text_entry_display.render(self.derivation) + View.DispShowImage() # Start the interactive update loop while True: From 293ec3b23e5966c8a85bfccf7971a169b444daa0 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Thu, 16 Sep 2021 16:15:05 +0000 Subject: [PATCH 25/40] update doc for recovery --- docs/recovery.md | 37 ++++++++++++++++++----- src/seedsigner/models/seed.py | 34 ++++++++++++++++++--- src/seedsigner/views/screensaver.py | 3 +- tests/test_encodepsbtqr.py | 10 +++---- tests/test_seed.py | 46 +++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 tests/test_seed.py diff --git a/docs/recovery.md b/docs/recovery.md index 931c897a..5725dc75 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -1,14 +1,35 @@ # SeedSigner Recovery Information -SeedSigner creates extended public keys for the coordinator following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) to encode the extended public key. SeedSigner only supports native segwit for single sig (P2WPKH) and native segwit multisig (P2WSH). +SeedSigner creates extended public keys for the coordinator following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) to encode the extended public key. SeedSigner supports native and nested segwit for single sig and multisig. -SeedSigners default settings for "Script Policy" (2021-08-21) is Multi Sig Native Segwit. The second option is Single Sig Native Segwit. +Derivation paths for standard script types for mainnet: -- Single Sig Derivation Path: m/84'/0'/0' -- Single Sig Public Key Encoding: 0x04b24746 - zpub -- Multisig Dervivation Path: m/48'/0'/0'/2' -- Multisig Public Key Encoding: 0x02aa7ed3 - Zpub +- Single Sig + - Native Segwit + - Derivation Path: m/84'/0'/0' + - Script Type: P2WPKH + - Public Key Encoding: 0x04b24746 - zpub + - Nested Segwit + - Derivation Path: m/49'/0'/0' + - Script Type: P2WPKH in P2SH + - Public Key Encoding: 0x049d7cb2 - ypub +- Multisig + - Native Segwit + - Derivation Path: m/48'/0'/0'/2' + - Script Type: P2WSH + - Public Key Encoding: 0x02aa7ed3 - Zpub + - Nested Segwit + - Derivation Path: m/48'/0'/0'/1' + - Script Type: P2WSH in P2SH + - Public Key Encoding: 0x0295b43f - Ypub -Changing the network settings from main to test will change the public key encoding and derivation path following bip-0084, bip-0048 and slip-0132 standards. +Custom derivation paths are also optional when generating an xpub from SeedSigner. The Public Key Encodings is detected based on the derivation path configured. Embit bitcoin library does the detection and is documented [here](https://github.com/diybitcoinhardware/embit/blob/master/docs/api/bip32.md#detect_version). For a video explination of these standards see a presentation by Stepan of Embit/Specter on this topic: https://youtube.com/watch?v=JCaC5DG2HTM -Custom derivation paths will be optional in the future. +Changing the network settings from main to test in SeedSigner will change the public key encoding and derivation path following [slip-0132](https://github.com/satoshilabs/slips/blob/master/slip-0132.md) standards. + +Related Standards: +- [bip-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) +- [bip-0044](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) +- [bip-0048](https://github.com/bitcoin/bips/blob/master/bip-0048.mediawiki) +- [bip-0049](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) +- [bip-0084](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) \ No newline at end of file diff --git a/src/seedsigner/models/seed.py b/src/seedsigner/models/seed.py index e0febe9b..9009c7bb 100644 --- a/src/seedsigner/models/seed.py +++ b/src/seedsigner/models/seed.py @@ -9,9 +9,9 @@ class Seed: self._valid = False self.passphrase = passphrase self.mnemonic = mnemonic - self.wordlist = wordlist + self._wordlist = wordlist - if self.wordlist == None: + if self._wordlist == None: raise Exception('Wordlist Required') self._valid = self._generate_seed() @@ -20,7 +20,7 @@ class Seed: def _generate_seed(self) -> bool: self.seed = None try: - self.seed = bip39.mnemonic_to_seed(self.mnemonic, password=self.passphrase, wordlist=self.wordlist) + self.seed = bip39.mnemonic_to_seed(self.mnemonic, password=self.passphrase, wordlist=self._wordlist) return True except Exception as e: return False @@ -54,7 +54,6 @@ class Seed: if self._init_complete: self._valid = self._generate_seed() - @property def passphrase(self): return self._passphrase @@ -71,6 +70,33 @@ class Seed: if self._init_complete: self._valid = self._generate_seed() + @property + def wordlist(self): + return self._wordlist + + @wordlist.setter + def wordlist(self, value): + previous_wordlist = self._wordlist + + if isinstance(value, list): + if len(value) == 2048: + self._wordlist = value + else: + raise Exception('Invalid Wordlist') + else: + raise Exception('Wordlist Must be List Type') + + if self.mnemonic != None: + # if has mnemonic, convert to new wordlist + previous_mnemonic_str = self._mnemonic + previous_mnemonic_list = previous_mnemonic_str.split() + new_mnemonic_list = [] + for word in previous_mnemonic_list: + idx = previous_wordlist.index(word) + new_mnemonic_list.append(self._wordlist[idx]) + + self._mnemonic = " ".join(new_mnemonic_list) + ### override operators def __eq__(self, other): diff --git a/src/seedsigner/views/screensaver.py b/src/seedsigner/views/screensaver.py index 3dd0712d..3f4afa0d 100644 --- a/src/seedsigner/views/screensaver.py +++ b/src/seedsigner/views/screensaver.py @@ -11,7 +11,8 @@ from seedsigner.helpers import B class LogoView(View): def __init__(self): - logo_url = os.path.join("seedsigner", "resources", "logo_black_240.png") + dirname = os.path.dirname(__file__) + logo_url = os.path.join(dirname, "../../", "seedsigner", "resources", "logo_black_240.png") self.logo = Image.open(logo_url) diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index 3de257c7..c5b4048b 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -72,7 +72,7 @@ def test_xpub_qr(): e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUBQR, network="test", derivation="m/48h/1h/0h/2h", wordlist=bip39.WORDLIST) - assert e.nextPart() == "[c49122a5/48h/1h/0h/2h]vpub5adb6xr5X1yqx2v7qoo5uR32BBCQsj9tRe9zbMRsH8X6tYGE6CzuiF6k3njCyHrU5JWPMsoPwNZd9Bk9Kaqy29izB8PbuU6smtv8HLcTEAv" + assert e.nextPart() == "[c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDNc5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBoMxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1JsZsfdBzsK3VUC5" def test_specter_xpub_qr(): @@ -80,9 +80,9 @@ def test_specter_xpub_qr(): e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.LOW, wordlist=bip39.WORDLIST) - assert e.nextPart() == "p1of4 [c49122a5/48h/1h/0h/2h]vpub5adb6xr5X1yqx" - assert e.nextPart() == "p2of4 2v7qoo5uR32BBCQsj9tRe9zbMRsH8X6tYGE6Czui" - assert e.nextPart() == "p3of4 F6k3njCyHrU5JWPMsoPwNZd9Bk9Kaqy29izB8Pbu" - assert e.nextPart() == "p4of4 U6smtv8HLcTEAv" + assert e.nextPart() == "p1of4 [c49122a5/48h/1h/0h/2h]Vpub5mXgECaX5yYDN" + assert e.nextPart() == "p2of4 c5VnUG4jVNptyEg65qUjuofWchQeuMWWiq8rcPBo" + assert e.nextPart() == "p3of4 MxfrVggXj5NJmaNEToWpax8GMMucozvAdqf1bW1J" + assert e.nextPart() == "p4of4 sZsfdBzsK3VUC5" diff --git a/tests/test_seed.py b/tests/test_seed.py new file mode 100644 index 00000000..a03f2f87 --- /dev/null +++ b/tests/test_seed.py @@ -0,0 +1,46 @@ +import pytest +from mock import MagicMock +from seedsigner.models import Seed +from embit import bip39 + +spanish_wordlist = ["ábaco", "abdomen", "abeja", "abierto", "abogado", "abono", "aborto", "abrazo", "abrir", "abuelo", "abuso", "acabar", "academia", "acceso", "acción", "aceite", "acelga", "acento", "aceptar", "ácido", "aclarar", "acné", "acoger", "acoso", "activo", "acto", "actriz", "actuar", "acudir", "acuerdo", "acusar", "adicto", "admitir", "adoptar", "adorno", "aduana", "adulto", "aéreo", "afectar", "afición", "afinar", "afirmar", "ágil", "agitar", "agonía", "agosto", "agotar", "agregar", "agrio", "agua", "agudo", "águila", "aguja", "ahogo", "ahorro", "aire", "aislar", "ajedrez", "ajeno", "ajuste", "alacrán", "alambre", "alarma", "alba", "álbum", "alcalde", "aldea", "alegre", "alejar", "alerta", "aleta", "alfiler", "alga", "algodón", "aliado", "aliento", "alivio", "alma", "almeja", "almíbar", "altar", "alteza", "altivo", "alto", "altura", "alumno", "alzar", "amable", "amante", "amapola", "amargo", "amasar", "ámbar", "ámbito", "ameno", "amigo", "amistad", "amor", "amparo", "amplio", "ancho", "anciano", "ancla", "andar", "andén", "anemia", "ángulo", "anillo", "ánimo", "anís", "anotar", "antena", "antiguo", "antojo", "anual", "anular", "anuncio", "añadir", "añejo", "año", "apagar", "aparato", "apetito", "apio", "aplicar", "apodo", "aporte", "apoyo", "aprender", "aprobar", "apuesta", "apuro", "arado", "araña", "arar", "árbitro", "árbol", "arbusto", "archivo", "arco", "arder", "ardilla", "arduo", "área", "árido", "aries", "armonía", "arnés", "aroma", "arpa", "arpón", "arreglo", "arroz", "arruga", "arte", "artista", "asa", "asado", "asalto", "ascenso", "asegurar", "aseo", "asesor", "asiento", "asilo", "asistir", "asno", "asombro", "áspero", "astilla", "astro", "astuto", "asumir", "asunto", "atajo", "ataque", "atar", "atento", "ateo", "ático", "atleta", "átomo", "atraer", "atroz", "atún", "audaz", "audio", "auge", "aula", "aumento", "ausente", "autor", "aval", "avance", "avaro", "ave", "avellana", "avena", "avestruz", "avión", "aviso", "ayer", "ayuda", "ayuno", "azafrán", "azar", "azote", "azúcar", "azufre", "azul", "baba", "babor", "bache", "bahía", "baile", "bajar", "balanza", "balcón", "balde", "bambú", "banco", "banda", "baño", "barba", "barco", "barniz", "barro", "báscula", "bastón", "basura", "batalla", "batería", "batir", "batuta", "baúl", "bazar", "bebé", "bebida", "bello", "besar", "beso", "bestia", "bicho", "bien", "bingo", "blanco", "bloque", "blusa", "boa", "bobina", "bobo", "boca", "bocina", "boda", "bodega", "boina", "bola", "bolero", "bolsa", "bomba", "bondad", "bonito", "bono", "bonsái", "borde", "borrar", "bosque", "bote", "botín", "bóveda", "bozal", "bravo", "brazo", "brecha", "breve", "brillo", "brinco", "brisa", "broca", "broma", "bronce", "brote", "bruja", "brusco", "bruto", "buceo", "bucle", "bueno", "buey", "bufanda", "bufón", "búho", "buitre", "bulto", "burbuja", "burla", "burro", "buscar", "butaca", "buzón", "caballo", "cabeza", "cabina", "cabra", "cacao", "cadáver", "cadena", "caer", "café", "caída", "caimán", "caja", "cajón", "cal", "calamar", "calcio", "caldo", "calidad", "calle", "calma", "calor", "calvo", "cama", "cambio", "camello", "camino", "campo", "cáncer", "candil", "canela", "canguro", "canica", "canto", "caña", "cañón", "caoba", "caos", "capaz", "capitán", "capote", "captar", "capucha", "cara", "carbón", "cárcel", "careta", "carga", "cariño", "carne", "carpeta", "carro", "carta", "casa", "casco", "casero", "caspa", "castor", "catorce", "catre", "caudal", "causa", "cazo", "cebolla", "ceder", "cedro", "celda", "célebre", "celoso", "célula", "cemento", "ceniza", "centro", "cerca", "cerdo", "cereza", "cero", "cerrar", "certeza", "césped", "cetro", "chacal", "chaleco", "champú", "chancla", "chapa", "charla", "chico", "chiste", "chivo", "choque", "choza", "chuleta", "chupar", "ciclón", "ciego", "cielo", "cien", "cierto", "cifra", "cigarro", "cima", "cinco", "cine", "cinta", "ciprés", "circo", "ciruela", "cisne", "cita", "ciudad", "clamor", "clan", "claro", "clase", "clave", "cliente", "clima", "clínica", "cobre", "cocción", "cochino", "cocina", "coco", "código", "codo", "cofre", "coger", "cohete", "cojín", "cojo", "cola", "colcha", "colegio", "colgar", "colina", "collar", "colmo", "columna", "combate", "comer", "comida", "cómodo", "compra", "conde", "conejo", "conga", "conocer", "consejo", "contar", "copa", "copia", "corazón", "corbata", "corcho", "cordón", "corona", "correr", "coser", "cosmos", "costa", "cráneo", "cráter", "crear", "crecer", "creído", "crema", "cría", "crimen", "cripta", "crisis", "cromo", "crónica", "croqueta", "crudo", "cruz", "cuadro", "cuarto", "cuatro", "cubo", "cubrir", "cuchara", "cuello", "cuento", "cuerda", "cuesta", "cueva", "cuidar", "culebra", "culpa", "culto", "cumbre", "cumplir", "cuna", "cuneta", "cuota", "cupón", "cúpula", "curar", "curioso", "curso", "curva", "cutis", "dama", "danza", "dar", "dardo", "dátil", "deber", "débil", "década", "decir", "dedo", "defensa", "definir", "dejar", "delfín", "delgado", "delito", "demora", "denso", "dental", "deporte", "derecho", "derrota", "desayuno", "deseo", "desfile", "desnudo", "destino", "desvío", "detalle", "detener", "deuda", "día", "diablo", "diadema", "diamante", "diana", "diario", "dibujo", "dictar", "diente", "dieta", "diez", "difícil", "digno", "dilema", "diluir", "dinero", "directo", "dirigir", "disco", "diseño", "disfraz", "diva", "divino", "doble", "doce", "dolor", "domingo", "don", "donar", "dorado", "dormir", "dorso", "dos", "dosis", "dragón", "droga", "ducha", "duda", "duelo", "dueño", "dulce", "dúo", "duque", "durar", "dureza", "duro", "ébano", "ebrio", "echar", "eco", "ecuador", "edad", "edición", "edificio", "editor", "educar", "efecto", "eficaz", "eje", "ejemplo", "elefante", "elegir", "elemento", "elevar", "elipse", "élite", "elixir", "elogio", "eludir", "embudo", "emitir", "emoción", "empate", "empeño", "empleo", "empresa", "enano", "encargo", "enchufe", "encía", "enemigo", "enero", "enfado", "enfermo", "engaño", "enigma", "enlace", "enorme", "enredo", "ensayo", "enseñar", "entero", "entrar", "envase", "envío", "época", "equipo", "erizo", "escala", "escena", "escolar", "escribir", "escudo", "esencia", "esfera", "esfuerzo", "espada", "espejo", "espía", "esposa", "espuma", "esquí", "estar", "este", "estilo", "estufa", "etapa", "eterno", "ética", "etnia", "evadir", "evaluar", "evento", "evitar", "exacto", "examen", "exceso", "excusa", "exento", "exigir", "exilio", "existir", "éxito", "experto", "explicar", "exponer", "extremo", "fábrica", "fábula", "fachada", "fácil", "factor", "faena", "faja", "falda", "fallo", "falso", "faltar", "fama", "familia", "famoso", "faraón", "farmacia", "farol", "farsa", "fase", "fatiga", "fauna", "favor", "fax", "febrero", "fecha", "feliz", "feo", "feria", "feroz", "fértil", "fervor", "festín", "fiable", "fianza", "fiar", "fibra", "ficción", "ficha", "fideo", "fiebre", "fiel", "fiera", "fiesta", "figura", "fijar", "fijo", "fila", "filete", "filial", "filtro", "fin", "finca", "fingir", "finito", "firma", "flaco", "flauta", "flecha", "flor", "flota", "fluir", "flujo", "flúor", "fobia", "foca", "fogata", "fogón", "folio", "folleto", "fondo", "forma", "forro", "fortuna", "forzar", "fosa", "foto", "fracaso", "frágil", "franja", "frase", "fraude", "freír", "freno", "fresa", "frío", "frito", "fruta", "fuego", "fuente", "fuerza", "fuga", "fumar", "función", "funda", "furgón", "furia", "fusil", "fútbol", "futuro", "gacela", "gafas", "gaita", "gajo", "gala", "galería", "gallo", "gamba", "ganar", "gancho", "ganga", "ganso", "garaje", "garza", "gasolina", "gastar", "gato", "gavilán", "gemelo", "gemir", "gen", "género", "genio", "gente", "geranio", "gerente", "germen", "gesto", "gigante", "gimnasio", "girar", "giro", "glaciar", "globo", "gloria", "gol", "golfo", "goloso", "golpe", "goma", "gordo", "gorila", "gorra", "gota", "goteo", "gozar", "grada", "gráfico", "grano", "grasa", "gratis", "grave", "grieta", "grillo", "gripe", "gris", "grito", "grosor", "grúa", "grueso", "grumo", "grupo", "guante", "guapo", "guardia", "guerra", "guía", "guiño", "guion", "guiso", "guitarra", "gusano", "gustar", "haber", "hábil", "hablar", "hacer", "hacha", "hada", "hallar", "hamaca", "harina", "haz", "hazaña", "hebilla", "hebra", "hecho", "helado", "helio", "hembra", "herir", "hermano", "héroe", "hervir", "hielo", "hierro", "hígado", "higiene", "hijo", "himno", "historia", "hocico", "hogar", "hoguera", "hoja", "hombre", "hongo", "honor", "honra", "hora", "hormiga", "horno", "hostil", "hoyo", "hueco", "huelga", "huerta", "hueso", "huevo", "huida", "huir", "humano", "húmedo", "humilde", "humo", "hundir", "huracán", "hurto", "icono", "ideal", "idioma", "ídolo", "iglesia", "iglú", "igual", "ilegal", "ilusión", "imagen", "imán", "imitar", "impar", "imperio", "imponer", "impulso", "incapaz", "índice", "inerte", "infiel", "informe", "ingenio", "inicio", "inmenso", "inmune", "innato", "insecto", "instante", "interés", "íntimo", "intuir", "inútil", "invierno", "ira", "iris", "ironía", "isla", "islote", "jabalí", "jabón", "jamón", "jarabe", "jardín", "jarra", "jaula", "jazmín", "jefe", "jeringa", "jinete", "jornada", "joroba", "joven", "joya", "juerga", "jueves", "juez", "jugador", "jugo", "juguete", "juicio", "junco", "jungla", "junio", "juntar", "júpiter", "jurar", "justo", "juvenil", "juzgar", "kilo", "koala", "labio", "lacio", "lacra", "lado", "ladrón", "lagarto", "lágrima", "laguna", "laico", "lamer", "lámina", "lámpara", "lana", "lancha", "langosta", "lanza", "lápiz", "largo", "larva", "lástima", "lata", "látex", "latir", "laurel", "lavar", "lazo", "leal", "lección", "leche", "lector", "leer", "legión", "legumbre", "lejano", "lengua", "lento", "leña", "león", "leopardo", "lesión", "letal", "letra", "leve", "leyenda", "libertad", "libro", "licor", "líder", "lidiar", "lienzo", "liga", "ligero", "lima", "límite", "limón", "limpio", "lince", "lindo", "línea", "lingote", "lino", "linterna", "líquido", "liso", "lista", "litera", "litio", "litro", "llaga", "llama", "llanto", "llave", "llegar", "llenar", "llevar", "llorar", "llover", "lluvia", "lobo", "loción", "loco", "locura", "lógica", "logro", "lombriz", "lomo", "lonja", "lote", "lucha", "lucir", "lugar", "lujo", "luna", "lunes", "lupa", "lustro", "luto", "luz", "maceta", "macho", "madera", "madre", "maduro", "maestro", "mafia", "magia", "mago", "maíz", "maldad", "maleta", "malla", "malo", "mamá", "mambo", "mamut", "manco", "mando", "manejar", "manga", "maniquí", "manjar", "mano", "manso", "manta", "mañana", "mapa", "máquina", "mar", "marco", "marea", "marfil", "margen", "marido", "mármol", "marrón", "martes", "marzo", "masa", "máscara", "masivo", "matar", "materia", "matiz", "matriz", "máximo", "mayor", "mazorca", "mecha", "medalla", "medio", "médula", "mejilla", "mejor", "melena", "melón", "memoria", "menor", "mensaje", "mente", "menú", "mercado", "merengue", "mérito", "mes", "mesón", "meta", "meter", "método", "metro", "mezcla", "miedo", "miel", "miembro", "miga", "mil", "milagro", "militar", "millón", "mimo", "mina", "minero", "mínimo", "minuto", "miope", "mirar", "misa", "miseria", "misil", "mismo", "mitad", "mito", "mochila", "moción", "moda", "modelo", "moho", "mojar", "molde", "moler", "molino", "momento", "momia", "monarca", "moneda", "monja", "monto", "moño", "morada", "morder", "moreno", "morir", "morro", "morsa", "mortal", "mosca", "mostrar", "motivo", "mover", "móvil", "mozo", "mucho", "mudar", "mueble", "muela", "muerte", "muestra", "mugre", "mujer", "mula", "muleta", "multa", "mundo", "muñeca", "mural", "muro", "músculo", "museo", "musgo", "música", "muslo", "nácar", "nación", "nadar", "naipe", "naranja", "nariz", "narrar", "nasal", "natal", "nativo", "natural", "náusea", "naval", "nave", "navidad", "necio", "néctar", "negar", "negocio", "negro", "neón", "nervio", "neto", "neutro", "nevar", "nevera", "nicho", "nido", "niebla", "nieto", "niñez", "niño", "nítido", "nivel", "nobleza", "noche", "nómina", "noria", "norma", "norte", "nota", "noticia", "novato", "novela", "novio", "nube", "nuca", "núcleo", "nudillo", "nudo", "nuera", "nueve", "nuez", "nulo", "número", "nutria", "oasis", "obeso", "obispo", "objeto", "obra", "obrero", "observar", "obtener", "obvio", "oca", "ocaso", "océano", "ochenta", "ocho", "ocio", "ocre", "octavo", "octubre", "oculto", "ocupar", "ocurrir", "odiar", "odio", "odisea", "oeste", "ofensa", "oferta", "oficio", "ofrecer", "ogro", "oído", "oír", "ojo", "ola", "oleada", "olfato", "olivo", "olla", "olmo", "olor", "olvido", "ombligo", "onda", "onza", "opaco", "opción", "ópera", "opinar", "oponer", "optar", "óptica", "opuesto", "oración", "orador", "oral", "órbita", "orca", "orden", "oreja", "órgano", "orgía", "orgullo", "oriente", "origen", "orilla", "oro", "orquesta", "oruga", "osadía", "oscuro", "osezno", "oso", "ostra", "otoño", "otro", "oveja", "óvulo", "óxido", "oxígeno", "oyente", "ozono", "pacto", "padre", "paella", "página", "pago", "país", "pájaro", "palabra", "palco", "paleta", "pálido", "palma", "paloma", "palpar", "pan", "panal", "pánico", "pantera", "pañuelo", "papá", "papel", "papilla", "paquete", "parar", "parcela", "pared", "parir", "paro", "párpado", "parque", "párrafo", "parte", "pasar", "paseo", "pasión", "paso", "pasta", "pata", "patio", "patria", "pausa", "pauta", "pavo", "payaso", "peatón", "pecado", "pecera", "pecho", "pedal", "pedir", "pegar", "peine", "pelar", "peldaño", "pelea", "peligro", "pellejo", "pelo", "peluca", "pena", "pensar", "peñón", "peón", "peor", "pepino", "pequeño", "pera", "percha", "perder", "pereza", "perfil", "perico", "perla", "permiso", "perro", "persona", "pesa", "pesca", "pésimo", "pestaña", "pétalo", "petróleo", "pez", "pezuña", "picar", "pichón", "pie", "piedra", "pierna", "pieza", "pijama", "pilar", "piloto", "pimienta", "pino", "pintor", "pinza", "piña", "piojo", "pipa", "pirata", "pisar", "piscina", "piso", "pista", "pitón", "pizca", "placa", "plan", "plata", "playa", "plaza", "pleito", "pleno", "plomo", "pluma", "plural", "pobre", "poco", "poder", "podio", "poema", "poesía", "poeta", "polen", "policía", "pollo", "polvo", "pomada", "pomelo", "pomo", "pompa", "poner", "porción", "portal", "posada", "poseer", "posible", "poste", "potencia", "potro", "pozo", "prado", "precoz", "pregunta", "premio", "prensa", "preso", "previo", "primo", "príncipe", "prisión", "privar", "proa", "probar", "proceso", "producto", "proeza", "profesor", "programa", "prole", "promesa", "pronto", "propio", "próximo", "prueba", "público", "puchero", "pudor", "pueblo", "puerta", "puesto", "pulga", "pulir", "pulmón", "pulpo", "pulso", "puma", "punto", "puñal", "puño", "pupa", "pupila", "puré", "quedar", "queja", "quemar", "querer", "queso", "quieto", "química", "quince", "quitar", "rábano", "rabia", "rabo", "ración", "radical", "raíz", "rama", "rampa", "rancho", "rango", "rapaz", "rápido", "rapto", "rasgo", "raspa", "rato", "rayo", "raza", "razón", "reacción", "realidad", "rebaño", "rebote", "recaer", "receta", "rechazo", "recoger", "recreo", "recto", "recurso", "red", "redondo", "reducir", "reflejo", "reforma", "refrán", "refugio", "regalo", "regir", "regla", "regreso", "rehén", "reino", "reír", "reja", "relato", "relevo", "relieve", "relleno", "reloj", "remar", "remedio", "remo", "rencor", "rendir", "renta", "reparto", "repetir", "reposo", "reptil", "res", "rescate", "resina", "respeto", "resto", "resumen", "retiro", "retorno", "retrato", "reunir", "revés", "revista", "rey", "rezar", "rico", "riego", "rienda", "riesgo", "rifa", "rígido", "rigor", "rincón", "riñón", "río", "riqueza", "risa", "ritmo", "rito", "rizo", "roble", "roce", "rociar", "rodar", "rodeo", "rodilla", "roer", "rojizo", "rojo", "romero", "romper", "ron", "ronco", "ronda", "ropa", "ropero", "rosa", "rosca", "rostro", "rotar", "rubí", "rubor", "rudo", "rueda", "rugir", "ruido", "ruina", "ruleta", "rulo", "rumbo", "rumor", "ruptura", "ruta", "rutina", "sábado", "saber", "sabio", "sable", "sacar", "sagaz", "sagrado", "sala", "saldo", "salero", "salir", "salmón", "salón", "salsa", "salto", "salud", "salvar", "samba", "sanción", "sandía", "sanear", "sangre", "sanidad", "sano", "santo", "sapo", "saque", "sardina", "sartén", "sastre", "satán", "sauna", "saxofón", "sección", "seco", "secreto", "secta", "sed", "seguir", "seis", "sello", "selva", "semana", "semilla", "senda", "sensor", "señal", "señor", "separar", "sepia", "sequía", "ser", "serie", "sermón", "servir", "sesenta", "sesión", "seta", "setenta", "severo", "sexo", "sexto", "sidra", "siesta", "siete", "siglo", "signo", "sílaba", "silbar", "silencio", "silla", "símbolo", "simio", "sirena", "sistema", "sitio", "situar", "sobre", "socio", "sodio", "sol", "solapa", "soldado", "soledad", "sólido", "soltar", "solución", "sombra", "sondeo", "sonido", "sonoro", "sonrisa", "sopa", "soplar", "soporte", "sordo", "sorpresa", "sorteo", "sostén", "sótano", "suave", "subir", "suceso", "sudor", "suegra", "suelo", "sueño", "suerte", "sufrir", "sujeto", "sultán", "sumar", "superar", "suplir", "suponer", "supremo", "sur", "surco", "sureño", "surgir", "susto", "sutil", "tabaco", "tabique", "tabla", "tabú", "taco", "tacto", "tajo", "talar", "talco", "talento", "talla", "talón", "tamaño", "tambor", "tango", "tanque", "tapa", "tapete", "tapia", "tapón", "taquilla", "tarde", "tarea", "tarifa", "tarjeta", "tarot", "tarro", "tarta", "tatuaje", "tauro", "taza", "tazón", "teatro", "techo", "tecla", "técnica", "tejado", "tejer", "tejido", "tela", "teléfono", "tema", "temor", "templo", "tenaz", "tender", "tener", "tenis", "tenso", "teoría", "terapia", "terco", "término", "ternura", "terror", "tesis", "tesoro", "testigo", "tetera", "texto", "tez", "tibio", "tiburón", "tiempo", "tienda", "tierra", "tieso", "tigre", "tijera", "tilde", "timbre", "tímido", "timo", "tinta", "tío", "típico", "tipo", "tira", "tirón", "titán", "títere", "título", "tiza", "toalla", "tobillo", "tocar", "tocino", "todo", "toga", "toldo", "tomar", "tono", "tonto", "topar", "tope", "toque", "tórax", "torero", "tormenta", "torneo", "toro", "torpedo", "torre", "torso", "tortuga", "tos", "tosco", "toser", "tóxico", "trabajo", "tractor", "traer", "tráfico", "trago", "traje", "tramo", "trance", "trato", "trauma", "trazar", "trébol", "tregua", "treinta", "tren", "trepar", "tres", "tribu", "trigo", "tripa", "triste", "triunfo", "trofeo", "trompa", "tronco", "tropa", "trote", "trozo", "truco", "trueno", "trufa", "tubería", "tubo", "tuerto", "tumba", "tumor", "túnel", "túnica", "turbina", "turismo", "turno", "tutor", "ubicar", "úlcera", "umbral", "unidad", "unir", "universo", "uno", "untar", "uña", "urbano", "urbe", "urgente", "urna", "usar", "usuario", "útil", "utopía", "uva", "vaca", "vacío", "vacuna", "vagar", "vago", "vaina", "vajilla", "vale", "válido", "valle", "valor", "válvula", "vampiro", "vara", "variar", "varón", "vaso", "vecino", "vector", "vehículo", "veinte", "vejez", "vela", "velero", "veloz", "vena", "vencer", "venda", "veneno", "vengar", "venir", "venta", "venus", "ver", "verano", "verbo", "verde", "vereda", "verja", "verso", "verter", "vía", "viaje", "vibrar", "vicio", "víctima", "vida", "vídeo", "vidrio", "viejo", "viernes", "vigor", "vil", "villa", "vinagre", "vino", "viñedo", "violín", "viral", "virgo", "virtud", "visor", "víspera", "vista", "vitamina", "viudo", "vivaz", "vivero", "vivir", "vivo", "volcán", "volumen", "volver", "voraz", "votar", "voto", "voz", "vuelo", "vulgar", "yacer", "yate", "yegua", "yema", "yerno", "yeso", "yodo", "yoga", "yogur", "zafiro", "zanja", "zapato", "zarza", "zona", "zorro", "zumo", "zurdo"] + +def test_seed(): + + seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash", wordlist=bip39.WORDLIST) + + # valid seed? + assert seed + + assert seed.seed == b'q\xb3\xd1i\x0c\x9b\x9b\xdf\xa7\xd9\xd97H\xa8,\xa7\xd9>\xeck\xc2\xf5ND?, \x88-\x07\x9aa\xc5\xee\xb7\xbf\xc4x\xd6\x07 X\xb6}?M\xaa\x05\xa6\xa7(>\xbf\x03\xb0\x9d\xef\xed":\xdf\x88w7' + + assert seed.mnemonic == "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" + + assert seed.passphrase == "" + + seed.wordlist = spanish_wordlist + + assert seed.mnemonic == "natural ayuda futuro nivel espejo abuelo vago bien repetir moreno relevo conga" + + seed.wordlist = bip39.WORDLIST + + seed.mnemonic = "height demise useless trap grow lion found off key clown transfer enroll" + + assert seed.mnemonic == "height demise useless trap grow lion found off key clown transfer enroll" + + seed.wordlist = spanish_wordlist + + assert seed.mnemonic == "hebilla cría truco tigre gris llenar folio negocio laico casa tieso eludir" + + seed.passphrase = "test" + + assert seed.seed == b'\xdd\r\xcb\x0b V\xb4@\xee+\x01`\xabem\xc1B\xfd\x8fba0\xab;[\xab\xc9\xf9\xba[F\x0c5,\x7fd8\xebI\x90"\xb8\x86C\x821\x01\xdb\xbe\xf3\xbc\x1cBH"%\x18\xc2{\x04\x08a]\xa5' + + assert seed.passphrase == "test" + + + + + + + \ No newline at end of file From d6a00c81eea62ed4b177e8d20da9cf4d1f070a00 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Thu, 16 Sep 2021 22:24:13 -0400 Subject: [PATCH 26/40] Add bitcoin address decoding --- src/seedsigner/models/decode_qr.py | 91 ++++++++++++++++++++++++++++++ src/seedsigner/models/qr_type.py | 1 + tests/test_decodepsbtqr.py | 71 ++++++++++++++++++++++- 3 files changed, 162 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index ecb6a5af..24e194d4 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -24,6 +24,7 @@ class DecodeQR: self.legacy_ur = LegacyURDecodeQR() # UR Legacy decoder self.base64_qr = Base64DecodeQR() # Single Segments Base64 self.base43_qr = Base43DecodeQR() # Single Segment Base43 + self.address_qr = BitcoinAddressQR() # Single Segment bitcoin address self.wordlist = None for key, value in kwargs.items(): @@ -108,6 +109,12 @@ class DecodeQR: if rt == DecodeQRStatus.COMPLETE: self.complete = True return rt + + elif self.qr_type == QRType.BITCOINADDRESSQR: + rt = self.address_qr.add(qr_str, QRType.BITCOINADDRESSQR) + if rt == DecodeQRStatus.COMPLETE: + self.complete = True + return rt else: return DecodeQRStatus.INVALID @@ -150,6 +157,12 @@ class DecodeQR: def getSeedPhrase(self): return self.seedqr.getSeedPhrase() + + def getAddress(self): + return self.address_qr.getAddress() + + def getAddressType(self): + return self.address_qr.getAddressType() def getPercentComplete(self) -> int: if self.qr_type == QRType.PSBTUR2: @@ -192,6 +205,11 @@ class DecodeQR: if self.qr_type in (QRType.SEEDSSQR, QRType.SEEDUR2, QRType.SEEDMNEMONIC, QRType.SEED4LETTERMNEMONIC): return True return False + + def isAddress(self): + if self.qr_type in (QRType.BITCOINADDRESSQR): + return True + return False def qrType(self): return self.qr_type @@ -220,6 +238,10 @@ class DecodeQR: elif DecodeQR.isBase64PSBT(s): return QRType.PSBTBASE64 + # Bitcoin Address + if DecodeQR.isBitcoinAddress(s): + return QRType.BITCOINADDRESSQR + _4LETTER_WORDLIST = [word[:4].strip() for word in wordlist] # Seed @@ -297,6 +319,16 @@ class DecodeQR: result.extend(b'\x00' * nPad) result.reverse() return bytes(result) + + @staticmethod + def isBitcoinAddress(s): + + if re.search(r'^bitcoin\:.*', s, re.IGNORECASE): + return True + elif re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', s): + return True + else: + return False ### ### SpecterDecodePSBTQR Class @@ -618,6 +650,65 @@ class SeedQR: return True return False +### +### BitcoinAddressQR Class +### Purpose: used in DecodeQR to decode single frame representing a bitcoin address +### + +class BitcoinAddressQR: + + def __init__(self, wordlist=None): + self.total_segments = 1 + self.collected_segments = 0 + self.complete = False + self.address = None + self.address_type = None + + def add(self, segment, qr_type=QRType.BITCOINADDRESSQR): + + r = re.search(r'((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})', segment) + if r != None: + self.address = r.group(1) + + if re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', self.address) != None: + self.complete = True + self.collected_segments = 1 + + # get address type + r = re.search(r'^((bc1|tb1|[123]|[mn])[a-zA-HJ-NP-Z0-9]{25,62})$', self.address) + if r != None: + r = r.group(2) + + if r == "1": + self.address_type = "P2PKH-main" # Legacy + elif r == "m" or r == "n": + self.address_type = "P2PKH-test" # Legacy + elif r == "3": + self.address_type = "P2SH-main" # Nested Segwit Single Sig (P2WPKH in P2SH) or Multisig (P2WSH in P2SH) + elif r == "2": + self.address_type = "P2SH-test" # Nested Segwit Single Sig (P2WPKH in P2SH) or Multisig (P2WSH in P2SH) + elif r == "bc1": + self.address_type = "Bech32-main" + elif r == "tb1": + self.address_type = "Bech32-test" + + return DecodeQRStatus.COMPLETE + + return DecodeQRStatus.INVALID + + def getAddress(self): + if self.address != None: + return self.address + return None + + def getAddressType(self): + if self.address != None: + if self.address_type != None: + return self.address_type + else: + return "Unknown" + return None + ### ### DecodeQRStatus Class IntEum ### Purpose: used in DecodeQR to communicate status of adding qr frame/segment diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py index a3c2490d..0bb7919e 100644 --- a/src/seedsigner/models/qr_type.py +++ b/src/seedsigner/models/qr_type.py @@ -17,4 +17,5 @@ class QRType(IntEnum): SEED4LETTERMNEMONIC = 9 XPUBQR = 20 SPECTERXPUBQR = 21 + BITCOINADDRESSQR = 30 INVALID = 100 \ No newline at end of file diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index 0b9d2840..98ba3b1b 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -333,7 +333,76 @@ def test_short_4_letter_mnemonic_qr(): assert d.isComplete() == True assert d.getSeedPhrase() == ["height", "demise", "useless", "trap", "grow", "lion", "found", "off", "key", "clown", "transfer", "enroll"] - +def test_bitcoin_address(): + + bad1 = "loremipsum" + bad2 = "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae" + bad3 = "121802020768124106400009195602431595117715840445" + + legacy_address1 = "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY" + legacy_address2 = "16ftSEQ4ctQFDtVZiUBusQUjRrGhM3JYwe" + + main_bech32_address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + test_bech32_address = "tb1qkurj377gtlmu0j5flcykcsh2xagexh9h3jk06a" + + main_nested_segwit_address = "3Nu78Cqcf6hsD4sUBAN9nP13tYiHU9QPFX" + test_nested_segwit_address = "2N6JbrvPMMwbBhu2KxqXyyHUQz3XKspvyfm" + + main_bech32_address2 = "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=12000" + main_bech32_address3 = "BITCOIN:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?junk" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(bad1) + + assert d.qrType() == QRType.INVALID + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(legacy_address1) + + assert d.getAddress() == legacy_address1 + assert d.getAddressType() == "P2PKH-main" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(legacy_address2) + + assert d.getAddress() == legacy_address2 + assert d.getAddressType() == "P2PKH-main" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(main_bech32_address) + + assert d.getAddress() == main_bech32_address + assert d.getAddressType() == "Bech32-main" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(test_bech32_address) + + assert d.getAddress() == test_bech32_address + assert d.getAddressType() == "Bech32-test" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(main_nested_segwit_address) + + assert d.getAddress() == main_nested_segwit_address + assert d.getAddressType() == "P2SH-main" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(test_nested_segwit_address) + + assert d.getAddress() == test_nested_segwit_address + assert d.getAddressType() == "P2SH-test" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(main_bech32_address2) + + assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + assert d.getAddressType() == "Bech32-main" + + d = DecodeQR(wordlist=bip39.WORDLIST) + d.addString(main_bech32_address3) + + assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + assert d.getAddressType() == "Bech32-main" From 98a5d17f7137b001a2df6a4e07cce22258a5b7ab Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 17 Sep 2021 18:36:53 -0500 Subject: [PATCH 27/40] Persistent camera rotation support --- src/default_settings.ini | 1 + src/seedsigner/camera.py | 39 +++++++-------------- src/seedsigner/controller.py | 13 +++++-- src/seedsigner/helpers/path.py | 1 + src/seedsigner/models/settings.py | 18 +++++++++- src/seedsigner/views/menu_view.py | 8 +++-- src/seedsigner/views/seed_tools_view.py | 8 ++--- src/seedsigner/views/settings_tools_view.py | 22 ++++++++++++ src/seedsigner/views/view.py | 2 +- src/settings.ini | 3 +- 10 files changed, 76 insertions(+), 39 deletions(-) diff --git a/src/default_settings.ini b/src/default_settings.ini index 5d1a99ee..77a8ae99 100644 --- a/src/default_settings.ini +++ b/src/default_settings.ini @@ -5,6 +5,7 @@ persistent_settings = False [display] text_color = ORANGE +camera_rotation = 0 [wallet] network = main diff --git a/src/seedsigner/camera.py b/src/seedsigner/camera.py index e2af97be..edac1702 100644 --- a/src/seedsigner/camera.py +++ b/src/seedsigner/camera.py @@ -10,6 +10,16 @@ from seedsigner.helpers import PiVideoStream, Singleton class Camera(Singleton): _video_stream = None _picamera = None + _camera_rotation = None + + @classmethod + def get_instance(cls): + # This is the only way to access the one and only Controller + from seedsigner.models import Settings + if cls._instance is None: + cls._instance = cls.__new__(cls) + cls._instance._camera_rotation = Settings.get_instance().camera_rotation + return cls._instance def start_video_stream_mode(self, resolution=(512, 384), framerate=12, format="bgr"): @@ -28,7 +38,7 @@ class Camera(Singleton): return frame else: if frame is not None: - return Image.fromarray(frame.astype('uint8'), 'RGB').rotate(90) + return Image.fromarray(frame.astype('uint8'), 'RGB').rotate(90 + self._camera_rotation) return None @@ -59,37 +69,12 @@ class Camera(Singleton): self._picamera.awb_mode = 'off' self._picamera.awb_gains = g - """ - When outputting to unencoded formats, the camera rounds the requested resolution. - The horizontal resolution is rounded up to the nearest multiple of 32 pixels, - while the vertical resolution is rounded up to the nearest multiple of 16 pixels. - """ - # resolution = self._picamera.resolution - # if resolution.width % 32 == 0: - # raw_width = resolution.width - # else: - # raw_width = resolution.width - (resolution.width % 32) + 32 - - # if resolution.height % 16 == 0: - # raw_height = resolution.height - # else: - # raw_height = resolution.height + 16 - - # print(resolution) - # print(raw_width, raw_height) - # np_array = numpy.empty((raw_width, raw_height, 3), dtype=numpy.uint8) - # self._picamera.capture(np_array, format='rgb') - - # np_array.reshape((raw_width, raw_height, 3)) - - # return Image.fromarray(np_array[:resolution.width, :resolution.height, :].astype('uint8'), 'RGB') - stream = io.BytesIO() self._picamera.capture(stream, format='jpeg') # "Rewind" the stream to the beginning so we can read its content stream.seek(0) - return Image.open(stream) + return Image.open(stream).rotate(90 + self._camera_rotation) def stop_single_frame_mode(self): diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 085bd2ef..8b2440bd 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -73,7 +73,8 @@ class Controller(Singleton): controller.settings_tools_view = SettingsToolsView() controller.screensaver = ScreensaverView(controller.buttons) - controller.screensaver_activation_ms = 60 * 1000 + controller.screensaver_activation_ms = 120 * 1000 + @property def camera(self): @@ -153,6 +154,8 @@ class Controller(Singleton): ret_val = self.show_qr_density_tool() elif ret_val == Path.PERSISTENT_SETTINGS: ret_val = self.show_persistent_settings_tool() + elif ret_val == Path.CAMERA_ROTATION: + ret_val = self.show_camera_rotation_tool() elif ret_val == Path.DONATE: ret_val = self.show_donate_tool() elif ret_val == Path.RESET: @@ -830,7 +833,13 @@ class Controller(Singleton): return Path.SETTINGS_SUB_MENU - ### Show Donate Screen and QR + + def show_camera_rotation_tool(self): + r = self.settings_tools_view.display_camera_rotation() + if r is not None: + self.settings.camera_rotation = r + + return Path.SETTINGS_SUB_MENU ### Show Donate Screen and QR def show_donate_tool(self): self.settings_tools_view.display_donate_info_screen() diff --git a/src/seedsigner/helpers/path.py b/src/seedsigner/helpers/path.py index 2873aed4..f641c459 100644 --- a/src/seedsigner/helpers/path.py +++ b/src/seedsigner/helpers/path.py @@ -28,6 +28,7 @@ class Path: WALLET = 64 QR_DENSITY_SETTING = 65 PERSISTENT_SETTINGS = 67 + CAMERA_ROTATION = 69 RESET = 68 # Seed Slots diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 0fcf9ad4..295e7058 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -5,6 +5,8 @@ from .encode_qr_density import EncodeQRDensity import configparser from embit import bip39 + + class Settings(Singleton): @classmethod @@ -13,7 +15,7 @@ class Settings(Singleton): if cls._instance: raise Exception("Instance already configured") - # Instantiate the one and only Controller instance + # Instantiate the one and only instance settings = cls.__new__(cls) cls._instance = settings @@ -27,6 +29,7 @@ class Settings(Singleton): }, 'display': { 'text_color': "ORANGE", + 'camera_rotation': 0, }, 'wallet': { 'network': "main", @@ -49,6 +52,7 @@ class Settings(Singleton): self._data["system"]["debug"] = config.getboolean("system", "debug") self._data["system"]["default_language"] = config["system"]["default_language"] self._data["display"]["text_color"] = config["display"]["text_color"] + self._data["display"]["camera_rotation"] = int(config["display"]["camera_rotation"]) self.network = config["wallet"]["network"] self.software = config["wallet"]["software"] self.qr_density = int(config["wallet"]["qr_density"]) @@ -125,6 +129,18 @@ class Settings(Singleton): def text_color(self): return self._data["display"]["text_color"] + @property + def camera_rotation(self): + return self._data["display"]["camera_rotation"] + + @camera_rotation.setter + def camera_rotation(self, value): + if value in [0, 90, 180, 270]: + self._data["display"]["camera_rotation"] = value + self.__writeConfig() + else: + raise Exception("Unexpected display.camera_rotation settings.ini value") + ### wallet @property diff --git a/src/seedsigner/views/menu_view.py b/src/seedsigner/views/menu_view.py index 68a60603..6fcbd1a9 100644 --- a/src/seedsigner/views/menu_view.py +++ b/src/seedsigner/views/menu_view.py @@ -124,7 +124,7 @@ class MenuView(View): ### Settings Menu def display_settings_menu(self) -> int: - lines = ["... [ Return to Main ]", "Wallet: ", "Network: ", "QR Density: ", "Input / Output Tests", "Persistent Settings: ", "Version Info", "Donate to SeedSigner", "Reset SeedSigner"] + lines = ["... [ Return to Main ]", "Wallet: ", "Network: ", "QR Density: ", "Input / Output Tests", "Persistent Settings: ", "Camera Rotation", "Version Info", "Donate to SeedSigner", "Reset SeedSigner"] input = 0 lines[1] = lines[1].replace("", Settings.get_instance().software) @@ -157,10 +157,12 @@ class MenuView(View): elif self.selected_menu_num == 6: return Path.PERSISTENT_SETTINGS elif self.selected_menu_num == 7: - return Path.VERSION_INFO + return Path.CAMERA_ROTATION elif self.selected_menu_num == 8: - return Path.DONATE + return Path.VERSION_INFO elif self.selected_menu_num == 9: + return Path.DONATE + elif self.selected_menu_num == 10: return Path.RESET raise Exception("Unhandled case") diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index dd16389d..a3ab85e3 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -1064,10 +1064,12 @@ class SeedToolsView(View): draw.rectangle((mask_width, 0, View.canvas_width - mask_width, pixels_per_block), fill=View.color) draw.rectangle((0, mask_height, pixels_per_block, View.canvas_height - mask_height), fill=View.color) - label_font = View.COURIERNEW24 + label_font = View.ASSISTANT26 x_label = block_labels_x[cur_block_x] tw, th = draw.textsize(x_label, font=label_font) - draw.text(((View.canvas_width - tw) / 2, (pixels_per_block - th) / 2), x_label, fill="BLACK", font=label_font) + + # note: have to nudge the y-coord up (the extra "- 4") for some reason + draw.text(((View.canvas_width - tw) / 2, ((pixels_per_block - th) / 2) - 4), x_label, fill="BLACK", font=label_font) y_label = block_labels_y[cur_block_y] tw, th = draw.textsize(y_label, font=label_font) @@ -1209,8 +1211,6 @@ class SeedToolsView(View): display_version = autocontrast( seed_entropy_image, cutoff=2 - ).rotate( - 90 ).crop( (120, 0, 600, 480) ).resize( diff --git a/src/seedsigner/views/settings_tools_view.py b/src/seedsigner/views/settings_tools_view.py index 1b533716..36cef358 100644 --- a/src/seedsigner/views/settings_tools_view.py +++ b/src/seedsigner/views/settings_tools_view.py @@ -79,6 +79,28 @@ class SettingsToolsView(View): else: return None + + def display_camera_rotation(self) -> int: + lines = ["... [ Return to Settings ]"] + lines.append("0 (default)") + lines.append("90") + lines.append("180") + lines.append("270") + + r = self.controller.menu_view.display_generic_selection_menu(lines, "Camera Rotation") + if r == 1: + return None + elif r == 2: + return 0 + elif r == 3: + return 90 + elif r == 4: + return 180 + elif r == 5: + return 270 + else: + return None + ### ### Version Info ### diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index d1770c28..dda71f3a 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -158,7 +158,7 @@ class View: text_overlay = Image.new("RGBA", (View.canvas_width, View.canvas_height), (255,255,255,0)) text_overlay_draw = ImageDraw.Draw(text_overlay) if not font: - font = View.COURIERNEW14 + font = View.ASSISTANT18 tw, th = text_overlay_draw.textsize(text, font=font) if text_background: text_overlay_draw.rectangle(((240 - tw) / 2 - 3, 240 - th, (240 - tw) / 2 + tw + 3, 240), fill=text_background) diff --git a/src/settings.ini b/src/settings.ini index 17101ca6..650f06ae 100644 --- a/src/settings.ini +++ b/src/settings.ini @@ -5,9 +5,10 @@ persistent_settings = False [display] text_color = ORANGE +camera_rotation = 180 [wallet] network = main software = Prompt qr_density = 2 -custom_derivation = m/0 \ No newline at end of file +custom_derivation = m/0/0 \ No newline at end of file From a82a6806306e5b0b623cdee39370688967025809 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Mon, 11 Oct 2021 21:06:22 -0400 Subject: [PATCH 28/40] add bitcoin address verification --- src/seedsigner/controller.py | 203 ++++++++++++++++++++++++++++- src/seedsigner/models/decode_qr.py | 132 +++++++++++++++++-- src/seedsigner/models/qr_type.py | 3 + tests/test_decodepsbtqr.py | 38 +++++- 4 files changed, 363 insertions(+), 13 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 085bd2ef..eaaf46ce 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -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]) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 24e194d4..9d07ed78 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -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 diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py index 0bb7919e..984e5d4c 100644 --- a/src/seedsigner/models/qr_type.py +++ b/src/seedsigner/models/qr_type.py @@ -18,4 +18,7 @@ class QRType(IntEnum): XPUBQR = 20 SPECTERXPUBQR = 21 BITCOINADDRESSQR = 30 + SPECTERWALLETQR = 40 + URWALLETQR = 41 + BLUEWALLETQR = 42 INVALID = 100 \ No newline at end of file diff --git a/tests/test_decodepsbtqr.py b/tests/test_decodepsbtqr.py index 98ba3b1b..270ae0f7 100644 --- a/tests/test_decodepsbtqr.py +++ b/tests/test_decodepsbtqr.py @@ -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 + From ccbd2e305242b7208bf059764885d590187d946c Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Wed, 13 Oct 2021 15:23:43 -0400 Subject: [PATCH 29/40] add up/down joystick for qr brightness --- src/default_settings.ini | 1 + src/seedsigner/controller.py | 57 ++++++++++++++++++++++++++---- src/seedsigner/helpers/qr.py | 4 +-- src/seedsigner/models/encode_qr.py | 4 +-- src/seedsigner/models/settings.py | 11 ++++++ src/settings.ini | 1 + 6 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/default_settings.ini b/src/default_settings.ini index 5d1a99ee..de7b047b 100644 --- a/src/default_settings.ini +++ b/src/default_settings.ini @@ -5,6 +5,7 @@ persistent_settings = False [display] text_color = ORANGE +qr_background_color = FFFFFF [wallet] network = main diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index eaaf46ce..8b806485 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -66,6 +66,7 @@ class Controller(Singleton): # settings controller.DEBUG = controller.settings.debug controller.color = controller.settings.text_color + controller.current_bg_qr_color = controller.settings.qr_background_color # Views controller.menu_view = MenuView() @@ -527,16 +528,26 @@ class Controller(Singleton): e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=derivation, network=self.settings.network, qr_type=qr_xpub_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) while e.totalParts() > 1: - image = e.nextPartImage(240,240,2) + image = e.nextPartImage(240,240,2,background=self.current_bg_qr_color) View.DispShowImage(image) time.sleep(0.1) if self.buttons.check_for_low(B.KEY_RIGHT): - break + break + elif self.buttons.check_for_low(B.KEY_UP): + self.prev_qr_background_color() + elif self.buttons.check_for_low(B.KEY_DOWN): + self.next_qr_background_color() - if e.totalParts() == 1: - image = e.nextPartImage(240,240,1) + while e.totalParts() == 1: + image = e.nextPartImage(240,240,1,background=self.current_bg_qr_color) View.DispShowImage(image) - self.buttons.wait_for([B.KEY_RIGHT]) + input = self.buttons.wait_for([B.KEY_RIGHT,B.KEY_UP,B.KEY_DOWN]) + if input == B.KEY_RIGHT: + break + elif input == B.KEY_UP: + self.prev_qr_background_color() + elif input == B.KEY_DOWN: + self.next_qr_background_color() return Path.MAIN_MENU @@ -951,11 +962,15 @@ class Controller(Singleton): self.menu_view.draw_modal(["Generating PSBT QR ..."]) e = EncodeQR(psbt=trimmed_psbt, qr_type=self.settings.qr_psbt_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) while True: - image = e.nextPartImage(240,240,1) + image = e.nextPartImage(240,240,1,background=self.current_bg_qr_color) View.DispShowImage(image) time.sleep(0.05) if self.buttons.check_for_low(B.KEY_RIGHT): - break + break + elif self.buttons.check_for_low(B.KEY_UP): + self.prev_qr_background_color() + elif self.buttons.check_for_low(B.KEY_DOWN): + self.next_qr_background_color() # Return to Main Menu return Path.MAIN_MENU @@ -1029,6 +1044,34 @@ class Controller(Singleton): return Path.SETTINGS_SUB_MENU + ### next_qr_background_colors() + + def next_qr_background_color(self): + if self.current_bg_qr_color == "FFFFFF": + self.current_bg_qr_color = "DDDDDD" + elif self.current_bg_qr_color == "DDDDDD": + self.current_bg_qr_color = "BBBBBB" + elif self.current_bg_qr_color == "BBBBBB": + self.current_bg_qr_color = "999999" + elif self.current_bg_qr_color == "999999": + self.current_bg_qr_color = "FFFFFF" + + self.settings.qr_background_color = self.current_bg_qr_color + + ### prev_qr_background_colors() + + def prev_qr_background_color(self): + if self.current_bg_qr_color == "999999": + self.current_bg_qr_color = "BBBBBB" + elif self.current_bg_qr_color == "BBBBBB": + self.current_bg_qr_color = "DDDDDD" + elif self.current_bg_qr_color == "DDDDDD": + self.current_bg_qr_color = "FFFFFF" + elif self.current_bg_qr_color == "FFFFFF": + self.current_bg_qr_color = "999999" + + self.settings.qr_background_color = self.current_bg_qr_color + ### Show Donate Screen and QR def show_donate_tool(self): diff --git a/src/seedsigner/helpers/qr.py b/src/seedsigner/helpers/qr.py index 38ad7bd0..59933044 100644 --- a/src/seedsigner/helpers/qr.py +++ b/src/seedsigner/helpers/qr.py @@ -13,13 +13,13 @@ class QR: qr.make(fit=True) return(qr.make_image(fill_color="black", back_color="white").resize((width,height)).convert('RGB')) - def qrimage_io(self, data, width=240, height=240, border=3): + def qrimage_io(self, data, width=240, height=240, border=3, background="FFFFFF"): if 1 <= border <= 10: border_str = str(border) else: border_str = "3" - cmd = 'qrencode -m '+border_str+' -s 3 -l L --foreground=000000 --background=FFFFFF -t PNG -o "/dev/shm/qrcode.png" "' + str(data) + '"' + cmd = 'qrencode -m '+border_str+' -s 3 -l L --foreground=000000 --background='+background+' -t PNG -o "/dev/shm/qrcode.png" "' + str(data) + '"' rv = subprocess.call(cmd, shell=True) # if qrencode fails, fall back to only encoder diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 842811d9..014f8c74 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -79,9 +79,9 @@ class EncodeQR: def part2Image(self, part, width=240, height=240, border=3): return self.qr.qrimage_io(part, width, height, border) - def nextPartImage(self, width=240, height=240, border=3): + def nextPartImage(self, width=240, height=240, border=3, background="FFFFFF"): part = self.nextPart() - return self.qr.qrimage_io(part, width, height, border) + return self.qr.qrimage_io(part, width, height, border, background=background) def isComplete(self): return self.encoder.isComplete() diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 0fcf9ad4..54799080 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -27,6 +27,7 @@ class Settings(Singleton): }, 'display': { 'text_color': "ORANGE", + 'qr_background_color': "FFFFFF" }, 'wallet': { 'network': "main", @@ -53,6 +54,7 @@ class Settings(Singleton): self.software = config["wallet"]["software"] self.qr_density = int(config["wallet"]["qr_density"]) self.custom_derivation = config["wallet"]["custom_derivation"] + self.qr_background_color = config["display"]["qr_background_color"] ### persistent settings handling @@ -124,6 +126,15 @@ class Settings(Singleton): @property def text_color(self): return self._data["display"]["text_color"] + + @property + def qr_background_color(self): + return self._data["display"]["qr_background_color"] + + @qr_background_color.setter + def qr_background_color(self, value): + self._data["display"]["qr_background_color"] = value + self.__writeConfig() ### wallet diff --git a/src/settings.ini b/src/settings.ini index 17101ca6..a5876498 100644 --- a/src/settings.ini +++ b/src/settings.ini @@ -5,6 +5,7 @@ persistent_settings = False [display] text_color = ORANGE +qr_background_color = FFFFFF [wallet] network = main From 5422aaf0d73871977aa557dd616649d07dccb76e Mon Sep 17 00:00:00 2001 From: jpph Date: Wed, 13 Oct 2021 21:38:58 +0200 Subject: [PATCH 30/40] make dice seed generation verifiable outside seedsigner --- src/seedsigner/helpers/mnemonic_generation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 5b3f8a44..d7baf472 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -1,7 +1,7 @@ from embit import bip39 from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes import unicodedata - +import hashlib def calculate_checksum(partial_mnemonic: list, wordlist): @@ -28,8 +28,7 @@ def generate_mnemonic_from_bytes(entropy_bytes): def generate_mnemonic_from_dice(roll_data: str): - entropyinteger = int(roll_data, 6) - entropy_bytes = entropyinteger.to_bytes(32, byteorder="little") + entropy_bytes = hashlib.sha256(roll_data.encode()).digest() # Return as a list return bip39.mnemonic_from_bytes(entropy_bytes).split() From 6e24a9566c57a63f002b44f18c1163d5bc41acf3 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Thu, 14 Oct 2021 10:29:48 -0400 Subject: [PATCH 31/40] fix seed qr displayed consistantly both zoomed and not zoomed in --- src/seedsigner/helpers/qr.py | 2 +- src/seedsigner/models/encode_qr.py | 5 ++++- src/seedsigner/views/seed_tools_view.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/helpers/qr.py b/src/seedsigner/helpers/qr.py index 59933044..1857201f 100644 --- a/src/seedsigner/helpers/qr.py +++ b/src/seedsigner/helpers/qr.py @@ -11,7 +11,7 @@ class QR: qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=border ) qr.add_data(data) qr.make(fit=True) - return(qr.make_image(fill_color="black", back_color="white").resize((width,height)).convert('RGB')) + return(qr.make_image(fill_color="black", back_color="white").resize((width,height)).convert('RGBA')) def qrimage_io(self, data, width=240, height=240, border=3, background="FFFFFF"): if 1 <= border <= 10: diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 014f8c74..394113e2 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -81,7 +81,10 @@ class EncodeQR: def nextPartImage(self, width=240, height=240, border=3, background="FFFFFF"): part = self.nextPart() - return self.qr.qrimage_io(part, width, height, border, background=background) + if self.qr_type == QRType.SEEDSSQR: + return self.qr.qrimage(part, width, height, border) + else: + return self.qr.qrimage_io(part, width, height, border, background=background) def isComplete(self): return self.encoder.isComplete() diff --git a/src/seedsigner/views/seed_tools_view.py b/src/seedsigner/views/seed_tools_view.py index dd16389d..137caa74 100644 --- a/src/seedsigner/views/seed_tools_view.py +++ b/src/seedsigner/views/seed_tools_view.py @@ -1064,7 +1064,7 @@ class SeedToolsView(View): draw.rectangle((mask_width, 0, View.canvas_width - mask_width, pixels_per_block), fill=View.color) draw.rectangle((0, mask_height, pixels_per_block, View.canvas_height - mask_height), fill=View.color) - label_font = View.COURIERNEW24 + label_font = View.ROBOTOMONO24 x_label = block_labels_x[cur_block_x] tw, th = draw.textsize(x_label, font=label_font) draw.text(((View.canvas_width - tw) / 2, (pixels_per_block - th) / 2), x_label, fill="BLACK", font=label_font) From b287b6d31f1dca35bedd421c940767ec505b0153 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Tue, 19 Oct 2021 15:17:13 -0400 Subject: [PATCH 32/40] add another bitcoin output descriptor regex parse --- src/seedsigner/controller.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 80debdba..b2c8fe19 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -836,12 +836,17 @@ class Controller(Singleton): if decoder2.isComplete() and decoder2.isWalletDescriptor(): desc_str = decoder2.getWalletDescriptor() + desc_str = desc_str.replace("\n","").replace(" ","") try: - if len(re.findall(r'\[([0-9,a-f,A-F]+?)(\/[0-9,\/,h\']+?)\].*?(\/0\/\*)', desc_str)) > 0: + 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) + elif len(re.findall (r'(\[[0-9,a-f,A-F]+?\/[0-9,\/,h,\']+?\][a-z,A-Z,0-9]*?)([\,,\)])', desc_str)) > 0: + p = re.compile(r'(\[[0-9,a-f,A-F]+?\/[0-9,\/,h,\']+?\][a-z,A-Z,0-9]*?)([\,,\)])') + desc_str = p.sub(r'\1/{0,1}/*\2', desc_str) except: - desc_str = decoder2.getWalletDescriptor() + desc_str = decoder2.getWalletDescriptor() + print(desc_str) desc = Descriptor.from_string(desc_str) else: return Path.MAIN_MENU From 2612cda43eb97c139e5dac5ba4e466e52425d2bd Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Tue, 19 Oct 2021 15:24:20 -0400 Subject: [PATCH 33/40] correct default camera rotation back to 0 degrees --- src/settings.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings.ini b/src/settings.ini index 2a37f377..2aeffa0d 100644 --- a/src/settings.ini +++ b/src/settings.ini @@ -6,7 +6,7 @@ persistent_settings = False [display] text_color = ORANGE qr_background_color = FFFFFF -camera_rotation = 180 +camera_rotation = 0 [wallet] network = main From f39961d3e1bdd672dd0f23f20ec86b3fc4eb4974 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Tue, 19 Oct 2021 15:34:51 -0400 Subject: [PATCH 34/40] fix when descriptor has newlines/whitespace --- src/seedsigner/models/decode_qr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 9d07ed78..07f90b9c 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -255,10 +255,11 @@ class DecodeQR: return QRType.PSBTBASE64 # Wallet Descriptor - elif re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE): + desc_str = s.replace("\n","").replace(" ","") + if 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): + elif re.search(r'^\{\"label\".*\"descriptor\"\:.*', desc_str, re.IGNORECASE): # if json starting with label and contains descriptor, assume specter wallet json return QRType.SPECTERWALLETQR From 3005ca10c66a610d040374128c514239c0130f6f Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Thu, 21 Oct 2021 16:29:18 -0400 Subject: [PATCH 35/40] darker qr option and better bitcoin address formatting --- src/seedsigner/controller.py | 28 ++++++++++++++++++---------- src/seedsigner/views/view.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index b2c8fe19..7e3263bb 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -673,12 +673,8 @@ class Controller(Singleton): 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.menu_view.draw_address(address) self.buttons.wait_for([B.KEY_RIGHT]) validate_network = NETWORKS[self.settings.network] @@ -714,7 +710,7 @@ class Controller(Singleton): 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 ]") + ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Cancel ]") 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: @@ -1065,21 +1061,33 @@ class Controller(Singleton): elif self.current_bg_qr_color == "BBBBBB": self.current_bg_qr_color = "999999" elif self.current_bg_qr_color == "999999": + self.current_bg_qr_color = "777777" + elif self.current_bg_qr_color == "777777": + self.current_bg_qr_color = "555555" + elif self.current_bg_qr_color == "555555": + self.current_bg_qr_color = "333333" + elif self.current_bg_qr_color == "333333": self.current_bg_qr_color = "FFFFFF" - + self.settings.qr_background_color = self.current_bg_qr_color ### prev_qr_background_colors() def prev_qr_background_color(self): - if self.current_bg_qr_color == "999999": + if self.current_bg_qr_color == "333333": + self.current_bg_qr_color = "555555" + elif self.current_bg_qr_color == "555555": + self.current_bg_qr_color = "777777" + elif self.current_bg_qr_color == "777777": + self.current_bg_qr_color = "999999" + elif self.current_bg_qr_color == "999999": self.current_bg_qr_color = "BBBBBB" elif self.current_bg_qr_color == "BBBBBB": self.current_bg_qr_color = "DDDDDD" elif self.current_bg_qr_color == "DDDDDD": self.current_bg_qr_color = "FFFFFF" elif self.current_bg_qr_color == "FFFFFF": - self.current_bg_qr_color = "999999" + self.current_bg_qr_color = "333333" self.settings.qr_background_color = self.current_bg_qr_color diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index dda71f3a..db46ed08 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -207,6 +207,37 @@ class View: return + def draw_address(self, address) -> None: + + if address.startswith(tuple(['1','2','3'])): + dividby = 14 + else: + dividby = 18 + address_parts = [address[i:i+dividby] for i in range(0, len(address), dividby)] + 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 "" + address_part_4 = address_parts[3] if len(address_parts) >= 4 else "" + + View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) + + tw, th = View.draw.textsize("Right to Continue", font=View.ASSISTANT18) + View.draw.text(((240 - tw) / 2, 210), "Right to Continue", fill=View.color, font=View.ASSISTANT18) + + tw, th = View.draw.textsize("Bitcoin Address", font=View.ASSISTANT22) + View.draw.text(((240 - tw) / 2, 35), "Bitcoin Address", fill=View.color, font=View.ASSISTANT22) + tw, th = View.draw.textsize(address_part_1, font=View.ROBOTOCONDENSED_REGULAR_22) + View.draw.text(((240 - tw) / 2, 70), address_part_1, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_22) + tw, th = View.draw.textsize(address_part_2, font=View.ROBOTOCONDENSED_REGULAR_22) + View.draw.text(((240 - tw) / 2, 105), address_part_2, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_22) + tw, th = View.draw.textsize(address_part_3, font=View.ROBOTOCONDENSED_REGULAR_22) + View.draw.text(((240 - tw) / 2, 140), address_part_3, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_22) + tw, th = View.draw.textsize(address_part_4, font=View.ROBOTOCONDENSED_REGULAR_22) + View.draw.text(((240 - tw) / 2, 175), address_part_4, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_22) + + View.DispShowImage() + + return def draw_prompt_yes_no(self, lines = [], title = "", bottom = "") -> None: From b3b3c5e93325e023a6a3860df6e26de5051036f2 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Sun, 7 Nov 2021 21:48:59 -0500 Subject: [PATCH 36/40] update embit to 0.4.10 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bce39d1f..702fce85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -embit==0.4.8 +embit==0.4.10 numpy==1.21.1 picamera==1.13 Pillow==8.2.0 From c8f41754e76563fc805bace2a40bde99e5b99284 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Sun, 7 Nov 2021 21:51:29 -0500 Subject: [PATCH 37/40] Receive Address typo correction --- src/seedsigner/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 7e3263bb..2b5653ea 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -813,7 +813,7 @@ class Controller(Singleton): 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") + self.menu_view.draw_modal(["Receive Address "+str(i), "Verified"], "", "Right to Exit") input = self.buttons.wait_for([B.KEY_RIGHT]) return Path.MAIN_MENU if address == change_address: From c4d05f95a10f5972a1de71bd539d89de43de0c2e Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Sun, 7 Nov 2021 22:10:51 -0500 Subject: [PATCH 38/40] make last 13 address fixed length mono space font --- src/seedsigner/views/signing_tools_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/seedsigner/views/signing_tools_view.py b/src/seedsigner/views/signing_tools_view.py index ebdee6cf..854aa8fe 100644 --- a/src/seedsigner/views/signing_tools_view.py +++ b/src/seedsigner/views/signing_tools_view.py @@ -45,15 +45,15 @@ class SigningToolsView(View): receiving_addr_str2 += "receiving addresses" elif len(p.destination_addresses) == 1: receiving_addr_str1 += "receiving address" - receiving_addr_str2 += "last 13: ..." + p.destination_addresses[0][-13:] + receiving_addr_str2 += "last 13: ... " + p.destination_addresses[0][-13:] else: receiving_addr_str1 += "Self-Transfer" tw, th = self.draw.textsize(receiving_addr_str1, font=View.ASSISTANT22) self.draw.text(((240 - tw) / 2, 75), receiving_addr_str1, fill=View.color, font=View.ASSISTANT22) if len(receiving_addr_str2) > 0: - tw, th = self.draw.textsize(receiving_addr_str2, font=View.ASSISTANT22) - self.draw.text(((240 - tw) / 2, 100), receiving_addr_str2, fill=View.color, font=View.ASSISTANT22) + tw, th = self.draw.textsize(receiving_addr_str2, font=View.ROBOTOCONDENSED_BOLD_18) + self.draw.text(((240 - tw) / 2, 105), receiving_addr_str2, fill=View.color, font=View.ROBOTOCONDENSED_BOLD_18) if p.spend_amount > 0: From 49044659017e66c117c874b69c875b9221650165 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Sun, 7 Nov 2021 22:28:56 -0500 Subject: [PATCH 39/40] screensaver works on animated qr screens now --- src/seedsigner/controller.py | 47 +++++++++++++++++++------------ src/seedsigner/helpers/buttons.py | 6 +++- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 2b5653ea..40d117ea 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -531,15 +531,21 @@ class Controller(Singleton): e = EncodeQR(seed_phrase=seed.mnemonic_list, passphrase=seed.passphrase, derivation=derivation, network=self.settings.network, qr_type=qr_xpub_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) while e.totalParts() > 1: - image = e.nextPartImage(240,240,2,background=self.current_bg_qr_color) - View.DispShowImage(image) - time.sleep(0.1) - if self.buttons.check_for_low(B.KEY_RIGHT): - break - elif self.buttons.check_for_low(B.KEY_UP): - self.prev_qr_background_color() - elif self.buttons.check_for_low(B.KEY_DOWN): - self.next_qr_background_color() + + cur_time = int(time.time() * 1000) + if cur_time - self.buttons.last_input_time > self.screensaver_activation_ms and not self.screensaver.is_running: + self.start_screensaver() + self.buttons.update_last_input_time() + else: + image = e.nextPartImage(240,240,2,background=self.current_bg_qr_color) + View.DispShowImage(image) + time.sleep(0.1) + if self.buttons.check_for_low(B.KEY_RIGHT): + break + elif self.buttons.check_for_low(B.KEY_UP): + self.prev_qr_background_color() + elif self.buttons.check_for_low(B.KEY_DOWN): + self.next_qr_background_color() while e.totalParts() == 1: image = e.nextPartImage(240,240,1,background=self.current_bg_qr_color) @@ -969,15 +975,20 @@ class Controller(Singleton): self.menu_view.draw_modal(["Generating PSBT QR ..."]) e = EncodeQR(psbt=trimmed_psbt, qr_type=self.settings.qr_psbt_type, qr_density=self.settings.qr_density, wordlist=self.settings.wordlist) while True: - image = e.nextPartImage(240,240,1,background=self.current_bg_qr_color) - View.DispShowImage(image) - time.sleep(0.05) - if self.buttons.check_for_low(B.KEY_RIGHT): - break - elif self.buttons.check_for_low(B.KEY_UP): - self.prev_qr_background_color() - elif self.buttons.check_for_low(B.KEY_DOWN): - self.next_qr_background_color() + cur_time = int(time.time() * 1000) + if cur_time - self.buttons.last_input_time > self.screensaver_activation_ms and not self.screensaver.is_running: + self.start_screensaver() + self.buttons.update_last_input_time() + else: + image = e.nextPartImage(240,240,1,background=self.current_bg_qr_color) + View.DispShowImage(image) + time.sleep(0.05) + if self.buttons.check_for_low(B.KEY_RIGHT): + break + elif self.buttons.check_for_low(B.KEY_UP): + self.prev_qr_background_color() + elif self.buttons.check_for_low(B.KEY_DOWN): + self.next_qr_background_color() # Return to Main Menu return Path.MAIN_MENU diff --git a/src/seedsigner/helpers/buttons.py b/src/seedsigner/helpers/buttons.py index d7f6fea3..3956a5b0 100644 --- a/src/seedsigner/helpers/buttons.py +++ b/src/seedsigner/helpers/buttons.py @@ -137,7 +137,11 @@ class Buttons: return True def check_for_low(self, key) -> bool: - return self.GPIO.input(key) == self.GPIO.LOW + if self.GPIO.input(key) == self.GPIO.LOW: + self.update_last_input_time() + return True + else: + return False def has_any_input(self) -> bool: for key in B.ALL_KEYS: From f5e5552d875842d9b760f88bf10802b71d6d27b5 Mon Sep 17 00:00:00 2001 From: Nick Klockenga Date: Sun, 7 Nov 2021 22:48:25 -0500 Subject: [PATCH 40/40] fix long password run off display --- src/seedsigner/controller.py | 4 ++-- src/seedsigner/views/view.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 40d117ea..9f601e8d 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -409,9 +409,9 @@ class Controller(Singleton): self.storage.save_passphrase(passphrase, slot_num) if r in (0,1): - self.menu_view.draw_modal(["Passphrase Added", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue") + self.menu_view.draw_passphrase("Passphrase Added", passphrase, "Right to Continue") elif r == 2: - self.menu_view.draw_modal(["Passphrase Updated", passphrase, "Added to Slot #" + str(slot_num)], "", "Right to Continue") + self.menu_view.draw_passphrase("Passphrase Updated", passphrase, "Right to Continue") self.buttons.wait_for([B.KEY_RIGHT]) return Path.SEED_TOOLS_SUB_MENU diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index db46ed08..e4dd6694 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -238,6 +238,33 @@ class View: View.DispShowImage() return + + def draw_passphrase(self, title, passphrase, bottom) -> None: + View.draw.rectangle((0, 0, View.canvas_width, View.canvas_height), outline=0, fill=0) + + tw, th = View.draw.textsize(bottom, font=View.ASSISTANT18) + View.draw.text(((240 - tw) / 2, 210), bottom, fill=View.color, font=View.ASSISTANT18) + + passphrase_part_1 = passphrase[0:20] + passphrase_part_2 = passphrase[20:40] + passphrase_part_3 = passphrase[40:60] + passphrase_part_4 = passphrase[60:1000] + + tw, th = View.draw.textsize(title, font=View.ASSISTANT26) + View.draw.text(((240 - tw) / 2, 30), title, fill=View.color, font=View.ASSISTANT26) + + tw, th = View.draw.textsize(passphrase_part_1, font=View.ROBOTOCONDENSED_REGULAR_20) + View.draw.text(((240 - tw) / 2, 70), passphrase_part_1, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_20) + tw, th = View.draw.textsize(passphrase_part_2, font=View.ROBOTOCONDENSED_REGULAR_20) + View.draw.text(((240 - tw) / 2, 100), passphrase_part_2, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_20) + tw, th = View.draw.textsize(passphrase_part_3, font=View.ROBOTOCONDENSED_REGULAR_20) + View.draw.text(((240 - tw) / 2, 130), passphrase_part_3, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_20) + tw, th = View.draw.textsize(passphrase_part_4, font=View.ROBOTOCONDENSED_REGULAR_20) + View.draw.text(((240 - tw) / 2, 160), passphrase_part_4, fill=View.color, font=View.ROBOTOCONDENSED_REGULAR_20) + + View.DispShowImage() + + return def draw_prompt_yes_no(self, lines = [], title = "", bottom = "") -> None: