Skip to content

Commit aaedd77

Browse files
committed
Progress toward paper wallet support
1 parent 5b1eae1 commit aaedd77

File tree

3 files changed

+78
-34
lines changed

3 files changed

+78
-34
lines changed

setup.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@
3131
extras_require = {
3232
option: open( os.path.join( HERE, f"requirements-{option}.txt" )).readlines()
3333
for option in [
34-
'serial', # slip39[serial]: Support serial I/O of generated wallet data
35-
'wallet', # slip39[wallet]: Support output of encrypted BIP-38 and Ethereum JSON wallets
3634
'gui', # slip39[gui]: Support PySimpleGUI/tkinter Graphical UI App
35+
'serial', # slip39[serial]: Support serial I/O of generated wallet data
3736
'dev', # slip39[dev]: All modules to support development
3837
]
3938
}

slip39/layout.py

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,15 @@ def elements( self ):
9898

9999

100100
class Text( Region ):
101+
SIZE_RATIO = 3/4
102+
101103
def __init__( self, *args, font=None, text=None, size=None, size_ratio=None, align=None,
102104
foreground=None, background=None, bold=None, italic=None, underline=None,
103105
**kwds ):
104106
self.font = font
105107
self.text = text
106108
self.size = size
107-
self.size_ratio = size_ratio or 3/4
109+
self.size_ratio = size_ratio or self.SIZE_RATIO
108110
self.align = align
109111
self.foreground = foreground
110112
self.background = background
@@ -258,18 +260,18 @@ def layout_wallet(
258260
# Assign each different Crypto name a different color, in template labels crypto-{f,b}1, crypto-{f,b}2, ...
259261
for c_n in range( len( COLOR )):
260262
public.add_region_proportional(
261-
Text( f'crypto-f{c_n}', x1=1/8, y1=-1/16, x2=7/8, y2=8/16, foreground=int( COLOR[c_n], 16 ), rotate=-45 )
263+
Text( f'crypto-f{c_n}', x1=1/8, y1=-1/16, x2=7/8, y2=7/16, foreground=int( COLOR[c_n], 16 ), rotate=-45 )
262264
)
263265
private.add_region_proportional(
264-
Text( f'crypto-b{c_n}', x1=2/8, y1=-1/16, x2=7/8, y2=8/16, foreground=int( COLOR[c_n], 16 ), rotate=-45 )
266+
Text( f'crypto-b{c_n}', x1=2/8, y1=-1/16, x2=7/8, y2=7/16, foreground=int( COLOR[c_n], 16 ), rotate=-45 )
265267
)
266268

267269
# Public addresses are vertical on left- and right-hand of public Region. In order to fit the
268270
# longer ETH addresses into a space with a fixed-width font, we know that the ratio of the width
269271
# to the height has to be about 1/20. Rotation is downward around upper-left corner; so,
270272
# lower-left corner will shift 1 height leftward and upward; so start 1 height right and down.
271273
address_length = public.y2 - public.y1
272-
address_height = address_length * 1/19
274+
address_height = address_length * 1/20
273275
public.add_region(
274276
Image(
275277
'address-l-bg',
@@ -282,6 +284,7 @@ def layout_wallet(
282284
).add_region(
283285
Text(
284286
'address-l',
287+
font = 'mono',
285288
rotate = -90,
286289
)
287290
)
@@ -300,13 +303,14 @@ def layout_wallet(
300303
).add_region(
301304
Text(
302305
'address-r',
306+
font = 'mono',
303307
rotate = +90,
304308
)
305309
)
306310

307311
# Make Public QR Code square w/ min of height, width, anchored at lower-left corner
308312
public.add_region_proportional(
309-
Text( 'address-qr-top', x1=1/16, y1=5/16, x2=1, y2=6/16 )
313+
Text( 'address-qr-t', x1=1/16, y1=5/16, x2=1, y2=6/16 )
310314
)
311315
public_qr = public.add_region_proportional(
312316
Image( 'address-qr', x1=1/16, y1=6/16, x2=1, y2=15/16 )
@@ -315,12 +319,13 @@ def layout_wallet(
315319
public_qr.x2 = public_qr.x1 + public_qr_size
316320
public_qr.y1 = public_qr.y2 - public_qr_size
317321
public.add_region_proportional(
318-
Text( 'address-qr-bot', x1=1/16, y1=15/16, x2=1, y2=16/16 )
322+
Text( 'address-qr-b', x1=1/16, y1=15/16, x2=1, y2=16/16 )
319323
)
320324

321325
# Private region
326+
322327
private.add_region_proportional(
323-
Text( 'private-qr-top', x1=0/16, y1=5/16, x2=1, y2=6/16 )
328+
Text( 'private-qr-t', x1=0/16, y1=5/16, x2=1, y2=6/16 )
324329
)
325330
private_qr = private.add_region_proportional(
326331
Image( 'private-qr', x1=0/16, y1=6/16, x2=1, y2=15/16 )
@@ -329,33 +334,56 @@ def layout_wallet(
329334
private_qr.x2 = private_qr.x1 + private_qr_size
330335
private_qr.y1 = private_qr.y2 - private_qr_size
331336
private.add_region_proportional(
332-
Text( 'private-qr-bot', x1=0/16, y1=15/16, x2=1, y2=16/16 )
337+
Text( 'private-qr-b', x1=0/16, y1=15/16, x2=1, y2=16/16 )
338+
)
339+
340+
# Hint above; but same computed width as private_qr
341+
private_h_t = private.add_region_proportional(
342+
Text( 'private-hint-t', x1=0/16, y1=0/16, x2=1, y2=1/16 )
333343
)
344+
private_h_t.x2 = private_qr.x2
345+
private_h_bg = private.add_region_proportional(
346+
Image( 'private-hint-bg',x1=0/16, y1=1/16, x2=1, y2=4/16 )
347+
)
348+
private_h_bg.x2 = private_qr.x2
349+
private_h = private.add_region_proportional(
350+
Text( 'private-hint', x1=0/16, y1=1/16, x2=1, y2=3/16 )
351+
)
352+
private_h.x2 = private_qr.x2
334353

335354
# We'll use the right side of the private region, each line rotated 90 degrees down and right.
336355
# So, we need the upper-left corner of the private-bg anchored at the upper-right corner of
337356
# private.
338357
private_length = private.y2 - private.y1 # line length is y-height
339-
private_fontsize = 8 # points == 1/72 inch
340-
private_height = private.x2 - private_qr.x2 # line height is
341-
private_fontwidth = private_length
358+
private_fontsize = 6.8 # points == 1/72 inch
359+
private_height = private.x2 - private_qr.x2 - .05
360+
private_lineheight = private_fontsize / 72 / Text.SIZE_RATIO * .9 # in.
342361

343-
public.add_region(
362+
private.add_region(
344363
Image(
345364
'private-bg',
346365
x1 = private.x2,
347366
y1 = private.y1,
348-
x2 = private.x2 + private_length + private_height,
367+
x2 = private.x2 + private_length,
349368
y2 = private.y1 + private_height,
350369
rotate = -90,
351370
)
352-
).add_region(
353-
Text(
354-
'private',
355-
size = private_fontsize,
356-
rotate = -90,
357-
)
358371
)
372+
# Now, add private key lines down the edge from right to left, rotating each into place
373+
for l in range( int( private_height // private_lineheight )):
374+
private.add_region(
375+
Text(
376+
f"private-{l}",
377+
font = 'mono',
378+
x1 = private.x2 - private_lineheight * l,
379+
y1 = private.y1,
380+
x2 = private.x2 + private_length,
381+
y2 = private.y1 + private_lineheight,
382+
size = private_fontsize,
383+
rotate = -90,
384+
)
385+
)
386+
359387
return wallet
360388

361389

@@ -511,7 +539,8 @@ def write_pdfs(
511539
json_pwd = None, # If JSON wallet output desired, supply password
512540
text = None, # Truthy outputs SLIP-39 recover phrases to stdout
513541
wallet_pwd = None, # If paper wallet images desired, supply password
514-
wallet_format = None, # ... and a format (eg. 'third')
542+
wallet_pwd_hint = None, # an optional password hint
543+
wallet_format = None, # and a paper wallet format (eg. 'third')
515544
):
516545
"""Writes a PDF containing a unique SLIP-39 encoded seed for each of the names specified.
517546
@@ -629,9 +658,9 @@ def write_pdfs(
629658
pdf.add_page( orientation='P' )
630659
page_n = p
631660
try:
632-
private_bip38 = account.bip38( 'password' )
661+
private_enc = account.encrypted( 'password' )
633662
except NotImplementedError as exc:
634-
log.exception( f"{account.crypto} doesn't support BIP-38: {exc}" )
663+
log.exception( f"{account.crypto} doesn't support BIP-38 or JSON wallet encryption: {exc}" )
635664
continue
636665

637666
wall_n += 1
@@ -653,9 +682,9 @@ def write_pdfs(
653682
wall_tpl['address-l'] = account.address
654683
wall_tpl['address-r-bg'] = os.path.join( images, '1x1-ffffff7f.png' )
655684
wall_tpl['address-r'] = account.address
656-
wall_tpl['address-qr-top'] = 'PUBLIC ADDRESS'
685+
wall_tpl['address-qr-t'] = 'PUBLIC ADDRESS'
657686
wall_tpl['address-qr'] = public_qr.make_image( back_color="transparent" ).get_image()
658-
wall_tpl['address-qr-bot'] = 'DEPOSIT/VERIFY'
687+
wall_tpl['address-qr-b'] = 'DEPOSIT/VERIFY'
659688

660689

661690
private_qr = qrcode.QRCode(
@@ -664,18 +693,24 @@ def write_pdfs(
664693
box_size = 10,
665694
border = 1,
666695
)
667-
private_qr.add_data( private_bip38 )
696+
private_qr.add_data( private_enc )
668697

669698
wall_tpl['private-bg'] = os.path.join( images, '1x1-ffffff7f.png' )
670699
def chunker( sequence, size ):
671700
while sequence:
672701
yield sequence[:size]
673702
sequence = sequence[size:]
674-
675-
wall_tpl['private'] = '\n '.join( chunker( private_bip38, 32 ))
676-
wall_tpl['private-qr-top'] = 'PRIVATE KEY'
703+
704+
# If not enough lines, will throw Exception, as it should! We don't want
705+
# to emit a Paper Wallet without the entire encrypted private key present.
706+
for l,line in enumerate( chunker( private_enc, 40 )):
707+
wall_tpl[f"private-{l}"]= line
708+
wall_tpl['private-hint-t'] = 'PASSPHRASE HINT:'
709+
wall_tpl['private-hint-bg'] = os.path.join( images, '1x1-ffffff7f.png' )
710+
wall_tpl['private-hint'] = wallet_pwd_hint
711+
wall_tpl['private-qr-t'] = 'PRIVATE KEY'
677712
wall_tpl['private-qr'] = private_qr.make_image( back_color="transparent" ).get_image()
678-
wall_tpl['private-qr-bot'] = 'SPEND'
713+
wall_tpl['private-qr-b'] = 'SPEND'
679714

680715
wall_tpl.render( offsetx=offsetx, offsety=offsety )
681716

slip39/main.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ def main( argv=None ):
4646
help="Save an encrypted JSON wallet for each Ethereum address w/ this password, '-' reads it from stdin (default: None)" )
4747
ap.add_argument( '-w', '--wallet',
4848
default=None,
49-
help="Product paper wallets in output PDF; each wallet private key is encrypted this password" )
49+
help="Produce paper wallets in output PDF; each wallet private key is encrypted this password" )
50+
ap.add_argument( '--wallet-hint',
51+
default=None,
52+
help="Paper wallets password hint" )
5053
ap.add_argument( '--wallet-format',
5154
default=None,
5255
help=f"Paper wallet size; {', '.join(WALLET_SIZES.keys())} or '(<h>,<w>),<margin>' (default: {WALLET})" )
@@ -117,6 +120,12 @@ def main( argv=None ):
117120
elif passphrase:
118121
log.warning( "It is recommended to not use '-p|--passphrase <password>'; specify '-' to read from input" )
119122

123+
wallet_pwd = args.wallet
124+
if wallet_pwd == '-':
125+
wallet_pwd = input_secure( 'Paper wallet passphrase: ', secret=True )
126+
wallet_pwd_hint = args.wallet_hint
127+
wallet_format = args.wallet_format
128+
120129
try:
121130
write_pdfs(
122131
names = args.names,
@@ -130,8 +139,9 @@ def main( argv=None ):
130139
filename = args.output,
131140
json_pwd = args.json,
132141
text = args.text,
133-
wallet_pwd = args.wallet,
134-
wallet_format = args.wallet_format,
142+
wallet_pwd = wallet_pwd,
143+
wallet_pwd_hint = wallet_pwd_hint,
144+
wallet_format = wallet_format,
135145
)
136146
except Exception as exc:
137147
log.exception( f"Failed to write PDFs: {exc}" )

0 commit comments

Comments
 (0)