Merge pull request #122 from SeedSigner/0_4_5

Merge 0.4.5 into Main Branch
This commit is contained in:
SeedSigner
2021-11-18 09:43:48 -06:00
committed by GitHub
45 changed files with 2606 additions and 704 deletions
+1
View File
@@ -1,3 +1,4 @@
.DS_Store
__pycache__/
src/seedsigner.egg-info/
.nova
+203
View File
@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1080 1080" style="enable-background:new 0 0 1080 1080;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FF7300;}
.st1{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M569.64,542.84c2.02,3.52,3.02,7.85,3.02,12.98s-1.02,9.66-3.08,13.57c-2.05,3.91-4.74,6.93-8.06,9.06
c-6.4,4.19-13.23,6.28-20.5,6.28c-3.71,0-7.22-0.45-10.54-1.36c-3.32-0.91-6.01-2.03-8.06-3.38c-4.19-2.52-7.11-4.97-8.77-7.34
l-1.07-1.31c-2.05-2.53-3.07-4.8-3.07-6.81c0-2.02,1.69-4.68,5.09-8c1.97-1.9,4.07-2.84,6.28-2.84s5.29,1.97,9.24,5.92
c1.11,1.34,2.69,2.63,4.74,3.85c2.06,1.23,3.95,1.84,5.69,1.84c7.35,0,11.02-3,11.02-9.01c0-1.81-1.01-3.33-3.02-4.56
c-2.01-1.22-4.52-2.11-7.53-2.67c-3-0.55-6.24-1.44-9.71-2.66c-3.48-1.23-6.72-2.67-9.72-4.33c-3-1.66-5.51-4.28-7.52-7.88
c-2.02-3.59-3.03-7.92-3.03-12.97c0-6.95,2.59-13.02,7.76-18.19c5.18-5.18,12.23-7.76,21.16-7.76c4.74,0,9.06,0.61,12.97,1.83
c3.91,1.23,6.62,2.47,8.12,3.74l2.96,2.25c2.45,2.29,3.68,4.22,3.68,5.81c0,1.58-0.95,3.75-2.85,6.51
c-2.69,3.95-5.45,5.93-8.29,5.93c-1.66,0-3.72-0.79-6.16-2.37c-0.24-0.16-0.7-0.56-1.37-1.19c-0.67-0.63-1.28-1.14-1.83-1.54
c-1.66-1.03-3.78-1.54-6.34-1.54c-2.57,0-4.7,0.61-6.4,1.84c-1.7,1.22-2.55,2.92-2.55,5.09c0,2.18,1.01,3.93,3.02,5.28
c2.02,1.34,4.52,2.25,7.53,2.72c3,0.47,6.28,1.21,9.83,2.19c3.56,0.99,6.83,2.2,9.84,3.62
C565.12,536.86,567.63,539.33,569.64,542.84z"/>
<path class="st0" d="M605.67,501.21c0.39,1.34,0.59,3.39,0.59,6.16v65.88c0,1.9-0.06,3.28-0.18,4.15s-0.49,1.9-1.12,3.08
c-1.11,2.14-4.19,3.2-9.25,3.2c-5.53,0-8.76-1.5-9.71-4.5c-0.48-1.27-0.72-3.28-0.72-6.05v-65.88c0-1.81,0.06-3.18,0.18-4.09
c0.12-0.9,0.49-1.95,1.13-3.14c1.11-2.13,4.18-3.2,9.24-3.2C601.36,496.82,604.64,498.28,605.67,501.21z"/>
<path class="st0" d="M695.09,534.62c0.79,1.5,1.19,3.72,1.19,6.64v26.07c0,2.53-1.11,4.94-3.32,7.23
c-6.63,6.87-16.59,10.31-29.86,10.31c-11.77,0-22-4.45-30.69-13.33c-8.69-8.89-13.03-19.62-13.03-32.18s4.42-23.05,13.27-31.46
c8.85-8.41,19.27-12.62,31.28-12.62c9.32,0,18.13,3.08,26.43,9.24c2.13,1.58,3.19,3.35,3.19,5.28s-0.9,4.17-2.72,6.69
c-3.08,4.19-5.89,6.28-8.42,6.28c-1.5,0-3.9-1.08-7.22-3.25c-3.32-2.18-7.27-3.26-11.85-3.26c-6.01,0-11.34,2.11-16,6.33
c-4.66,4.23-6.99,9.76-6.99,16.6c0,6.83,2.37,12.64,7.11,17.41c4.74,4.78,10.11,7.17,16.12,7.17c4.34,0,8.25-0.71,11.73-2.13
v-12.91h-9.37c-2.44,0-4.18-0.32-5.21-0.96c-1.03-0.62-1.7-1.55-2.01-2.78c-0.32-1.22-0.48-2.88-0.48-4.98
c0-2.09,0.18-3.77,0.53-5.03c0.36-1.26,1.01-2.14,1.96-2.61c1.42-0.71,3.32-1.07,5.69-1.07h20.85
C691.54,531.46,694.15,532.57,695.09,534.62z"/>
<path class="st0" d="M784.03,501.44c0.47,1.27,0.71,3.28,0.71,6.05v65.76c0,1.9-0.06,3.28-0.18,4.15s-0.49,1.9-1.13,3.08
c-1.1,2.14-4.18,3.2-9.24,3.2c-3.08,0-5.21-0.24-6.4-0.71c-1.18-0.47-2.17-1.23-2.96-2.25c-17.77-23.7-29.47-39.18-35.07-46.45
v38.98c0,1.9-0.06,3.28-0.18,4.15s-0.49,1.9-1.13,3.08c-1.1,2.14-4.18,3.2-9.24,3.2c-4.9,0-7.9-1.06-9.01-3.2
c-0.63-1.18-1-2.25-1.12-3.2s-0.18-2.33-0.18-4.15v-66.12c0-3,0.37-5.19,1.13-6.57c0.75-1.38,1.87-2.31,3.37-2.79
c1.51-0.47,3.44-0.71,5.81-0.71s4.25,0.22,5.63,0.65c1.38,0.44,2.35,0.93,2.9,1.49c0.32,0.23,1.22,1.26,2.73,3.08
c16.66,22.67,27.8,37.64,33.41,44.9v-40.05c0-3,0.38-5.19,1.13-6.57s1.87-2.31,3.38-2.79c1.49-0.47,3.38-0.71,5.63-0.71
s4.06,0.2,5.44,0.59c1.39,0.4,2.42,0.88,3.09,1.42C783.22,499.51,783.71,500.34,784.03,501.44z"/>
<path class="st0" d="M818.33,517.8v11.97h23.46c1.82,0,3.18,0.06,4.09,0.17c0.91,0.12,1.96,0.5,3.14,1.13
c2.13,1.1,3.2,4.19,3.2,9.24c0,5.53-1.5,8.77-4.5,9.72c-1.34,0.47-3.4,0.71-6.16,0.71h-23.23v11.97h36.5
c1.82,0,3.18,0.06,4.09,0.17c0.9,0.12,1.95,0.5,3.14,1.13c2.13,1.11,3.2,4.19,3.2,9.24c0,5.53-1.47,8.77-4.39,9.72
c-1.34,0.47-3.4,0.71-6.16,0.71h-46.92c-5.53,0-8.77-1.5-9.72-4.5c-0.47-1.27-0.71-3.28-0.71-6.05v-65.88
c0-4.03,0.75-6.77,2.25-8.23c1.5-1.47,4.34-2.2,8.53-2.2h46.69c1.82,0,3.18,0.06,4.09,0.18c0.9,0.12,1.95,0.49,3.14,1.13
c2.13,1.1,3.2,4.18,3.2,9.24c0,5.53-1.47,8.77-4.39,9.72c-1.34,0.47-3.4,0.71-6.16,0.71H818.33z"/>
<path class="st0" d="M946.13,575.15c0,3-2.45,5.53-7.34,7.58c-2.53,1.11-4.56,1.66-6.1,1.66c-1.55,0-2.81-0.37-3.8-1.12
s-1.72-1.53-2.19-2.32c-0.79-1.5-3.87-8.65-9.24-21.44l-3.68,0.23h-14.93v13.39c0,1.82-0.05,3.18-0.18,4.09
c-0.11,0.91-0.49,1.96-1.12,3.14c-1.11,2.14-4.19,3.2-9.24,3.2c-5.53,0-8.77-1.46-9.72-4.38c-0.47-1.34-0.71-3.4-0.71-6.16v-65.77
c0-1.81,0.06-3.18,0.18-4.09c0.11-0.9,0.49-1.95,1.12-3.14c1.11-2.13,4.19-3.2,9.24-3.2h25.6c6.95,0,13.71,2.53,20.26,7.59
c3.16,2.45,5.77,5.76,7.82,9.95c2.06,4.19,3.09,8.85,3.09,13.98c0,8.93-2.97,16.28-8.89,22.04c1.74,4.19,4.42,10.39,8.05,18.61
C945.54,571.83,946.13,573.88,946.13,575.15z"/>
<g>
<path class="st0" d="M136.94,572.78c-2.05-2.53-3.08-4.8-3.08-6.81c0-2.02,1.69-4.68,5.09-8c1.97-1.9,4.07-2.84,6.28-2.84
s5.29,1.98,9.24,5.92c1.11,1.34,2.69,2.63,4.74,3.85c2.05,1.23,3.95,1.84,5.69,1.84c7.35,0,11.02-3,11.02-9.01
c0-1.82-1.01-3.33-3.02-4.56c-2.01-1.23-4.52-2.12-7.52-2.67c-3-0.55-6.24-1.44-9.72-2.67c-3.48-1.22-6.72-2.66-9.72-4.32
c-3-1.66-5.51-4.29-7.52-7.88c-2.01-3.59-3.02-7.92-3.02-12.98c0-6.95,2.59-13.01,7.76-18.19c5.17-5.17,12.23-7.76,21.16-7.76
c4.74,0,9.06,0.61,12.97,1.84c3.91,1.23,6.62,2.47,8.12,3.73l2.96,2.25c2.45,2.29,3.67,4.23,3.67,5.81s-0.95,3.75-2.84,6.52
c-2.69,3.95-5.45,5.92-8.3,5.92c-1.66,0-3.71-0.79-6.16-2.37c-0.24-0.16-0.69-0.55-1.36-1.19c-0.67-0.63-1.28-1.14-1.84-1.54
c-1.66-1.02-3.77-1.54-6.34-1.54c-2.57,0-4.7,0.61-6.4,1.84c-1.7,1.23-2.55,2.92-2.55,5.1c0,2.17,1.01,3.93,3.02,5.27
c2.02,1.34,4.52,2.25,7.53,2.73c3,0.47,6.28,1.2,9.83,2.19c3.56,0.99,6.83,2.19,9.84,3.61c3,1.42,5.51,3.89,7.52,7.41
c2.02,3.52,3.02,7.84,3.02,12.98s-1.03,9.66-3.08,13.57c-2.06,3.91-4.74,6.93-8.06,9.07c-6.4,4.19-13.23,6.28-20.5,6.28
c-3.71,0-7.23-0.46-10.55-1.36c-3.32-0.9-6.01-2.03-8.06-3.37c-4.19-2.53-7.11-4.98-8.77-7.35L136.94,572.78z"/>
<path class="st0" d="M230.62,564.02h36.5c1.82,0,3.18,0.06,4.09,0.18c0.91,0.12,1.96,0.49,3.14,1.13c2.13,1.11,3.2,4.19,3.2,9.24
c0,5.53-1.46,8.77-4.38,9.72c-1.35,0.47-3.4,0.71-6.16,0.71h-46.92c-5.53,0-8.77-1.5-9.72-4.5c-0.48-1.27-0.71-3.28-0.71-6.05
v-65.88c0-4.03,0.75-6.77,2.25-8.23c1.5-1.46,4.34-2.19,8.53-2.19h46.69c1.82,0,3.18,0.06,4.09,0.18s1.96,0.49,3.14,1.13
c2.13,1.11,3.2,4.19,3.2,9.24c0,5.53-1.46,8.77-4.38,9.72c-1.35,0.47-3.4,0.71-6.16,0.71h-36.38v11.97h23.46
c1.82,0,3.18,0.06,4.09,0.18c0.91,0.12,1.96,0.49,3.14,1.13c2.13,1.11,3.2,4.19,3.2,9.24c0,5.53-1.5,8.77-4.5,9.72
c-1.34,0.47-3.4,0.71-6.16,0.71h-23.22V564.02z"/>
<path class="st0" d="M311.14,564.02h36.5c1.82,0,3.18,0.06,4.09,0.18c0.91,0.12,1.96,0.49,3.14,1.13c2.13,1.11,3.2,4.19,3.2,9.24
c0,5.53-1.46,8.77-4.38,9.72c-1.35,0.47-3.4,0.71-6.16,0.71h-46.92c-5.53,0-8.77-1.5-9.72-4.5c-0.48-1.27-0.71-3.28-0.71-6.05
v-65.88c0-4.03,0.75-6.77,2.25-8.23c1.5-1.46,4.34-2.19,8.53-2.19h46.69c1.82,0,3.18,0.06,4.09,0.18s1.96,0.49,3.14,1.13
c2.13,1.11,3.2,4.19,3.2,9.24c0,5.53-1.46,8.77-4.38,9.72c-1.35,0.47-3.4,0.71-6.16,0.71h-36.38v11.97h23.46
c1.82,0,3.18,0.06,4.09,0.18c0.91,0.12,1.96,0.49,3.14,1.13c2.13,1.11,3.2,4.19,3.2,9.24c0,5.53-1.5,8.77-4.5,9.72
c-1.34,0.47-3.4,0.71-6.16,0.71h-23.22V564.02z"/>
<path class="st0" d="M381.23,498.01l21.92,0.12c11.45,0,21.52,4.13,30.22,12.38c8.69,8.26,13.03,18.41,13.03,30.45
c0,12.05-4.25,22.38-12.74,30.99c-8.49,8.61-18.78,12.92-30.87,12.92h-21.69c-4.82,0-7.82-0.99-9.01-2.96
c-0.95-1.66-1.42-4.19-1.42-7.58v-65.88c0-1.9,0.06-3.28,0.18-4.15c0.12-0.87,0.49-1.9,1.12-3.08
C373.1,499.08,376.17,498.01,381.23,498.01z M418.56,557.56c4.58-4.23,6.87-9.54,6.87-15.94c0-6.4-2.23-11.75-6.69-16.05
c-4.46-4.31-9.7-6.46-15.7-6.46h-11.37v44.79h11.49C408.84,563.89,413.98,561.78,418.56,557.56z"/>
</g>
<g>
<path class="st1" d="M569.64,542.84c-2.01-3.51-4.52-5.98-7.52-7.4c-3.01-1.42-6.28-2.63-9.84-3.62
c-3.55-0.98-6.83-1.72-9.83-2.19c-3.01-0.47-5.51-1.38-7.53-2.72c-2.01-1.35-3.02-3.1-3.02-5.28c0-2.17,0.85-3.87,2.55-5.09
c1.7-1.23,3.83-1.84,6.4-1.84c2.56,0,4.68,0.51,6.34,1.54c0.55,0.4,1.16,0.91,1.83,1.54c0.67,0.63,1.13,1.03,1.37,1.19
c2.44,1.58,4.5,2.37,6.16,2.37c2.84,0,5.6-1.98,8.29-5.93c1.9-2.76,2.85-4.93,2.85-6.51c0-1.59-1.23-3.52-3.68-5.81l-2.96-2.25
c-1.5-1.27-4.21-2.51-8.12-3.74c-3.91-1.22-8.23-1.83-12.97-1.83c-8.93,0-15.98,2.58-21.16,7.76c-5.17,5.17-7.76,11.24-7.76,18.19
c0,5.05,1.01,9.38,3.03,12.97c2.01,3.6,4.52,6.22,7.52,7.88c3,1.66,6.24,3.1,9.72,4.33c3.47,1.22,6.71,2.11,9.71,2.66
c3.01,0.56,5.52,1.45,7.53,2.67c2.01,1.23,3.02,2.75,3.02,4.56c0,6.01-3.67,9.01-11.02,9.01c-1.74,0-3.63-0.61-5.69-1.84
c-2.05-1.22-3.63-2.51-4.74-3.85c-3.95-3.95-7.03-5.92-9.24-5.92s-4.31,0.94-6.28,2.84c-3.4,3.32-5.09,5.98-5.09,8
c0,2.01,1.02,4.28,3.07,6.81l1.07,1.31c1.66,2.37,4.58,4.82,8.77,7.34c2.05,1.35,4.74,2.47,8.06,3.38
c3.32,0.91,6.83,1.36,10.54,1.36c7.27,0,14.1-2.09,20.5-6.28c3.32-2.13,6.01-5.15,8.06-9.06c2.06-3.91,3.08-8.44,3.08-13.57
S571.66,546.36,569.64,542.84z"/>
<path class="st1" d="M605.67,501.21c-1.03-2.93-4.31-4.39-9.84-4.39c-5.06,0-8.13,1.07-9.24,3.2c-0.64,1.19-1.01,2.24-1.13,3.14
c-0.12,0.91-0.18,2.28-0.18,4.09v65.88c0,2.77,0.24,4.78,0.72,6.05c0.95,3,4.18,4.5,9.71,4.5c5.06,0,8.14-1.06,9.25-3.2
c0.63-1.18,1-2.21,1.12-3.08s0.18-2.25,0.18-4.15v-65.88C606.26,504.6,606.06,502.55,605.67,501.21z"/>
<path class="st1" d="M695.09,534.62c-0.94-2.05-3.55-3.16-7.82-3.32h-20.85c-2.37,0-4.27,0.36-5.69,1.07
c-0.95,0.47-1.6,1.35-1.96,2.61c-0.35,1.26-0.53,2.94-0.53,5.03c0,2.1,0.16,3.76,0.48,4.98c0.31,1.23,0.98,2.16,2.01,2.78
c1.03,0.64,2.77,0.96,5.21,0.96h9.37v12.91c-3.48,1.42-7.39,2.13-11.73,2.13c-6.01,0-11.38-2.39-16.12-7.17
c-4.74-4.77-7.11-10.58-7.11-17.41c0-6.84,2.33-12.37,6.99-16.6c4.66-4.22,9.99-6.33,16-6.33c4.58,0,8.53,1.08,11.85,3.26
c3.32,2.17,5.72,3.25,7.22,3.25c2.53,0,5.34-2.09,8.42-6.28c1.82-2.52,2.72-4.76,2.72-6.69s-1.06-3.7-3.19-5.28
c-8.3-6.16-17.11-9.24-26.43-9.24c-12.01,0-22.43,4.21-31.28,12.62c-8.85,8.41-13.27,18.9-13.27,31.46s4.34,23.29,13.03,32.18
c8.69,8.88,18.92,13.33,30.69,13.33c13.27,0,23.23-3.44,29.86-10.31c2.21-2.29,3.32-4.7,3.32-7.23v-26.07
C696.28,538.34,695.88,536.12,695.09,534.62z"/>
<path class="st1" d="M784.03,501.44c-0.32-1.1-0.81-1.93-1.48-2.49c-0.67-0.54-1.7-1.02-3.09-1.42c-1.38-0.39-3.19-0.59-5.44-0.59
s-4.14,0.24-5.63,0.71c-1.51,0.48-2.63,1.41-3.38,2.79s-1.13,3.57-1.13,6.57v40.05c-5.61-7.26-16.75-22.23-33.41-44.9
c-1.51-1.82-2.41-2.85-2.73-3.08c-0.55-0.56-1.52-1.05-2.9-1.49c-1.38-0.43-3.26-0.65-5.63-0.65s-4.3,0.24-5.81,0.71
c-1.5,0.48-2.62,1.41-3.37,2.79c-0.76,1.38-1.13,3.57-1.13,6.57v66.12c0,1.82,0.06,3.2,0.18,4.15s0.49,2.02,1.12,3.2
c1.11,2.14,4.11,3.2,9.01,3.2c5.06,0,8.14-1.06,9.24-3.2c0.64-1.18,1.01-2.21,1.13-3.08s0.18-2.25,0.18-4.15v-38.98
c5.6,7.27,17.3,22.75,35.07,46.45c0.79,1.02,1.78,1.78,2.96,2.25c1.19,0.47,3.32,0.71,6.4,0.71c5.06,0,8.14-1.06,9.24-3.2
c0.64-1.18,1.01-2.21,1.13-3.08s0.18-2.25,0.18-4.15v-65.76C784.74,504.72,784.5,502.71,784.03,501.44z"/>
<path class="st1" d="M862.06,564.01c-1.19-0.63-2.24-1.01-3.14-1.13c-0.91-0.11-2.27-0.17-4.09-0.17h-36.5v-11.97h23.23
c2.76,0,4.82-0.24,6.16-0.71c3-0.95,4.5-4.19,4.5-9.72c0-5.05-1.07-8.14-3.2-9.24c-1.18-0.63-2.23-1.01-3.14-1.13
c-0.91-0.11-2.27-0.17-4.09-0.17h-23.46V517.8h36.38c2.76,0,4.82-0.24,6.16-0.71c2.92-0.95,4.39-4.19,4.39-9.72
c0-5.06-1.07-8.14-3.2-9.24c-1.19-0.64-2.24-1.01-3.14-1.13c-0.91-0.12-2.27-0.18-4.09-0.18h-46.69c-4.19,0-7.03,0.73-8.53,2.2
c-1.5,1.46-2.25,4.2-2.25,8.23v65.88c0,2.77,0.24,4.78,0.71,6.05c0.95,3,4.19,4.5,9.72,4.5h46.92c2.76,0,4.82-0.24,6.16-0.71
c2.92-0.95,4.39-4.19,4.39-9.72C865.26,568.2,864.19,565.12,862.06,564.01z"/>
<path class="st1" d="M944.35,568.99c-3.63-8.22-6.31-14.42-8.05-18.61c5.92-5.76,8.89-13.11,8.89-22.04
c0-5.13-1.03-9.79-3.09-13.98c-2.05-4.19-4.66-7.5-7.82-9.95c-6.55-5.06-13.31-7.59-20.26-7.59h-25.6c-5.05,0-8.13,1.07-9.24,3.2
c-0.63,1.19-1.01,2.24-1.12,3.14c-0.12,0.91-0.18,2.28-0.18,4.09v65.77c0,2.76,0.24,4.82,0.71,6.16c0.95,2.92,4.19,4.38,9.72,4.38
c5.05,0,8.13-1.06,9.24-3.2c0.63-1.18,1.01-2.23,1.12-3.14c0.13-0.91,0.18-2.27,0.18-4.09v-13.39h14.93l3.68-0.23
c5.37,12.79,8.45,19.94,9.24,21.44c0.47,0.79,1.2,1.57,2.19,2.32s2.25,1.12,3.8,1.12c1.54,0,3.57-0.55,6.1-1.66
c4.89-2.05,7.34-4.58,7.34-7.58C946.13,573.88,945.54,571.83,944.35,568.99z M920.89,536.16c-2.29,1.74-4.58,2.61-6.87,2.61
h-15.17V517.8h14.93c2.45,0,4.82,0.89,7.11,2.66c2.3,1.78,3.44,4.41,3.44,7.88C924.33,531.82,923.19,534.43,920.89,536.16z"/>
</g>
</g>
<path class="st0" d="M1009.44,463.97c-0.01-0.02-0.02-0.04-0.04-0.06c-6.57-12.07-14.63-23.19-23.89-33.06
c-9.03-9.63-19.22-18.08-30.31-25.09c-21.24-13.42-45.8-21.54-71.99-22.49H205.02c-28.34,0.93-54.68,9.01-77.27,22.49
c-25.45,15.18-46.14,37.2-59.63,63.53c-11.06,21.58-17.26,46.06-17.26,72.04c0,28.1,7.24,54.49,19.9,77.26
c12.72,22.87,30.9,42.09,52.69,55.75c22.65,14.21,49.22,22.39,77.59,22.39h677.91c28.37,0,54.93-8.18,77.6-22.39
c0.04-0.03,0.09-0.05,0.13-0.09c21.84-13.71,40.02-33.03,52.72-55.98c12.56-22.7,19.74-48.97,19.74-76.94
C1029.14,513.41,1021.97,486.96,1009.44,463.97z M944.35,568.99c1.19,2.84,1.78,4.89,1.78,6.16c0,3-2.45,5.53-7.34,7.58
c-2.53,1.11-4.56,1.66-6.1,1.66c-1.55,0-2.81-0.37-3.8-1.12s-1.72-1.53-2.19-2.32c-0.79-1.5-3.87-8.65-9.24-21.44l-3.68,0.23h-14.93
v13.39c0,1.82-0.05,3.18-0.18,4.09c-0.11,0.91-0.49,1.96-1.12,3.14c-1.11,2.14-4.19,3.2-9.24,3.2c-5.53,0-8.77-1.46-9.72-4.38
c-0.47-1.34-0.71-3.4-0.71-6.16v-65.77c0-1.81,0.06-3.18,0.18-4.09c0.11-0.9,0.49-1.95,1.12-3.14c1.11-2.13,4.19-3.2,9.24-3.2h25.6
c6.95,0,13.71,2.53,20.26,7.59c3.16,2.45,5.77,5.76,7.82,9.95c2.06,4.19,3.09,8.85,3.09,13.98c0,8.93-2.97,16.28-8.89,22.04
C938.04,554.57,940.72,560.77,944.35,568.99z M841.79,529.77c1.82,0,3.18,0.06,4.09,0.17c0.91,0.12,1.96,0.5,3.14,1.13
c2.13,1.1,3.2,4.19,3.2,9.24c0,5.53-1.5,8.77-4.5,9.72c-1.34,0.47-3.4,0.71-6.16,0.71h-23.23v11.97h36.5c1.82,0,3.18,0.06,4.09,0.17
c0.9,0.12,1.95,0.5,3.14,1.13c2.13,1.11,3.2,4.19,3.2,9.24c0,5.53-1.47,8.77-4.39,9.72c-1.34,0.47-3.4,0.71-6.16,0.71h-46.92
c-5.53,0-8.77-1.5-9.72-4.5c-0.47-1.27-0.71-3.28-0.71-6.05v-65.88c0-4.03,0.75-6.77,2.25-8.23c1.5-1.47,4.34-2.2,8.53-2.2h46.69
c1.82,0,3.18,0.06,4.09,0.18c0.9,0.12,1.95,0.49,3.14,1.13c2.13,1.1,3.2,4.18,3.2,9.24c0,5.53-1.47,8.77-4.39,9.72
c-1.34,0.47-3.4,0.71-6.16,0.71h-36.38v11.97H841.79z M784.74,507.49v65.76c0,1.9-0.06,3.28-0.18,4.15s-0.49,1.9-1.13,3.08
c-1.1,2.14-4.18,3.2-9.24,3.2c-3.08,0-5.21-0.24-6.4-0.71c-1.18-0.47-2.17-1.23-2.96-2.25c-17.77-23.7-29.47-39.18-35.07-46.45
v38.98c0,1.9-0.06,3.28-0.18,4.15s-0.49,1.9-1.13,3.08c-1.1,2.14-4.18,3.2-9.24,3.2c-4.9,0-7.9-1.06-9.01-3.2
c-0.63-1.18-1-2.25-1.12-3.2s-0.18-2.33-0.18-4.15v-66.12c0-3,0.37-5.19,1.13-6.57c0.75-1.38,1.87-2.31,3.37-2.79
c1.51-0.47,3.44-0.71,5.81-0.71s4.25,0.22,5.63,0.65c1.38,0.44,2.35,0.93,2.9,1.49c0.32,0.23,1.22,1.26,2.73,3.08
c16.66,22.67,27.8,37.64,33.41,44.9v-40.05c0-3,0.38-5.19,1.13-6.57s1.87-2.31,3.38-2.79c1.49-0.47,3.38-0.71,5.63-0.71
s4.06,0.2,5.44,0.59c1.39,0.4,2.42,0.88,3.09,1.42c0.67,0.56,1.16,1.39,1.48,2.49C784.5,502.71,784.74,504.72,784.74,507.49z
M647.46,556.6c4.74,4.78,10.11,7.17,16.12,7.17c4.34,0,8.25-0.71,11.73-2.13v-12.91h-9.37c-2.44,0-4.18-0.32-5.21-0.96
c-1.03-0.62-1.7-1.55-2.01-2.78c-0.32-1.22-0.48-2.88-0.48-4.98c0-2.09,0.18-3.77,0.53-5.03c0.36-1.26,1.01-2.14,1.96-2.61
c1.42-0.71,3.32-1.07,5.69-1.07h20.85c4.27,0.16,6.88,1.27,7.82,3.32c0.79,1.5,1.19,3.72,1.19,6.64v26.07
c0,2.53-1.11,4.94-3.32,7.23c-6.63,6.87-16.59,10.31-29.86,10.31c-11.77,0-22-4.45-30.69-13.33c-8.69-8.89-13.03-19.62-13.03-32.18
s4.42-23.05,13.27-31.46c8.85-8.41,19.27-12.62,31.28-12.62c9.32,0,18.13,3.08,26.43,9.24c2.13,1.58,3.19,3.35,3.19,5.28
s-0.9,4.17-2.72,6.69c-3.08,4.19-5.89,6.28-8.42,6.28c-1.5,0-3.9-1.08-7.22-3.25c-3.32-2.18-7.27-3.26-11.85-3.26
c-6.01,0-11.34,2.11-16,6.33c-4.66,4.23-6.99,9.76-6.99,16.6C640.35,546.02,642.72,551.83,647.46,556.6z M606.26,507.37v65.88
c0,1.9-0.06,3.28-0.18,4.15s-0.49,1.9-1.12,3.08c-1.11,2.14-4.19,3.2-9.25,3.2c-5.53,0-8.76-1.5-9.71-4.5
c-0.48-1.27-0.72-3.28-0.72-6.05v-65.88c0-1.81,0.06-3.18,0.18-4.09c0.12-0.9,0.49-1.95,1.13-3.14c1.11-2.13,4.18-3.2,9.24-3.2
c5.53,0,8.81,1.46,9.84,4.39C606.06,502.55,606.26,504.6,606.26,507.37z M548.55,551.73c-2.01-1.22-4.52-2.11-7.53-2.67
c-3-0.55-6.24-1.44-9.71-2.66c-3.48-1.23-6.72-2.67-9.72-4.33c-3-1.66-5.51-4.28-7.52-7.88c-2.02-3.59-3.03-7.92-3.03-12.97
c0-6.95,2.59-13.02,7.76-18.19c5.18-5.18,12.23-7.76,21.16-7.76c4.74,0,9.06,0.61,12.97,1.83c3.91,1.23,6.62,2.47,8.12,3.74
l2.96,2.25c2.45,2.29,3.68,4.22,3.68,5.81c0,1.58-0.95,3.75-2.85,6.51c-2.69,3.95-5.45,5.93-8.29,5.93c-1.66,0-3.72-0.79-6.16-2.37
c-0.24-0.16-0.7-0.56-1.37-1.19c-0.67-0.63-1.28-1.14-1.83-1.54c-1.66-1.03-3.78-1.54-6.34-1.54c-2.57,0-4.7,0.61-6.4,1.84
c-1.7,1.22-2.55,2.92-2.55,5.09c0,2.18,1.01,3.93,3.02,5.28c2.02,1.34,4.52,2.25,7.53,2.72c3,0.47,6.28,1.21,9.83,2.19
c3.56,0.99,6.83,2.2,9.84,3.62c3,1.42,5.51,3.89,7.52,7.4c2.02,3.52,3.02,7.85,3.02,12.98s-1.02,9.66-3.08,13.57
c-2.05,3.91-4.74,6.93-8.06,9.06c-6.4,4.19-13.23,6.28-20.5,6.28c-3.71,0-7.22-0.45-10.54-1.36c-3.32-0.91-6.01-2.03-8.06-3.38
c-4.19-2.52-7.11-4.97-8.77-7.34l-1.07-1.31c-2.05-2.53-3.07-4.8-3.07-6.81c0-2.02,1.69-4.68,5.09-8c1.97-1.9,4.07-2.84,6.28-2.84
s5.29,1.97,9.24,5.92c1.11,1.34,2.69,2.63,4.74,3.85c2.06,1.23,3.95,1.84,5.69,1.84c7.35,0,11.02-3,11.02-9.01
C551.57,554.48,550.56,552.96,548.55,551.73z M77.74,584.87c-4.65-13.65-7.18-28.29-7.18-43.54c0-16.2,2.87-31.72,8.16-46.1
c18.71-50.84,67.61-87.44,126.31-89.47c0.09-0.01,0.19-0.01,0.27-0.01h272.65v268.5H201.04C144.05,674.25,95.48,636.85,77.74,584.87
z"/>
<path class="st1" d="M569.64,542.84c2.02,3.52,3.02,7.85,3.02,12.98s-1.02,9.66-3.08,13.57c-2.05,3.91-4.74,6.93-8.06,9.06
c-6.4,4.19-13.23,6.28-20.5,6.28c-3.71,0-7.22-0.45-10.54-1.36c-3.32-0.91-6.01-2.03-8.06-3.38c-4.19-2.52-7.11-4.97-8.77-7.34
l-1.07-1.31c-2.05-2.53-3.07-4.8-3.07-6.81c0-2.02,1.69-4.68,5.09-8c1.97-1.9,4.07-2.84,6.28-2.84s5.29,1.97,9.24,5.92
c1.11,1.34,2.69,2.63,4.74,3.85c2.06,1.23,3.95,1.84,5.69,1.84c7.35,0,11.02-3,11.02-9.01c0-1.81-1.01-3.33-3.02-4.56
c-2.01-1.22-4.52-2.11-7.53-2.67c-3-0.55-6.24-1.44-9.71-2.66c-3.48-1.23-6.72-2.67-9.72-4.33c-3-1.66-5.51-4.28-7.52-7.88
c-2.02-3.59-3.03-7.92-3.03-12.97c0-6.95,2.59-13.02,7.76-18.19c5.18-5.18,12.23-7.76,21.16-7.76c4.74,0,9.06,0.61,12.97,1.83
c3.91,1.23,6.62,2.47,8.12,3.74l2.96,2.25c2.45,2.29,3.68,4.22,3.68,5.81c0,1.58-0.95,3.75-2.85,6.51
c-2.69,3.95-5.45,5.93-8.29,5.93c-1.66,0-3.72-0.79-6.16-2.37c-0.24-0.16-0.7-0.56-1.37-1.19c-0.67-0.63-1.28-1.14-1.83-1.54
c-1.66-1.03-3.78-1.54-6.34-1.54c-2.57,0-4.7,0.61-6.4,1.84c-1.7,1.22-2.55,2.92-2.55,5.09c0,2.18,1.01,3.93,3.02,5.28
c2.02,1.34,4.52,2.25,7.53,2.72c3,0.47,6.28,1.21,9.83,2.19c3.56,0.99,6.83,2.2,9.84,3.62
C565.12,536.86,567.63,539.33,569.64,542.84z"/>
<path class="st1" d="M605.67,501.21c0.39,1.34,0.59,3.39,0.59,6.16v65.88c0,1.9-0.06,3.28-0.18,4.15s-0.49,1.9-1.12,3.08
c-1.11,2.14-4.19,3.2-9.25,3.2c-5.53,0-8.76-1.5-9.71-4.5c-0.48-1.27-0.72-3.28-0.72-6.05v-65.88c0-1.81,0.06-3.18,0.18-4.09
c0.12-0.9,0.49-1.95,1.13-3.14c1.11-2.13,4.18-3.2,9.24-3.2C601.36,496.82,604.64,498.28,605.67,501.21z"/>
<path class="st1" d="M695.09,534.62c0.79,1.5,1.19,3.72,1.19,6.64v26.07c0,2.53-1.11,4.94-3.32,7.23
c-6.63,6.87-16.59,10.31-29.86,10.31c-11.77,0-22-4.45-30.69-13.33c-8.69-8.89-13.03-19.62-13.03-32.18s4.42-23.05,13.27-31.46
c8.85-8.41,19.27-12.62,31.28-12.62c9.32,0,18.13,3.08,26.43,9.24c2.13,1.58,3.19,3.35,3.19,5.28s-0.9,4.17-2.72,6.69
c-3.08,4.19-5.89,6.28-8.42,6.28c-1.5,0-3.9-1.08-7.22-3.25c-3.32-2.18-7.27-3.26-11.85-3.26c-6.01,0-11.34,2.11-16,6.33
c-4.66,4.23-6.99,9.76-6.99,16.6c0,6.83,2.37,12.64,7.11,17.41c4.74,4.78,10.11,7.17,16.12,7.17c4.34,0,8.25-0.71,11.73-2.13v-12.91
h-9.37c-2.44,0-4.18-0.32-5.21-0.96c-1.03-0.62-1.7-1.55-2.01-2.78c-0.32-1.22-0.48-2.88-0.48-4.98c0-2.09,0.18-3.77,0.53-5.03
c0.36-1.26,1.01-2.14,1.96-2.61c1.42-0.71,3.32-1.07,5.69-1.07h20.85C691.54,531.46,694.15,532.57,695.09,534.62z"/>
<path class="st1" d="M784.03,501.44c0.47,1.27,0.71,3.28,0.71,6.05v65.76c0,1.9-0.06,3.28-0.18,4.15s-0.49,1.9-1.13,3.08
c-1.1,2.14-4.18,3.2-9.24,3.2c-3.08,0-5.21-0.24-6.4-0.71c-1.18-0.47-2.17-1.23-2.96-2.25c-17.77-23.7-29.47-39.18-35.07-46.45
v38.98c0,1.9-0.06,3.28-0.18,4.15s-0.49,1.9-1.13,3.08c-1.1,2.14-4.18,3.2-9.24,3.2c-4.9,0-7.9-1.06-9.01-3.2
c-0.63-1.18-1-2.25-1.12-3.2s-0.18-2.33-0.18-4.15v-66.12c0-3,0.37-5.19,1.13-6.57c0.75-1.38,1.87-2.31,3.37-2.79
c1.51-0.47,3.44-0.71,5.81-0.71s4.25,0.22,5.63,0.65c1.38,0.44,2.35,0.93,2.9,1.49c0.32,0.23,1.22,1.26,2.73,3.08
c16.66,22.67,27.8,37.64,33.41,44.9v-40.05c0-3,0.38-5.19,1.13-6.57s1.87-2.31,3.38-2.79c1.49-0.47,3.38-0.71,5.63-0.71
s4.06,0.2,5.44,0.59c1.39,0.4,2.42,0.88,3.09,1.42C783.22,499.51,783.71,500.34,784.03,501.44z"/>
<path class="st1" d="M818.33,517.8v11.97h23.46c1.82,0,3.18,0.06,4.09,0.17c0.91,0.12,1.96,0.5,3.14,1.13
c2.13,1.1,3.2,4.19,3.2,9.24c0,5.53-1.5,8.77-4.5,9.72c-1.34,0.47-3.4,0.71-6.16,0.71h-23.23v11.97h36.5c1.82,0,3.18,0.06,4.09,0.17
c0.9,0.12,1.95,0.5,3.14,1.13c2.13,1.11,3.2,4.19,3.2,9.24c0,5.53-1.47,8.77-4.39,9.72c-1.34,0.47-3.4,0.71-6.16,0.71h-46.92
c-5.53,0-8.77-1.5-9.72-4.5c-0.47-1.27-0.71-3.28-0.71-6.05v-65.88c0-4.03,0.75-6.77,2.25-8.23c1.5-1.47,4.34-2.2,8.53-2.2h46.69
c1.82,0,3.18,0.06,4.09,0.18c0.9,0.12,1.95,0.49,3.14,1.13c2.13,1.1,3.2,4.18,3.2,9.24c0,5.53-1.47,8.77-4.39,9.72
c-1.34,0.47-3.4,0.71-6.16,0.71H818.33z"/>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

+3 -1
View File
@@ -15,6 +15,7 @@ Static QR Formats:
- 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.
@@ -29,4 +30,5 @@ Animated QR Formats:
Static QR Formats:
- Seed
- SeedSigner Seed QR format
- 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.
+29 -8
View File
@@ -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)
+2 -2
View File
@@ -1,4 +1,4 @@
embit==0.4.5
embit==0.4.10
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
spidev==3.5
+1 -1
View File
@@ -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!",
+2 -2
View File
@@ -5,11 +5,11 @@ persistent_settings = False
[display]
text_color = ORANGE
qr_background_color = FFFFFF
camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
script_policy = PKWSH
custom_derivation_enabled = False
custom_derivation = m/0/0
+12 -27
View File
@@ -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):
+424 -132
View File
@@ -1,19 +1,20 @@
# External Dependencies
import time
import re
from multiprocessing import Process, Queue
from subprocess import call
import os, sys
from embit import bip39, 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
# 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)
from .models import (EncodeQRDensity, QRType, Seed, SeedStorage, Settings, DecodeQR, DecodeQRStatus, EncodeQR, PSBTParser)
class Controller(Singleton):
"""
@@ -32,7 +33,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
@@ -65,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()
@@ -72,6 +74,10 @@ class Controller(Singleton):
controller.io_test_view = IOTestView()
controller.signing_tools_view = SigningToolsView(controller.storage)
controller.settings_tools_view = SettingsToolsView()
controller.screensaver = ScreensaverView(controller.buttons)
controller.screensaver_activation_ms = 120 * 1000
@property
def camera(self):
@@ -80,6 +86,9 @@ class Controller(Singleton):
def start(self) -> None:
opening_splash = OpeningSplashView()
opening_splash.start()
if self.DEBUG:
# Let Exceptions halt execution
try:
@@ -107,6 +116,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
@@ -142,10 +155,10 @@ 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.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:
@@ -174,26 +187,26 @@ class Controller(Singleton):
### Generate Last Word 12 / 24 Menu
def show_generate_last_word_tool(self) -> int:
seed_phrase = []
seed = Seed(wordlist=self.settings.wordlist)
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_display_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 +219,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 +228,19 @@ class Controller(Singleton):
### Create a Seed w/ Dice Screen
def show_create_seed_with_dice_tool(self) -> int:
seed_phrase = []
seed = Seed(wordlist=self.settings.wordlist)
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 +253,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(wordlist=self.settings.wordlist)
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 +291,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(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 ]")
@@ -309,26 +325,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 +352,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])
@@ -394,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
@@ -408,8 +423,7 @@ class Controller(Singleton):
### Generate XPUB
def show_generate_xpub(self):
seed_phrase = []
passphrase = ""
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:
@@ -418,29 +432,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,41 +459,55 @@ 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:
# Cancel
return Path.SEED_TOOLS_SUB_MENU
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"])
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 = self.settings.custom_derivation
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(existing_derivation=self.settings.custom_derivation)
self.settings.custom_derivation = derivation # save for next time
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?")
@@ -490,28 +515,55 @@ 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()
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_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=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)
View.DispShowImage(image)
time.sleep(0.1)
if self.buttons.check_for_low(B.KEY_RIGHT):
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()
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
### Sign Transactions
def show_sign_transaction(self):
seed_phrase = []
passphrase = ""
seed = Seed(wordlist=self.settings.wordlist)
used_saved_seed = False
# reusable qr scan function
@@ -519,7 +571,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:
@@ -527,8 +579,8 @@ 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))
time.sleep(0.1) # turn this up or down to tune performace while decoding psbt
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 performance while decoding psbt
if camera._video_stream is None:
break
@@ -567,10 +619,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
@@ -581,9 +632,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:
@@ -598,14 +648,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:
@@ -626,6 +675,208 @@ class Controller(Singleton):
else:
return Path.MAIN_MENU
elif decoder.isComplete() and decoder.isAddress():
address = decoder.getAddress()
address_type = decoder.getAddressType()
self.menu_view.draw_address(address)
self.buttons.wait_for([B.KEY_RIGHT])
validate_network = NETWORKS[self.settings.network]
validate_network_text = self.settings.network
if "main" in address_type:
validate_network = NETWORKS["main"]
validate_network_text = "main"
elif "test" in address_type:
validate_network = NETWORKS["test"]
validate_network_text = "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("... [ 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:
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_text, "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(["Receive 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()
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:
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()
print(desc_str)
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])
@@ -633,62 +884,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:
@@ -701,14 +947,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,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:
@@ -728,13 +973,22 @@ 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)
time.sleep(0.05)
if self.buttons.check_for_low(B.KEY_RIGHT):
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
@@ -780,15 +1034,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):
@@ -817,8 +1062,55 @@ 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 = "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 == "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 = "333333"
self.settings.qr_background_color = self.current_bg_qr_color
### 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()
+47 -3
View File
@@ -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)
@@ -115,7 +137,18 @@ 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:
if self.GPIO.input(key) == GPIO.LOW:
return True
return False
@@ -133,4 +166,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
@@ -1,9 +1,10 @@
from embit import bip39
from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes
import unicodedata
import hashlib
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")
@@ -13,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(" ".join(mnemonic_copy), ignore_checksum=True)
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()
@@ -27,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()
+1 -1
View File
@@ -27,8 +27,8 @@ class Path:
DONATE = 63
WALLET = 64
QR_DENSITY_SETTING = 65
WALLET_POLICY = 66
PERSISTENT_SETTINGS = 67
CAMERA_ROTATION = 69
RESET = 68
# Seed Slots
+3 -3
View File
@@ -11,15 +11,15 @@ 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):
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
+2
View File
@@ -1,4 +1,6 @@
from .seed import *
from .qr_type import *
from .encode_qr_density import *
from .decode_qr import *
from .encode_qr import *
from .psbt_parser import *
+358 -30
View File
@@ -2,12 +2,13 @@ from pyzbar import pyzbar
from pyzbar.pyzbar import ZBarSymbol
from enum import IntEnum
import re
import json
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 import QRType, Seed
###
### DecodeQR Class
@@ -16,8 +17,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
@@ -25,11 +24,21 @@ 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.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():
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 +51,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
@@ -77,6 +86,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)
@@ -95,6 +111,18 @@ 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
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
@@ -120,6 +148,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):
@@ -135,6 +165,15 @@ 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 getWalletDescriptor(self):
return self.specter_wallet_qr.getWalletDescriptor()
def getPercentComplete(self) -> int:
if self.qr_type == QRType.PSBTUR2:
@@ -152,6 +191,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
@@ -164,7 +208,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
@@ -172,6 +216,16 @@ 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 == QRType.BITCOINADDRESSQR:
return True
return False
def isWalletDescriptor(self):
if self.qr_type in (QRType.SPECTERWALLETQR, QRType.URWALLETQR, QRType.BLUEWALLETQR):
return True
return False
def qrType(self):
return self.qr_type
@@ -188,26 +242,52 @@ class DecodeQR:
return barcode.data.decode("utf-8")
@staticmethod
def SegmentType(s):
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):
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 re.search(r'\d{48,96}', s):
return QRType.SEEDSSQR
elif all(x in bip39.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(" ")):
# 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
# Wallet Descriptor
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\"\:.*', desc_str, re.IGNORECASE):
# if json starting with label and contains descriptor, assume specter wallet json
return QRType.SPECTERWALLETQR
# 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
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
return QRType.INVALID
@staticmethod
def isBase64(s):
@@ -226,6 +306,61 @@ 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)
@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
### Purpose: used in DecodePSBTQR to decode Specter Desktop Animated QR PSBT encoding
@@ -367,7 +502,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:
@@ -410,23 +545,67 @@ class Base64DecodeQR:
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
### 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 SeedQR:
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 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:
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):
_4LETTER_WORDLIST = [word[:4].strip() for word in self.wordlist]
if qr_type == QRType.SEEDSSQR:
try:
@@ -436,7 +615,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 = self.wordlist[index]
self.seed_phrase.append(word)
if len(self.seed_phrase) > 0:
if self.is1224Phrase() == False:
@@ -453,7 +632,10 @@ class SeedQR:
try:
# embit mnemonic code to validate
bip39.mnemonic_to_seed(segment.strip())
seed = Seed(segment, passphrase="", wordlist=self.wordlist)
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 +651,13 @@ class SeedQR:
sl = segment.strip().split(" ")
words = []
for s in sl:
words.append(bip39.WORDLIST[DecodeQR._4LETTER_WORDLIST.index(s)])
words.append(self.wordlist[_4LETTER_WORDLIST.index(s)])
# embit mnemonic code to validate
bip39.mnemonic_to_seed(" ".join(words).strip())
seed = Seed(words, passphrase="", wordlist=self.wordlist)
if not seed:
# seed is not valid, return invalid
return DecodeQRStatus.INVALID
self.seed_phrase = words
if self.is1224Phrase() == False:
return DecodeQRStatus.INVALID
@@ -496,6 +681,149 @@ 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
###
### 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
+40 -35
View File
@@ -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
@@ -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 import Seed, QRType, EncodeQRDensity
###
### EncodeQR Class
@@ -16,16 +16,18 @@ from seedsigner.models.qr_type import QRType
class EncodeQR:
WORDLIST = None
def __init__(self, **kwargs):
self.psbt = None
self.seed_phrase = None
self.passphrase = None
self.derivation = None
self.network = None
self.policy = None
self.qr_type = None
self.qr_density = None
self.qr = QR()
self.wordlist = None
for key, value in kwargs.items():
if key == "psbt":
@@ -38,12 +40,15 @@ 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":
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')
@@ -57,11 +62,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.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.qr_density, self.wordlist)
else:
raise Exception('Encoder Type not Supported')
@@ -74,9 +79,12 @@ 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)
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()
@@ -184,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
@@ -193,7 +205,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
@@ -203,29 +215,27 @@ class SeedSSQR:
class XPubQR:
def __init__(self, seed_phrase, passphrase, derivation, network, policy):
def __init__(self, seed_phrase, passphrase, derivation, network, 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
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"])
if self.wordlist == None:
raise Exception('Wordlist Required')
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[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)
@@ -249,7 +259,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, qr_density, wordlist):
self.qr_max_fragement_size = 65
if qr_density == EncodeQRDensity.LOW:
self.qr_max_fragement_size = 40
@@ -258,7 +268,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, wordlist)
self.__createParts()
def __createParts(self):
@@ -303,9 +313,4 @@ class SpecterXPubQR(XPubQR):
return len(self.parts)
def isComplete(self):
return self.sent_complete
class EncodeQRDensity(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
return self.sent_complete
@@ -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
+18 -19
View File
@@ -1,12 +1,12 @@
from embit import psbt, script, ec, bip32, bip39
from embit.networks import NETWORKS
from io import BytesIO
from seedsigner.models 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 +18,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(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 +37,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:
@@ -57,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
+5
View File
@@ -9,6 +9,7 @@ class QRType(IntEnum):
PSBTBASE64 = 1
PSBTSPECTER = 2
PSBTURLEGACY = 3
PSBTBASE43 = 4
PSBTUR2 = 5
SEEDSSQR = 6
SEEDUR2 = 7
@@ -16,4 +17,8 @@ class QRType(IntEnum):
SEED4LETTERMNEMONIC = 9
XPUBQR = 20
SPECTERXPUBQR = 21
BITCOINADDRESSQR = 30
SPECTERWALLETQR = 40
URWALLETQR = 41
BLUEWALLETQR = 42
INVALID = 100
+108
View File
@@ -0,0 +1,108 @@
# 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
self._wordlist = wordlist
if self._wordlist == None:
raise Exception('Wordlist Required')
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)
@property
def mnemonic_display_list(self):
return unicodedata.normalize("NFC", self._mnemonic).split()
@mnemonic.setter
def mnemonic(self, value):
if isinstance(value, list):
self._mnemonic = unicodedata.normalize("NFKD", " ".join(value).strip())
elif isinstance(value, str):
self._mnemonic = unicodedata.normalize("NFKD", value.strip())
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()
@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):
if isinstance(other, Seed):
return self.seed == other.seed
return False
def __bool__(self):
return self._valid
+156 -125
View File
@@ -1,151 +1,182 @@
# External Dependencies
from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes
from embit import bip39
from seedsigner.models import Seed
class SeedStorage:
SEEDWORDS = bip39.WORDLIST
def __init__(self) -> None:
self.seeds = []
# 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_mnemonic(self, mnemonic, passphrase: str = "", slot_num = -1) -> bool:
self.seeds.append(Seed(mnemonic, passphrase))
def __init__(self) -> None:
# used to track virtual slot number, TODO: remove concept in UI of slots
if slot_num > 0:
self.slots[slot_num - 1] = len(self.seeds) - 1
def add_seed(self, seed: Seed, slot_num = -1):
self.seeds.append(seed)
# used to track virtual slot number, TODO: remove concept in UI of slots
if slot_num > 0:
self.slots[slot_num - 1] = len(self.seeds) - 1
def add_passphrase_to_seed(self, seed, passphrase: str = ""):
if not isinstance(passphrase, str):
raise ValueError("Invalid passphrase format")
# find existing seed to add passphrase to
if seed in self.seeds:
idx = self.seeds.index(seed)
seed.passphrase = passphrase
return True
else:
return False
def remove_passphrase_from_seed(self, seed):
return self.add_passphrase_to_seed(seed, "")
# validates mnemonic part of a seed in list or string format
def validate_mnemonic(self, seed_phrase) -> bool:
try:
seed = Seed(seed_phrase)
except Exception as e:
return False
return True
def seed_count(self):
return len(self.seeds)
self.saved_seeds = [[],[],[]]
self.passphrase = ["","",""]
#
# Slot related methods to be removed once the concepts of slots is removed
# TODO: remove concept in UI of slots
#
def get_mnemonic_from_slot(self, slot_num = 0):
if self.slots[slot_num - 1] == -1:
raise ValueError("Slot is unexpectedly empty")
return self.seeds[self.slots[slot_num - 1]].mnemonic
###
### Seed Related Methods
###
def get_passphrase_from_slot(self, slot_num = 0):
if self.slots[slot_num - 1] == -1:
raise ValueError("Slot is unexpectedly empty")
return self.seeds[self.slots[slot_num - 1]].passphrase
def add_passphrase_to_slot(self, passphrase: str = "", slot_num = -1):
if self.slots[slot_num - 1] == -1:
raise ValueError("Slot is unexpectedly empty")
return self.add_passphrase_to_seed(self.seeds[self.slots[slot_num - 1]], passphrase)
def 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 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 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_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 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 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 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 ""
def check_slot(self, slot_num) -> bool:
if self.slots[slot_num-1] != -1:
return True
else:
return False
def delete_passphrase(self, slot_num) -> bool:
slot_idx = slot_num - 1
self.passphrase[slot_idx] = ""
def check_slot_passphrase(self, slot_num) -> bool:
if self.slots[slot_num-1] != -1:
if self.seeds[self.slots[slot_num-1]].passphrase != "":
return True
return False
def slot_avaliable(self) -> bool:
if self.slots[0] == -1 or self.slots[1] == -1 or self.slots[2] == -1:
return True
return True
def num_of_saved_seeds(self) -> int:
count = 0
if self.slots[0] != -1:
count += 1
if self.slots[1] != -1:
count += 1
if self.slots[2] != -1:
count += 1
return True
return count
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 num_of_free_slots(self) -> int:
count = 0
if self.slots[0] == -1:
count += 1
if self.slots[1] == -1:
count += 1
if self.slots[2] == -1:
count += 1
def check_slot_1(self) -> bool:
if len(self.saved_seeds[0]) > 0:
return True
else:
return False
return count
def check_slot_2(self) -> bool:
if len(self.saved_seeds[1]) > 0:
return True
else:
return False
def save_seed_phrase(self, seed_phrase = [], slot_num = 0) -> bool:
return self.add_mnemonic(seed_phrase,slot_num=slot_num)
def check_slot_3(self) -> bool:
if len(self.saved_seeds[2]) > 0:
return True
else:
return False
def save_passphrase(self, passphrase, slot_num = 0) -> bool:
return self.add_passphrase_to_slot(passphrase, slot_num=slot_num)
def check_slot(self, slot_num) -> bool:
if len(self.saved_seeds[slot_num-1]) > 0:
return True
else:
return False
def get_seed_phrase(self, slot_num) -> []:
if self.slots[slot_num - 1] == -1:
return []
def check_slot_passphrase(self, slot_num) -> bool:
if len(self.passphrase[slot_num-1]) > 0:
return True
else:
return False
return self.seeds[self.slots[slot_num - 1]].mnemonic.split()
def check_if_seed_valid(self, seed_phrase) -> bool:
try:
bip39.mnemonic_to_seed((" ".join(seed_phrase)).strip())
except ValueError:
return False
def get_passphrase(self, slot_num) -> str:
if self.slots[slot_num - 1] == -1:
return ""
return True
return self.seeds[self.slots[slot_num - 1]].passphrase
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 get_seed(self, slot_num) -> Seed:
if self.slots[slot_num - 1] == -1:
return Seed()
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
return self.seeds[self.slots[slot_num - 1]]
return count
def delete_passphrase(self, slot_num) -> bool:
if self.slots[slot_num - 1] == -1:
return False
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
return self.remove_passphrase_from_seed(self.seeds[self.slots[slot_num - 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
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
return count
def check_if_seed_valid(self, seed_phrase) -> bool:
return self.validate_mnemonic(seed_phrase)
+73 -55
View File
@@ -1,8 +1,9 @@
from seedsigner.helpers import Singleton
from .encode_qr import EncodeQRDensity
from .qr_type import QRType
from .encode_qr_density import EncodeQRDensity
import configparser
from embit import bip39
class Settings(Singleton):
@@ -12,7 +13,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
@@ -21,17 +22,18 @@ class Settings(Singleton):
'system': {
'debug': False,
'default_language': "en",
'persistent_settings': False
'persistent_settings': False,
'wordlist': bip39.WORDLIST
},
'display': {
'text_color': "ORANGE",
'qr_background_color': "FFFFFF",
'camera_rotation': 0
},
'wallet': {
'network': "main",
'software': "Specter Desktop",
'qr_density': EncodeQRDensity.MEDIUM,
'script_policy': "PKWSH",
'custom_derivation_enabled': False,
'custom_derivation': 'm/0/0'
}
}
@@ -49,12 +51,12 @@ 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"])
self.script_policy = config["wallet"]["script_policy"]
self.custom_derivation_enabled = config.getboolean("wallet", "custom_derivation_enabled")
self.custom_derivation = config["wallet"]["custom_derivation"]
self.qr_background_color = config["display"]["qr_background_color"]
### persistent settings handling
@@ -116,12 +118,37 @@ class Settings(Singleton):
@property
def language(self):
return self._data["system"]["default_language"]
@property
def wordlist(self):
return self._data["system"]["wordlist"]
### display
@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()
@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
@@ -190,59 +217,50 @@ 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
@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 wallet.custom_derivation_enabled settings.ini value")
raise Exception("Unexpected network type")
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,95 @@
Copyright 2020 The Assistant Project Authors (https://github.com/hafontia/Assistant).
Copyright 2010 The Source Sans Pro Authors (https://github.com/adobe-fonts/source-sans-pro), with Reserved Font Name 'Source'.
Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

+1
View File
@@ -4,3 +4,4 @@ from .menu_view import *
from .seed_tools_view import *
from .settings_tools_view import *
from .signing_tools_view import *
from .screensaver import OpeningSplashView, ScreensaverView
+9 -6
View File
@@ -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()
@@ -75,7 +78,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 +86,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 +144,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
+16 -17
View File
@@ -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
@@ -124,14 +124,13 @@ class MenuView(View):
### Settings Menu
def display_settings_menu(self) -> int:
lines = ["... [ Return to Main ]", "Wallet: <wallet>", "Script Policy: <policy>", "Network: <network>", "QR Density: <density>", "Input / Output Tests", "Persistent Settings: <persistent>", "Version Info", "Donate to SeedSigner", "Reset SeedSigner"]
lines = ["... [ Return to Main ]", "Wallet: <wallet>", "Network: <network>", "QR Density: <density>", "Input / Output Tests", "Persistent Settings: <persistent>", "Camera Rotation", "Version Info", "Donate to SeedSigner", "Reset SeedSigner"]
input = 0
lines[1] = lines[1].replace("<wallet>", Settings.get_instance().software)
lines[2] = lines[2].replace("<policy>", Settings.get_instance().policy_name)
lines[3] = lines[3].replace("<network>", Settings.get_instance().network)
lines[4] = lines[4].replace("<density>", Settings.get_instance().qr_density_name)
lines[6] = lines[6].replace("<persistent>", Settings.get_instance().persistent_display)
lines[2] = lines[2].replace("<network>", Settings.get_instance().network)
lines[3] = lines[3].replace("<density>", Settings.get_instance().qr_density_name)
lines[5] = lines[5].replace("<persistent>", Settings.get_instance().persistent_display)
# Draw Menu
self.selected_menu_num = 1
@@ -150,15 +149,15 @@ 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 == 7:
return Path.CAMERA_ROTATION
elif self.selected_menu_num == 8:
return Path.VERSION_INFO
elif self.selected_menu_num == 9:
@@ -311,8 +310,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 +349,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 +377,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
+136
View File
@@ -0,0 +1,136 @@
import os
import random
import time
from PIL import Image, ImageDraw
from . import View
from seedsigner.helpers import B
class LogoView(View):
def __init__(self):
dirname = os.path.dirname(__file__)
logo_url = os.path.join(dirname, "../../", "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()
# Fade in alpha
for i in range(250, -1, -25):
self.logo.putalpha(255 - i)
background = Image.new("RGBA", self.logo.size, (0,0,0))
View.disp.ShowImage(Image.alpha_composite(background, self.logo), 0, 0)
# Display version num and hold for a few seconds
font = View.ROBOTOCONDENSED_REGULAR_22
version = f"v{controller.VERSION}"
tw, th = font.getsize(version)
x = int((View.canvas_width - tw) / 2)
y = int(View.canvas_height / 2) + 40
draw = ImageDraw.Draw(self.logo)
draw.text((x, y), version, fill="orange", font=font)
View.DispShowImage(self.logo)
time.sleep(3)
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):
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):
# Restore the original screen
View.DispShowImage(self.last_screen)
self._is_running = False
File diff suppressed because one or more lines are too long
+185 -27
View File
@@ -1,10 +1,8 @@
# 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):
def __init__(self) -> None:
@@ -12,6 +10,7 @@ class SettingsToolsView(View):
self.qr = QR()
self.donate_image = None
self.derivation = None
### Donate Menu Item
@@ -64,24 +63,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 ]"]
@@ -98,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
###
@@ -109,10 +112,165 @@ 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()
###
### 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="/")
keyboard_digits.render_keys()
render_right_panel()
text_entry_display.render(self.derivation)
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()
+23 -23
View File
@@ -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 = ""
@@ -45,45 +45,45 @@ 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.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.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:
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()
+166 -81
View File
@@ -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)
@@ -67,6 +77,7 @@ class View:
disp = None
previous_button_width = None
def __init__(self) -> None:
# Import here to avoid circular imports
from seedsigner.controller import Controller
@@ -88,13 +99,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
@@ -123,6 +144,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)
@@ -134,11 +158,12 @@ 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)
text_overlay_draw.text(((240 - tw) / 2, 240 - th - 1), text, fill=text_color, font=font)
View.DispShowImage(image_copy, alpha_overlay=text_overlay)
@@ -146,92 +171,152 @@ 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()
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_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:
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)
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 +336,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()
+2 -2
View File
@@ -5,11 +5,11 @@ persistent_settings = False
[display]
text_color = ORANGE
qr_background_color = FFFFFF
camera_rotation = 0
[wallet]
network = main
software = Prompt
qr_density = 2
script_policy = PKWSH
custom_derivation_enabled = False
custom_derivation = m/0/0
+32 -26
View File
@@ -6,37 +6,43 @@ 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
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"
+132 -24
View File
@@ -1,8 +1,7 @@
import pytest
from mock import MagicMock
from seedsigner.models.decode_qr import DecodeQR, QRType, DecodeQRStatus
from seedsigner.models.psbt_parser import PSBTParser
from embit import psbt
from seedsigner.models import Seed, DecodeQR, DecodeQRStatus, QRType, EncodeQRDensity, PSBTParser
from embit import psbt, bip39
# this is an of this bug: https://github.com/Foundation-Devices/foundation-ur-py/issues/3
@@ -40,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
@@ -62,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
@@ -80,7 +79,13 @@ 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, 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
@@ -101,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
@@ -121,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
@@ -130,7 +135,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
@@ -148,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
@@ -157,7 +162,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
@@ -201,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
@@ -213,7 +218,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
@@ -235,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
@@ -247,7 +252,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
@@ -273,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
@@ -285,7 +290,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
@@ -306,30 +311,133 @@ 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
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"
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
+12 -13
View File
@@ -1,8 +1,7 @@
import pytest
from mock import MagicMock
from seedsigner.models.decode_qr import QRType
from seedsigner.models.encode_qr import EncodeQR, 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():
@@ -11,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:
@@ -26,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:
@@ -61,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())
@@ -71,19 +70,19 @@ 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", 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():
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", 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"
+2 -2
View File
@@ -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))
+8
View File
@@ -0,0 +1,8 @@
import pytest
from mock import MagicMock
from seedsigner.models import PSBTParser
from embit import psbt
def test_sig_count():
print("test")
File diff suppressed because one or more lines are too long