Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions slip39/layout/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def produce_pdf(
groups: Dict[str,Tuple[int,List[str]]], # SLIP-39 Groups {<name>: (<need>,[<mnemonic>,...])
accounts: Sequence[Sequence[Account]], # The crypto account(s); at least 1 of each required
using_bip39: bool, # Using BIP-39 wallets Seed generation
anonymous: Optional[bool] = None, # Avoid printing crypto addresses, QR codes
anonymous: bool = ANONYMOUS, # Avoid printing crypto addresses, QR codes
card_format: str = CARD, # 'index' or '(<h>,<w>),<margin>'
paper_format: Any = None, # 'Letter', (x,y) dimensions in mm.
orientations: Sequence[str] = None, # available orientations; default portrait, landscape
Expand Down Expand Up @@ -355,21 +355,28 @@ def produce_pdf(

_,f,b = page[-1][-1]

f['card-title'] = \
b['card-title'] = f"SLIP39 {g_name}({mn_n+1}/{len(g_mnems)}) for: {name}"
f['card-requires'] = requires
if not (ANONYMOUS if anonymous is None else anonymous):
f['card-crypto1']= f"{accounts[0][0].crypto} {accounts[0][0].path}: {accounts[0][0].address}"
# Anonymous cards show only the group name: not the name, the other groups, nor the
# cryptocurrency addresses and QR codes.
title = g_name
if not anonymous:
title = f"SLIP39 {g_name}"
if len( g_mnems ) > 1:
title = f"{title} {mn_n+1}"
if name:
title = f"{title} for {name}"
f['card-requires'] = requires
f['card-crypto1'] = f"{accounts[0][0].crypto} {accounts[0][0].path}: {accounts[0][0].address}"
f['card-qr1'] = io.BytesIO( qr_acct[0].to_string(encoding='unicode').encode('UTF-8')) # get_image()
if not (ANONYMOUS if anonymous is None else anonymous) and len( accounts[0] ) > 1:
f['card-crypto2'] = f"{accounts[0][1].crypto} {accounts[0][1].path}: {accounts[0][1].address}"
f['card-qr2'] = io.BytesIO( qr_acct[1].to_string(encoding='unicode').encode('UTF-8')) # get_image()
f[f'card-g{g_n}'] = \
b[f'card-g{g_n}'] = f"{g_name:7.7}..{mn_n+1}" if len(g_name) > 8 else f"{g_name} {mn_n+1}"
if len( accounts[0] ) > 1:
f['card-crypto2'] = f"{accounts[0][1].crypto} {accounts[0][1].path}: {accounts[0][1].address}"
f['card-qr2'] = io.BytesIO( qr_acct[1].to_string(encoding='unicode').encode('UTF-8')) # get_image()
f[f'card-g{g_n}'] = \
b[f'card-g{g_n}'] = f"{g_name:7.7}..{mn_n+1}" if len(g_name) > 8 else f"{g_name} {mn_n+1}"
f['card-title'] = b['card-title'] = title
if watermark:
f['card-watermark'] = watermark
for n,m in enumerate_mnemonic( mnem ).items():
mn_idx = f"mnem-{n}-big" if (ANONYMOUS if anonymous is None else anonymous) else f"mnem-{n}"
mn_idx = f"mnem-{n}-big" if anonymous else f"mnem-{n}"
f[mn_idx] = m

# Back contains QR code of the card's SLIP-39 Mnemonic
Expand Down Expand Up @@ -412,7 +419,7 @@ def write_pdfs(
group_threshold = None, # int, or 1/2 of groups by default
cryptocurrency = None, # sequence of [ 'ETH:<path>', ... ] to produce accounts for
edit = None, # Adjust crypto paths according to the provided path edit
anonymous = None, # Produce cryptocurrency addresses and QR codes
anonymous = ANONYMOUS, # Produce cryptocurrency addresses and QR codes
card_format = None, # Eg. "credit"; False outputs no SLIP-39 Mnemonic cards to PDF
paper_format = None, # Eg. "Letter", "Legal", (x,y)
filename = None, # A file name format w/ {path}, etc. formatters
Expand Down
59 changes: 58 additions & 1 deletion slip39/output_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import pytest
import qrcode

from pytest import approx
from fpdf import FPDF, FlexTemplate

from .layout import Region, Text, Image, Box, Coordinate
from .layout import Region, Text, Image, Box, Coordinate, write_pdfs
from .defaults import MM_IN


Expand Down Expand Up @@ -73,3 +74,59 @@ def test_Region():
tpl.render( offsetx = card_size.x * MM_IN, offsety = card_size.y * MM_IN )

#pdf.output( "test.pdf" ) # To view results in test.pdf, uncomment


@pytest.fixture
def rendered_cards( monkeypatch ):
"""Records the texts of every template rendered into a PDF."""
rendered = []
render = FlexTemplate.render

def record( self, *args, **kwds ):
rendered.append( dict( self.texts ))
return render( self, *args, **kwds )
monkeypatch.setattr( FlexTemplate, 'render', record )
return rendered


def write_cards( tmp_path, **kwds ):
write_pdfs(
names = [ "Alice" ],
master_secret = bytes.fromhex( "ff" * 16 ),
cryptocurrency = [ "BTC" ],
cover_page = False,
filepath = str( tmp_path ),
filename = "cards",
**kwds
)


def test_cards( tmp_path, rendered_cards ):
"""Cards carry the name, the requirements of all groups, the address, and the group label."""
write_cards( tmp_path )
cards = [ t for t in rendered_cards if 'card-title' in t ]
fronts = [ t for t in cards if 'card-requires' in t ] # the backs only have a title (and QR code)
assert fronts
assert { t['card-title'] for t in cards } == {
"SLIP39 First for Alice", "SLIP39 Second for Alice",
"SLIP39 Fam 1 for Alice", "SLIP39 Fam 2 for Alice", "SLIP39 Fam 3 for Alice", "SLIP39 Fam 4 for Alice",
*( f"SLIP39 Frens {n} for Alice" for n in range( 1, 7 )),
}
for t in fronts:
assert "Recover w/ 2 of 4" in t['card-requires']
assert 'card-crypto1' in t and 'card-qr1' in t
assert any( k.startswith( 'card-g' ) for k in t )


def test_anonymous_cards( tmp_path, rendered_cards ):
"""Anonymous cards (front and back) show just the group name, and the mnemonic."""
write_cards( tmp_path, anonymous=True )
cards = [ t for t in rendered_cards if 'card-title' in t ]
assert cards
assert { t['card-title'] for t in cards } == { "First", "Second", "Fam", "Frens" }
for t in cards:
assert not any( k.startswith(( 'card-g', 'card-requires', 'card-crypto', 'card-qr1', 'card-qr2' )) for k in t ), \
f"Anonymous card shows: {sorted( t )}"
assert "Alice" not in "".join( str( v ) for v in t.values() )
# The mnemonic words are the (large) ones
assert any( k.endswith( '-big' ) for t in cards for k in t if k.startswith( 'mnem-' ))
Loading