Skip to content

Commit 5117e5c

Browse files
committed
slip39.App works
1 parent d91b9c1 commit 5117e5c

File tree

5 files changed

+358
-229
lines changed

5 files changed

+358
-229
lines changed

SLIP39.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

slip39/App/main.py

Lines changed: 143 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
import argparse
22
import logging
33
import math
4-
import sys
4+
import os
55

66
import PySimpleGUI as sg
77

88
from ..types import Account
99
from ..api import create, group_parser
1010
from ..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

1518
I_kwds = dict(
1619
change_submits = True,
1720
font = font,
1821
)
19-
2022
T_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

166242
def 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

slip39/defaults.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,9 @@
6969

7070

7171
BAUDRATE = 115200
72+
73+
FILENAME_KEYWORDS = [ 'name', 'date', 'time', 'crypto', 'path', 'address' ]
74+
FILENAME_FORMAT = "{name}-{date}+{time}-{crypto}-{address}.pdf"
75+
76+
# Default Crypto accounts (and optional paths) to generate
77+
CRYPTO_PATHS = [ 'ETH', 'BTC' ]

0 commit comments

Comments
 (0)