11import argparse
22import logging
33import math
4- import sys
4+ import os
55
66import PySimpleGUI as sg
77
88from ..types import Account
99from ..api import create , group_parser
1010from ..util import log_level , log_cfg
11- from ..defaults import GROUPS , GROUP_THRESHOLD_RATIO
11+ from ..layout import write_pdfs
12+ from ..defaults import GROUPS , GROUP_THRESHOLD_RATIO , CRYPTO_PATHS
1213
13- font = ('Sans' , 12 )
14+ log = logging .getLogger ( __package__ )
15+
16+ font = ('Courier' , 14 )
1417
1518I_kwds = dict (
1619 change_submits = True ,
1720 font = font ,
1821)
19-
2022T_kwds = dict (
2123 font = font ,
2224)
25+ B_kwds = dict (
26+ font = font ,
27+ )
2328
2429
25- def groups_layout ( names , group_threshold , groups ):
30+ def groups_layout ( names , group_threshold , groups , passphrase = None ):
2631 """Return a layout for the specified number of SLIP-39 groups.
2732
2833 """
@@ -52,21 +57,37 @@ def groups_layout( names, group_threshold, groups ):
5257 ], key = '-GROUP-SIZES-' ) ]]
5358 ),
5459 ]
55-
60+ prefix = ( 30 , 1 )
5661 layout = [
5762 [
63+ sg .Text ( "Save PDF to (ie. USB drive): " , size = prefix , ** T_kwds ),
64+ sg .Input ( sg .user_settings_get_entry ( "-target folder-" , "" ), k = '-TARGET-' , ** I_kwds ),
65+ sg .FolderBrowse ( ** B_kwds )
66+ ],
67+ ] + [
68+ [
69+ sg .Text ( "Seed File Name(s): " , size = prefix , ** T_kwds ),
5870 sg .Input ( f"{ ', ' .join ( names )} " , key = '-NAMES-' , ** I_kwds ),
59- sg .Text ( "Requires collection of at least" , ** T_kwds ),
71+ sg .Text ( "(comma-separted)" , ** T_kwds ),
72+ ]
73+ ] + [
74+ [
75+ sg .Text ( "Requires recovery of at least: " , size = prefix , ** T_kwds ),
6076 sg .Input ( f"{ group_threshold } " , key = '-THRESHOLD-' , ** I_kwds ),
61- sg .Text ( f" of { len (groups )} SLIP-39 Recovery Groups" , ** T_kwds ),
77+ sg .Text ( f" of { len (groups )} SLIP-39 Recovery Groups" , key = '-RECOVERY-' , ** T_kwds ),
78+ ],
79+ ] + [
80+ [
81+ sg .Text ( "Passphrase to encrypt Seed: " , size = prefix , ** T_kwds ),
82+ sg .Input ( f"{ passphrase or '' } " , key = '-PASSPHRASE-' , ** I_kwds ),
6283 ],
6384 ] + [
6485 [
6586 sg .Frame ( 'Groups' , [group_body ], key = '-GROUPS-' ),
6687 ],
6788 ] + [
6889 [
69- sg .Button ( '+' ), sg .Button ( 'Generate' ), sg .Exit ()
90+ sg .Button ( '+' , ** B_kwds ), sg .Button ( 'Save' , ** B_kwds ), sg .Exit ()
7091 ],
7192 [
7293 sg .Frame (
@@ -80,28 +101,70 @@ def groups_layout( names, group_threshold, groups ):
80101 [[ sg .Text ( key = '-STATUS-' , ** T_kwds ), ]]
81102 ),
82103 ],
83-
84104 ]
85105 return layout
86106
87107
88- def app ( names , group_threshold , groups , cryptopaths ):
108+ def app (
109+ names = None ,
110+ group = None ,
111+ threshold = None ,
112+ cryptocurrency = None ,
113+ passphrase = None ,
114+ ):
115+ # Convert sequence of group specifications into standard { "<group>": (<needs>, <size>) ... }
116+ if isinstance ( group , dict ):
117+ groups = group
118+ else :
119+ groups = dict (
120+ group_parser ( g )
121+ for g in group or GROUPS
122+ )
123+ assert groups and all ( isinstance ( k , str ) and len ( v ) == 2 for k ,v in groups .items () ), \
124+ f"Each group member specification must be a '<group>': (<needs>, <size>) pair, not { type (next (groups .items ()))} "
125+
126+ group_threshold = int ( threshold ) if threshold else math .ceil ( len ( groups ) * GROUP_THRESHOLD_RATIO )
127+
128+ cryptopaths = []
129+ for crypto in cryptocurrency or CRYPTO_PATHS :
130+ try :
131+ if isinstance ( crypto , str ):
132+ crypto ,paths = crypto .split ( ':' ) # Find the <crypto>,<path> tuple
133+ else :
134+ crypto ,paths = crypto # Already a <crypto>,<path> tuple?
135+ except ValueError :
136+ crypto ,paths = crypto ,None
137+ cryptopaths .append ( (crypto ,paths ) )
138+
89139 sg .theme ( 'DarkAmber' )
90140
91- layout = groups_layout ( names , group_threshold , groups )
141+ sg .user_settings_set_entry ( '-target folder-' , os .getcwd () )
142+
143+ layout = groups_layout ( names , group_threshold , groups , passphrase )
92144 window = None
93145 status = None
94146 event = False
95147 while event not in (sg .WIN_CLOSED , 'Exit' ,):
148+ # Create window (for initial window.read()), or update status
96149 if window :
97150 window ['-STATUS-' ].update ( status or 'OK' )
151+ window ['-RECOVERY-' ].update ( f"of { len (groups )} SLIP-39 Recovery Groups" , ** T_kwds ),
98152 else :
99153 window = sg .Window ( f"{ ', ' .join ( names )} Mnemonic Cards" , layout )
100154
101155 status = None
102156 event , values = window .read ()
103157 logging .info ( f"{ event } , { values } " )
104158
159+ if '-TARGET-' in values :
160+ # A target directory has been selected;
161+ try :
162+ os .chdir ( values ['-TARGET-' ] )
163+ except Exception as exc :
164+ status = f"Error changing to target directory { values ['-TARGET-' ]} : { exc } "
165+ logging .exception ( f"{ status } " )
166+ continue
167+
105168 if event == '+' :
106169 g = len (groups )
107170 name = f"Group { g + 1 } "
@@ -112,55 +175,68 @@ def app( names, group_threshold, groups, cryptopaths ):
112175 window .extend_layout ( window ['-GROUP-NEEDS-' ], [[ sg .Input ( f"{ needs [0 ]} " , key = f"-G-NEED-{ g } " , ** I_kwds ) ]] ) # noqa: 241
113176 window .extend_layout ( window ['-GROUP-SIZES-' ], [[ sg .Input ( f"{ needs [1 ]} " , key = f"-G-SIZE-{ g } " , ** I_kwds ) ]] ) # noqa: 241
114177
115- if event == 'Generate' :
178+ try :
179+ g_thr_val = values ['-THRESHOLD-' ]
180+ g_thr = int ( g_thr_val )
181+ except Exception as exc :
182+ status = f"Error defining group threshold { g_thr_val } : { exc } "
183+ logging .exception ( f"{ status } " )
184+ continue
185+
186+ g_rec = {}
187+ status = None
188+ for g in range ( 16 ):
189+ grp_idx = f"-G-NAME-{ g } "
190+ if grp_idx not in values :
191+ break
192+ grp = '?'
116193 try :
117- g_thr_val = values ['-THRESHOLD-' ]
118- g_thr = int ( g_thr_val )
194+ grp = str ( values [grp_idx ] )
195+ g_rec [ grp ] = int ( values [ f"-G-NEED- { g } " ] or 0 ), int ( values [ f"-G-SIZE- { g } " ] or 0 )
119196 except Exception as exc :
120- status = f"Error defining group threshold { g_thr_val } : { exc } "
197+ status = f"Error defining group { g + 1 } { grp !r } : { exc } "
121198 logging .exception ( f"{ status } " )
122199 continue
123200
124- g_rec = {}
125- status = None
126- for g in range ( 16 ):
127- nam_idx = f"-G-NAME-{ g } "
128- if nam_idx not in values :
129- break
130- try :
131- nam = values [nam_idx ]
132- req ,siz = int ( values [f"-G-NEED-{ g } " ] ), int ( values [f"-G-SIZE-{ g } " ] )
133- g_rec [nam ] = (req , siz )
134- except Exception as exc :
135- status = f"Error defining group { g + 1 } : { exc } "
136- logging .exception ( f"{ status } " )
137- continue
138-
139- summary = f"Require { g_thr } /{ len (g_rec )} Groups, from: { f', ' .join ( f'{ n } ({ need } /{ size } )' for n ,(need ,size ) in g_rec .items ())} "
140- window ['-SUMMARY-' ].update ( summary )
141- if status is None :
142- details = {}
143- try :
144- for name in names :
145- details [name ] = create (
146- name = name ,
147- group_threshold = group_threshold ,
148- groups = g_rec ,
149- cryptopaths = cryptopaths ,
150- )
151- except Exception as exc :
152- status = f"Error creating: { exc } "
153- logging .exception ( f"{ status } " )
154- continue
155-
156- status = 'OK'
157- for n in names :
158- accts = ', ' .join ( f'{ a .crypto } @ { a .path } : { a .address } ' for a in details [n ].accounts [0 ] )
159- status += f"; { accts } "
201+ summary = f"Require { g_thr } /{ len (g_rec )} Groups, from: { f', ' .join ( f'{ n } ({ need } /{ size } )' for n ,(need ,size ) in g_rec .items ())} "
202+ passphrase = values ['-PASSPHRASE-' ].strip ()
203+ if passphrase :
204+ summary += f", decrypted w/ passphrase { passphrase !r} "
205+ window ['-SUMMARY-' ].update ( summary )
160206
161- window .close ()
207+ names = [
208+ name .strip ()
209+ for name in values ['-NAMES-' ].split ( ',' )
210+ if name
211+ ]
212+ details = {}
213+ try :
214+ for name in names or [ "SLIP39" ]:
215+ details [name ] = create (
216+ name = name ,
217+ group_threshold = group_threshold ,
218+ groups = g_rec ,
219+ cryptopaths = cryptopaths ,
220+ passphrase = passphrase .encode ( 'utf-8' ) if passphrase else b'' ,
221+ )
222+ except Exception as exc :
223+ status = f"Error creating: { exc } "
224+ logging .exception ( f"{ status } " )
225+ continue
162226
163- return 0
227+ # If we get here, no failure status has been detected; we could save (details is now { "<filename>": <details> })
228+ if event == 'Save' :
229+ details = write_pdfs (
230+ names = details ,
231+ )
232+
233+ name_len = max ( len ( name ) for name in details )
234+ status = '\n ' .join (
235+ f"{ name :>{name_len }} == " + ', ' .join ( f'{ a .crypto } @ { a .path } : { a .address } ' for a in details [name ].accounts [0 ] )
236+ for name in details
237+ )
238+
239+ window .close ()
164240
165241
166242def main ( argv = None ):
@@ -181,6 +257,9 @@ def main( argv=None ):
181257 ap .add_argument ( '-c' , '--cryptocurrency' , action = 'append' ,
182258 default = [],
183259 help = f"A crypto name and optional derivation path ('../<range>/<range>' allowed); defaults: { ', ' .join ( f'{ c } :{ Account .path_default (c )} ' for c in Account .CRYPTOCURRENCIES )} " )
260+ ap .add_argument ( '--passphrase' ,
261+ default = None ,
262+ help = "Encrypt the master secret w/ this passphrase, '-' reads it from stdin (default: None/'')" )
184263 ap .add_argument ( 'names' , nargs = "*" ,
185264 help = "Account names to produce" )
186265 args = ap .parse_args ( argv )
@@ -192,26 +271,15 @@ def main( argv=None ):
192271 if args .verbose :
193272 logging .getLogger ().setLevel ( log_cfg ['level' ] )
194273
195- names = args .names or [ 'SLIP-39' ]
196- groups = dict (
197- group_parser ( g )
198- for g in args .group or GROUPS
199- )
200- group_threshold = args .threshold or math .ceil ( len ( groups ) * GROUP_THRESHOLD_RATIO )
201-
202- cryptopaths = []
203- for crypto in args .cryptocurrency or ['ETH' , 'BTC' ]:
204- try :
205- crypto ,paths = crypto .split ( ':' )
206- except ValueError :
207- crypto ,paths = crypto ,None
208- cryptopaths .append ( (crypto ,paths ) )
209-
210- sys .exit (
274+ try :
211275 app (
212- names = names ,
213- group_threshold = group_threshold ,
214- groups = groups ,
215- cryptopaths = cryptopaths ,
276+ names = args .names ,
277+ threshold = args .threshold ,
278+ group = args .group ,
279+ cryptocurrency = args .cryptocurrency ,
280+ passphrase = args .passphrase ,
216281 )
217- )
282+ except Exception as exc :
283+ log .exception ( f"Failed running App: { exc } " )
284+ return 1
285+ return 0
0 commit comments