mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge pull request #122 from SeedSigner/0_4_5
Merge 0.4.5 into Main Branch
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
src/seedsigner.egg-info/
|
||||
.nova
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user