Skip to content

Commit 11e2298

Browse files
committed
Work toward more sane decimals handling for eg. BTC; not there, yet...
o WBTC has decimals = 8, resulting in 8 // 3 == 2 decimals precision
1 parent 4ccf623 commit 11e2298

File tree

4 files changed

+121
-60
lines changed

4 files changed

+121
-60
lines changed

slip39/api.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,20 @@ class Account:
288288
| XRP | Legacy | m/44'/144'/0'/0/0 | r... | Beta |
289289
290290
"""
291-
CRYPTO_NAMES = dict( # Currently supported (in order of visibility)
291+
CRYPTO_SYMBOLS = dict(
292+
# Convert known Symbols to official Cryptocurrency Name. By convention, Symbols are
293+
# capitalized to avoid collisions with names
294+
ETH = 'Ethereum',
295+
BTC = 'Bitcoin',
296+
LTC = 'Litecoin',
297+
DOGE = 'Dogecoin',
298+
CRO = 'Cronos',
299+
BNB = 'Binance',
300+
XRP = 'Ripple',
301+
)
302+
CRYPTO_NAMES = dict(
303+
# Currently supported (in order of visibility), and conversion of known Names to Symbol. By
304+
# convention, Cryptocurrency names are lower-cased to avoid collisions with symbols.
292305
ethereum = 'ETH',
293306
bitcoin = 'BTC',
294307
litecoin = 'LTC',
@@ -388,13 +401,13 @@ def address_format( cls, crypto, format=None ):
388401

389402
@classmethod
390403
def supported( cls, crypto ):
391-
"""Validates that the specified cryptocurrency is supported and returns the normalized "symbol"
392-
for it, or raises an a ValueError. Eg. "Ethereum" --> "ETH"
404+
"""Validates that the specified cryptocurrency is supported and returns the normalized "SYMBOL"
405+
for it, or raises an a ValueError. Eg. "ETH"/"Ethereum" --> "ETH"
393406
394407
"""
395408
validated = cls.CRYPTO_NAMES.get(
396409
crypto.lower(),
397-
crypto.upper() if crypto.upper() in cls.CRYPTOCURRENCIES else None
410+
crypto.upper() if crypto.upper() in cls.CRYPTO_SYMBOLS else None
398411
)
399412
if validated:
400413
return validated

slip39/defaults.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@
178178
# current prices for cryptocurrencies -- if you have access to an Ethereum blockchain (either
179179
# locally or via an HTTPS API like Alchemy), then we can securely and reliably get current prices.
180180
# To avoid conflicts, by convention we upper-case symbols, lower-case full names.
181-
INVOICE_ROWS = 50
181+
INVOICE_ROWS = 40
182182
INVOICE_CURRENCY = "USD"
183183
INVOICE_PROXIES = {
184184
"USD": "USDC",

slip39/invoice/artifact.py

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -207,24 +207,33 @@ def cryptocurrency_symbol( name, chain=None, w3_url=None, use_provider=None ):
207207

208208

209209
def cryptocurrency_proxy( name, decimals=None, chain=None, w3_url=None, use_provider=None ):
210-
"""Return the named ERC-20 Token (or a known "Proxy" token, eg. BTC -> WBTC). Otherwise, if it is
211-
a recognized core supported Cryptocurrency, return a TokenInfo useful for formatting.
210+
"""Return the named ERC-20 Token (or a known "Proxy" token, eg. BTC -> WBTC).
212211
213212
"""
214213
try:
215214
return tokeninfo( name, w3_url=w3_url, use_provider=use_provider )
216215
except Exception as exc:
217216
log.info( f"Could not identify currency {name!r} as an ERC-20 Token: {exc}" )
218217
# Not a known Token; a core known Cryptocurrency?
218+
return cryptocurrency_known( name, decimals=decimals )
219+
220+
221+
def cryptocurrency_known( name, decimals=None ):
222+
"""If it is a recognized core supported Cryptocurrency, return a TokenInfo useful for formatting.
223+
Since Bitcoin (and other similar cryptocurrencies) are typically assumed to have 8 decimal
224+
places (the Sat(oshi) is 1/10^8 of a Bitcoin), we'll make the default decimals//3 precision work
225+
out to 8).
226+
227+
"""
219228
try:
220229
symbol = Account.supported( name )
221230
except ValueError as exc:
222231
log.info( f"Failed to identify currency {name!r} as a supported Cryptocurrency: {exc}" )
223232
raise
224233
return TokenInfo(
225-
name = name,
234+
name = Account.CRYPTO_SYMBOLS[symbol],
226235
symbol = symbol,
227-
decimals = 18 if decimals is None else decimals,
236+
decimals = 24 if decimals is None else decimals,
228237
icon = next( ( Path( __file__ ).resolve().parent / "Cryptos" ).glob( symbol + '*.*' ), None ),
229238
)
230239

@@ -253,7 +262,8 @@ class Invoice:
253262
Can emit the line items in groups with summary sub-totals and a final total.
254263
255264
Reframes the price of each line in terms of the Invoice's currencies (default: USD), providing
256-
"Total <SYMBOL>" and "Taxes <SYMBOL>" for each Cryptocurrency symbols.
265+
"Total <SYMBOL>" and "Taxes <SYMBOL>" for each Cryptocurrency symbols. NOTE: These are a
266+
*running* Total; the LineItem Amount is each line's total, in terms of that line's currency.
257267
258268
Now that we have Web3 details, we can query tokeninfos and hence format currency decimals
259269
correctly.
@@ -381,7 +391,7 @@ def __init__(
381391
log.warning( f"Ignoring {c}: {exc}" )
382392
continue
383393
if conversions.get( (c,two.symbol) ) is None or conversions.get( (one.symbol,two.symbol) ) is None:
384-
log.info( f"Updating {c:>6}/{two.symbol:<6} = {conversions.get( (c,two.symbol) )}"
394+
log.info( f"Updated {c:>6}/{two.symbol:<6} = {conversions.get( (c,two.symbol) )}"
385395
f" and {one.symbol:>6}/{two.symbol:<6} = {conversions.get( (one.symbol,two.symbol) )},"
386396
f" to: {ratio}" )
387397
conversions[c,two.symbol] = ratio
@@ -563,8 +573,10 @@ def can( c ):
563573

564574
log.info( f"Tabulating indices {commas(selected, final='and')}: {commas( headers_selected, final='and' )}" )
565575
pages = list( self.pages( *args, **kwds ))
566-
# Overall formatted decimals for entire invoice; use the greatest desired decimals of any
567-
# token/line. Individual line items may have been rounded to lower decimals.
576+
577+
# Overall formatted decimals for the entire invoice; use the greatest desired decimals of
578+
# any token/line, based on Cryptocurrency proxies. Individual line items may have been
579+
# rounded to lower decimals.
568580
decimals_i = headers_can.index( can( '_Decimals' ))
569581
decimals = max( line[decimals_i] for page in pages for line in page )
570582
floatfmt = f',.{decimals}f'
@@ -582,8 +594,15 @@ def can( c ):
582594
first = page_prev is None
583595
final = p + 1 == len( pages )
584596

597+
# {Sub}total for payment cryptocurrrencies are rounded to the individual
598+
# Cryptocurrency's designated decimals // 3. For typical ERC-20 tokens, this is
599+
# eg. USDC: 6 // 3 == 2, WBTC: 18 // 3 == 6. For known Cryptocurrencies, eg. BTC: 24 //
600+
# 3 == 8. TODO: There should be a more sensible / less brittle way to do this.
585601
def deci( c ):
586-
return self.currencies_proxy[c].decimals // 3
602+
try:
603+
return cryptocurrency_known( c ).decimals // 3
604+
except Exception:
605+
return self.currencies_proxy[c].decimals // 3
587606

588607
def toti( c ):
589608
return headers_can.index( can( f'_Total {c}' ))
@@ -599,20 +618,20 @@ def taxi( c ):
599618
c,
600619
self.currencies_account[c].name if c == self.currencies_account[c].symbol else self.currencies_proxy[c].name,
601620
] + [
602-
round( page[-1][toti( c )], deci( c )),
603621
round( page[-1][taxi( c )], deci( c )),
622+
round( page[-1][toti( c )], deci( c )),
604623
] if first else [
605-
round( page[-1][toti( c )] - page_prev[-1][toti( c )], deci( c )),
606624
round( page[-1][taxi( c )] - page_prev[-1][taxi( c )], deci( c )),
625+
round( page[-1][toti( c )] - page_prev[-1][toti( c )], deci( c )),
607626
]
608627
for c in sorted( self.currencies )
609628
],
610629
headers = (
611630
'Account',
612631
'Crypto',
613632
'Currency',
633+
'Taxes' if final else f'Taxes {p+1}/{len( pages )}',
614634
'Subtotal' if final else f'Subtotal {p+1}/{len( pages )}',
615-
'Taxes' if final else f'Taxes {p+1}/{len( pages )}'
616635
),
617636
intfmt = ',',
618637
floatfmt = ',g',
@@ -625,17 +644,17 @@ def taxi( c ):
625644
self.currencies_account[c].address,
626645
c,
627646
self.currencies_account[c].name if c == self.currencies_account[c].symbol else self.currencies_proxy[c].name,
628-
round( page[-1][toti( c )], deci( c )),
629647
round( page[-1][taxi( c )], deci( c )),
648+
round( page[-1][toti( c )], deci( c )),
630649
]
631650
for c in sorted( self.currencies )
632651
],
633652
headers = (
634653
'Account',
635654
'Crypto',
636655
'Currency',
656+
'Taxes' if final else f'Taxes {p+1}/{len( pages )}',
637657
'Total' if final else f'Total {p+1}/{len( pages )}',
638-
'Taxes' if final else f'Taxes {p+1}/{len( pages )}'
639658
),
640659
intfmt = ',',
641660
floatfmt = ',g',
@@ -680,7 +699,7 @@ def layout_invoice(
680699
681700
"""
682701
prio_backing = -3
683-
prio_normal = -2 # noqa: F841
702+
prio_normal = -2
684703
prio_contrast = -1
685704

686705
if inv_margin is None:
@@ -690,7 +709,7 @@ def layout_invoice(
690709
inv_interior = inv.add_region_relative(
691710
Region( 'inv-interior', x1=+inv_margin, y1=+inv_margin, x2=-inv_margin, y2=-inv_margin )
692711
).add_region_proportional(
693-
# inv-background -- A full background image, out to the margins
712+
# inv-image -- A full background image, out to the margins; bottom-most in stack
694713
Image(
695714
'inv-image',
696715
priority = prio_backing,
@@ -703,16 +722,20 @@ def layout_invoice(
703722
β = math.atan( b / a ) # noqa: F841
704723
rotate = 90 - math.degrees( β ) # noqa: F841
705724

706-
inv_top = inv_interior.add_region_proportional(
707-
Region( 'inv-top', x1=0, y1=0, x2=1, y2=1/6 )
708-
).add_region_proportional(
725+
head = 25/100 # 25% header, 70% body
726+
foot = 95/100 # 5% footer
727+
728+
# inv-head: Image for header of Invoice (if no inv-image)
729+
inv_head = inv_interior.add_region_proportional(
709730
Image(
710-
'inv-top-bg',
711-
priority = prio_backing,
731+
'inv-head',
732+
y2 = head,
733+
priority = prio_normal,
712734
),
713735
)
714736

715-
inv_top.add_region_proportional(
737+
# inv-label: Label "Invoice"
738+
inv_head.add_region_proportional(
716739
Image(
717740
'inv-label-bg',
718741
x1 = 0,
@@ -728,20 +751,40 @@ def layout_invoice(
728751
text = "Invoice",
729752
)
730753
)
731-
inv_body = inv_interior.add_region_proportional(
732-
Region( 'inv-body', x1=0, y1=1/6, x2=1, y2=1 )
733-
)
734754

735-
inv_body.add_region_proportional(
755+
# inv-body, ...: Image for Body of Invoice;
756+
# inv-table: Main Invoice text area
757+
inv_interior.add_region_proportional(
758+
Image(
759+
'inv-body',
760+
y1 = head,
761+
y2 = foot,
762+
priority = prio_normal,
763+
)
764+
).add_region_proportional(
765+
Image(
766+
'inv-table-bg',
767+
priority = prio_contrast,
768+
),
769+
).add_region_proportional(
736770
Text(
737771
'inv-table',
738772
y2 = 1/rows,
739773
font = 'mono',
740-
bold = True,
741774
size_ratio = 9/16,
742775
multiline = True,
743776
)
744777
)
778+
779+
# inv-foot: Image for bottom of Invoice (if no inv-image)
780+
inv_interior.add_region_proportional(
781+
Image(
782+
'inv-foot',
783+
y1 = foot,
784+
priority = prio_normal,
785+
),
786+
)
787+
745788
return inv
746789

747790

@@ -905,7 +948,8 @@ def produce_invoice(
905948
last = i + 1 == len( details )
906949
inv_tpl['inv-image'] = inv_image
907950
inv_tpl['inv-table'] = f"{tbl}\n\n{80 * ( '=' if last else '-' )}\n{tot if last else sub}"
908-
inv_tpl['inv-top-bg'] = layout / '1x1-ffffff54.png'
951+
inv_tpl['inv-label-bg'] = layout / '1x1-ffffffbf.png'
952+
inv_tpl['inv-table-bg'] = layout / '1x1-ffffffbf.png'
909953

910954
inv_tpl.render( offsetx=offsetx, offsety=offsety )
911955
# Caller already has the Invoice; return the PDF and computed InvoiceMetadata

slip39/invoice/artifact_test.py

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_tabulate( tmp_path ):
158158
account( SEED_ZOOS, crypto='Bitcoin' ),
159159
]
160160
conversions = {
161-
("XRP","BTC"): 60000,
161+
("BTC","XRP"): 60000,
162162
}
163163

164164
total = Invoice(
@@ -205,17 +205,17 @@ def test_tabulate( tmp_path ):
205205
|---------------+---------+---------+---------+---------+--------+----------+------------|
206206
| Worthless | 1 | 12,346 | no tax | 0 | 12,346 | 12,346 | ZEENUS |
207207
208-
| Account | Crypto | Currency | Subtotal | Taxes |
209-
|--------------------------------------------+----------+---------------+------------+---------|
210-
| bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | Bitcoin | 0 | 0 |
211-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 0 | 0 |
212-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | HOT | HoloToken | 0 | 0 |
213-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 0 | 0 |
214-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | Wrapped BTC | 0 | 0 |
215-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 0 | 0 |
216-
| rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | Ripple | 0 | 0 |
217-
218-
| Account | Crypto | Currency | Total | Taxes |
208+
| Account | Crypto | Currency | Taxes | Subtotal |
209+
|--------------------------------------------+----------+---------------+---------+------------|
210+
| bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | Bitcoin | 0 | 0 |
211+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 0 | 0 |
212+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | HOT | HoloToken | 0 | 0 |
213+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 0 | 0 |
214+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | Wrapped BTC | 0 | 0 |
215+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 0 | 0 |
216+
| rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | Ripple | 0 | 0 |
217+
218+
| Account | Crypto | Currency | Taxes | Total |
219219
|--------------------------------------------+----------+---------------+---------+---------|
220220
| bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | Bitcoin | 0 | 0 |
221221
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 0 | 0 |
@@ -233,9 +233,7 @@ def test_tabulate( tmp_path ):
233233
for line,_ in line_amounts
234234
if line.currency in ("ZEENUS", None) or line.currency.upper().startswith( 'US' )
235235
],
236-
accounts = [
237-
account( SEED_ZOOS, crypto='Ethereum' ),
238-
],
236+
accounts = accounts,
239237
conversions = dict( conversions ) | {
240238
(eth,usd): 1500 for eth in ("ETH","WETH") for usd in ("USDC",)
241239
} | {
@@ -257,17 +255,23 @@ def test_tabulate( tmp_path ):
257255
| 1 | Worthless | 1 | ZEENUS | ZEENUS | 12,345.68 | no tax | 0.00 | 12,346.00 | 417.88 |
258256
| 2 | Simple | 1 | USD | USDC | 12,345.68 | no tax | 0.00 | 12,345.68 | 12,763.56 |
259257
260-
| Account | Crypto | Currency | Subtotal | Taxes |
261-
|--------------------------------------------+----------+---------------+--------------+-----------|
262-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 8.50904 | 0.013267 |
263-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 12,763.6 | 19.9 |
264-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 8.50904 | 0.013267 |
265-
266-
| Account | Crypto | Currency | Total | Taxes |
267-
|--------------------------------------------+----------+---------------+--------------+-----------|
268-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 8.50904 | 0.013267 |
269-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 12,763.6 | 19.9 |
270-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 8.50904 | 0.013267 |""" # noqa: E501
258+
| Account | Crypto | Currency | Taxes | Subtotal |
259+
|--------------------------------------------+----------+---------------+-------------+---------------|
260+
| bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | Bitcoin | 0.00088444 | 0.567269 |
261+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 0.0132667 | 8.50904 |
262+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 19.9 | 12,763.6 |
263+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | Wrapped BTC | 0 | 0.57 |
264+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 0.013267 | 8.50904 |
265+
| rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | Ripple | 53.0667 | 34,036.2 |
266+
267+
| Account | Crypto | Currency | Taxes | Total |
268+
|--------------------------------------------+----------+---------------+-------------+---------------|
269+
| bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | Bitcoin | 0.00088444 | 0.567269 |
270+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 0.0132667 | 8.50904 |
271+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 19.9 | 12,763.6 |
272+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | Wrapped BTC | 0 | 0.57 |
273+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 0.013267 | 8.50904 |
274+
| rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | Ripple | 53.0667 | 34,036.2 |""" # noqa: E501
271275

272276
this = Path( __file__ ).resolve()
273277
test = this.with_suffix( '' )
@@ -298,7 +302,7 @@ def test_tabulate( tmp_path ):
298302
"""
299303
),
300304
directory = test,
301-
inv_image = 'dominion-invoice.png',
305+
inv_image = 'dominionrnd-invoice.png', # Full page background image
302306
)
303307

304308
print( f"Invoice metadata: {metadata}" )

0 commit comments

Comments
 (0)