Skip to content

Commit 103e096

Browse files
committed
More correct decimals in Sub/Total tabulations
1 parent e4f3315 commit 103e096

File tree

2 files changed

+117
-60
lines changed

2 files changed

+117
-60
lines changed

slip39/invoice/artifact.py

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@
2323
from collections import namedtuple
2424
from typing import Dict, Union, Optional, Sequence
2525
from fractions import Fraction
26+
from pathlib import Path
2627

2728
from tabulate import tabulate
2829

2930
from ..api import Account
3031
from ..util import commas, is_listlike
3132
from ..defaults import INVOICE_CURRENCY
3233
from ..layout import Region, Text, Image, Box, Coordinate
33-
from .ethereum import tokeninfo, tokenprices # , tokenratio
34+
from .ethereum import tokeninfo, tokenprices, TokenInfo # , tokenratio
3435

3536
"""
3637
Invoice artifacts:
@@ -190,7 +191,7 @@ def cryptocurrency_symbol( name, chain=None, w3_url=None, use_provider=None ):
190191
try:
191192
return Account.supported( name )
192193
except ValueError as exc:
193-
log.info( f"Failed to identify currency {name!r} as an supported Cryptocurrency: {exc}" )
194+
log.info( f"Could not identify currency {name!r} as a supported Cryptocurrency: {exc}" )
194195
# Not a known core Cryptocurrency; a Token?
195196
try:
196197
return tokeninfo( name, w3_url=w3_url, use_provider=use_provider ).symbol
@@ -199,6 +200,29 @@ def cryptocurrency_symbol( name, chain=None, w3_url=None, use_provider=None ):
199200
raise
200201

201202

203+
def cryptocurrency_proxy( name, decimals=None, chain=None, w3_url=None, use_provider=None ):
204+
"""Return the named ERC-20 Token (or a known "Proxy" token, eg. BTC -> WBTC). Otherwise, if it is
205+
a recognized core supported Cryptocurrency, return a TokenInfo useful for formatting.
206+
207+
"""
208+
try:
209+
return tokeninfo( name, w3_url=w3_url, use_provider=use_provider )
210+
except Exception as exc:
211+
log.info( f"Could not identify currency {name!r} as an ERC-20 Token: {exc}" )
212+
# Not a known Token; a core known Cryptocurrency?
213+
try:
214+
symbol = Account.supported( name )
215+
except ValueError as exc:
216+
log.info( f"Failed to identify currency {name!r} as a supported Cryptocurrency: {exc}" )
217+
raise
218+
return TokenInfo(
219+
name = name,
220+
symbol = symbol,
221+
decimals = 18 if decimals is None else decimals,
222+
icon = next( ( Path( __file__ ).resolve().parent / "Cryptos" ).glob( symbol + '*.*' ), None ),
223+
)
224+
225+
202226
class Invoice:
203227
"""The totals for some invoice line items, in terms of some currencies, payable into some
204228
Cryptocurrency accounts.
@@ -300,19 +324,36 @@ def __init__(
300324

301325
# Find all LineItem.currency -> Invoice.currencies conversions required. This establishes
302326
# the baseline conversions ratio requirements to convert LineItems to each Invoice currency.
303-
# No prices are yet found.
327+
# No prices are yet found. After this, currencies_proxy will contain all Invoice and LineItem
328+
# Crypto proxies
304329
if conversions is None:
305330
conversions = {}
306-
line_symbols = set(
307-
cryptocurrency_symbol( line.currency or INVOICE_CURRENCY )
331+
line_currencies = set(
332+
line.currency or INVOICE_CURRENCY
308333
for line in self.lines
309334
)
310-
log.info( f"Found {len( line_symbols )} Line-Item currencies: {commas( line_symbols, final='and')}" )
311-
for c in currencies:
312-
for ls in line_symbols:
335+
line_symbols = set(
336+
cryptocurrency_symbol( lc )
337+
for lc in line_currencies
338+
)
339+
log.info( f"Found {len( line_symbols )} LineItem currencies: {commas( line_symbols, final='and')}" )
340+
for ls in line_symbols:
341+
for c in currencies:
313342
if ls != c:
314343
conversions.setdefault( (ls,c), None ) # eg. ('USDC','BTC'): None
315344

345+
# Finally, add all remaining Invoice and LineItem cryptocurrencies to currencies_proxy, by
346+
# their original names, symbols and names. Now, every Invoice and LineItem currency (by
347+
# name and alias) is represented in currencies_proxy.
348+
for lc in line_currencies | currencies:
349+
proxy = cryptocurrency_proxy( lc, w3_url=w3_url, use_provider=use_provider )
350+
for alias in set( (lc, proxy.name, proxy.symbol) ):
351+
if alias in currencies_proxy:
352+
assert currencies_proxy[alias] == proxy, \
353+
f"Incompatible LineItem.currency {lc!r} w/ currency {alias!r}: \n{proxy} != {currencies_proxy[alias]}"
354+
currencies_proxy[alias] = proxy
355+
log.info( f"Found {len( currencies_proxy )} Invoice and LineItem currencies: {commas( ( f'{n}: {p.symbol}' for n,p in currencies_proxy.items() ), final='and')}" )
356+
316357
# Resolve all resolvable conversions (ie. from any supplied), see what's left
317358
while ( remaining := conversions_remaining( conversions ) ) and not isinstance( remaining, str ):
318359
print( f"Working: \n{conversions_table( conversions )}" )
@@ -519,37 +560,48 @@ def tables(
519560
tablefmt = tablefmt or 'orgtbl',
520561
)
521562
first = page_prev is None
563+
564+
def deci( c ):
565+
return self.currencies_proxy[c].decimals // 3
566+
567+
def toti( c ):
568+
return headers.index( f'Total {c}' )
569+
570+
def taxi( c ):
571+
return headers.index( f'Taxes {c}' )
572+
522573
subtotal = tabulate(
523574
# And the per-currency Sub-totals (for each page)
524575
[
525-
[self.currencies_account[c], c]
526-
+ [
527-
page[-1][headers.index( f"Total {c}" )],
528-
page[-1][headers.index( f"Taxes {c}" )],
576+
[
577+
self.currencies_account[c], c, self.currencies_proxy[c].name,
578+
] + [
579+
round( page[-1][toti( c )], deci( c )),
580+
round( page[-1][taxi( c )], deci( c )),
529581
] if first else [
530-
page[-1][headers.index( f"Total {c}" )] - page_prev[-1][headers.index( f"Total {c}" )],
531-
page[-1][headers.index( f"Taxes {c}" )] - page_prev[-1][headers.index( f"Taxes {c}" )],
582+
round( page[-1][toti( c )] - page_prev[-1][toti( c )], deci( c )),
583+
round( page[-1][taxi( c )] - page_prev[-1][taxi( c )], deci( c )),
532584
]
533585
for c in sorted( self.currencies )
534586
],
535-
headers = ( "Account", "Cryptocurrency", f"Subtotal {p}", f"Subtotal {p} Taxes" ),
536-
intfmt = intfmt,
537-
floatfmt = floatfmt,
587+
headers = ( "Account", "Crypto", "Currency", f"Subtotal {p+1}/{len( pages )}", f"Subtotal {p+1}/{len( pages )} Taxes" ),
588+
intfmt = ",",
589+
floatfmt = ",g",
538590
tablefmt = tablefmt or 'orgtbl',
539591
)
540592
total = tabulate(
541593
# And the per-currency Totals (up to current page)
542594
[
543-
[self.currencies_account[c], c]
544-
+ [
545-
page[-1][headers.index( f"Total {c}" )],
546-
page[-1][headers.index( f"Taxes {c}" )],
595+
[
596+
self.currencies_account[c], c, self.currencies_proxy[c].name,
597+
round( page[-1][toti( c )], deci( c )),
598+
round( page[-1][taxi( c )], deci( c )),
547599
]
548600
for c in sorted( self.currencies )
549601
],
550-
headers = ( "Account", "Cryptocurrency", f"Total {p}", f"Total {p} Taxes" ),
551-
intfmt = intfmt,
552-
floatfmt = floatfmt,
602+
headers = ( "Account", "Crypto", "Currency", f"Total {p+1}/{len( pages )}", f"Total {p+1}/{len( pages )} Taxes" ),
603+
intfmt = ",",
604+
floatfmt = ",g",
553605
tablefmt = tablefmt or 'orgtbl',
554606
)
555607

slip39/invoice/artifact_test.py

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def test_tabulate():
155155
account( SEED_ZOOS, crypto='Bitcoin' ),
156156
]
157157
conversions = {
158-
("XRP","BTC"): 0.00001834,
158+
("XRP","BTC"): 60000,
159159
}
160160

161161
total = Invoice(
@@ -165,7 +165,7 @@ def test_tabulate():
165165
],
166166
accounts = accounts,
167167
currencies = [ "USD", "HOT" ],
168-
conversions = conversions,
168+
conversions = dict( conversions ),
169169
)
170170

171171
print( json.dumps( list( total.pages() ), indent=4, default=str ))
@@ -182,7 +182,7 @@ def test_tabulate():
182182
for t in tables:
183183
print( t )
184184

185-
# Get the default formatting for line's w/ currencies w/ 0 decimals, 0 value
185+
# Get the default formatting for line's w/ currencies w/ 0 decimals, 0 value.
186186
worthless = '\n\n====\n\n'.join(
187187
f"{table}\n\n{sub}\n\n{tot}"
188188
for _,table,sub,tot in Invoice(
@@ -193,7 +193,7 @@ def test_tabulate():
193193
],
194194
currencies = ["HOT", "ETH", "BTC", "USD"],
195195
accounts = accounts,
196-
conversions = conversions,
196+
conversions = dict( conversions ),
197197
).tables()
198198
)
199199
print( worthless )
@@ -202,27 +202,28 @@ def test_tabulate():
202202
|--------+---------------+---------+---------+---------+---------+--------+----------+------------+----------+-------------+-------------+-------------+--------------+--------------+--------------+-------------+-------------+-------------+-------------+--------------+--------------+--------------+-------------|
203203
| 0 | Worthless | 1 | 12,346 | no tax | 0 | 12,346 | 12,346 | ZEENUS | ZEENUS | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
204204
205-
| Account | Cryptocurrency | Subtotal 0 | Subtotal 0 Taxes |
206-
|-------------------------------------------------+------------------+--------------+--------------------|
207-
| BTC: bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | 0 | 0 |
208-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | 0 | 0 |
209-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | HOT | 0 | 0 |
210-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | 0 | 0 |
211-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | 0 | 0 |
212-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | 0 | 0 |
213-
| XRP: rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | 0 | 0 |
205+
| Account | Crypto | Currency | Subtotal 1/1 | Subtotal 1/1 Taxes |
206+
|-------------------------------------------------+----------+---------------+----------------+----------------------|
207+
| BTC: bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | Wrapped BTC | 0 | 0 |
208+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Wrapped Ether | 0 | 0 |
209+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | HOT | HoloToken | 0 | 0 |
210+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 0 | 0 |
211+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | Wrapped BTC | 0 | 0 |
212+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 0 | 0 |
213+
| XRP: rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | XRP | 0 | 0 |
214214
215-
| Account | Cryptocurrency | Total 0 | Total 0 Taxes |
216-
|-------------------------------------------------+------------------+-----------+-----------------|
217-
| BTC: bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | 0 | 0 |
218-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | 0 | 0 |
219-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | HOT | 0 | 0 |
220-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | 0 | 0 |
221-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | 0 | 0 |
222-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | 0 | 0 |
223-
| XRP: rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | 0 | 0 |""" # noqa: E501
215+
| Account | Crypto | Currency | Total 1/1 | Total 1/1 Taxes |
216+
|-------------------------------------------------+----------+---------------+-------------+-------------------|
217+
| BTC: bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | Wrapped BTC | 0 | 0 |
218+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Wrapped Ether | 0 | 0 |
219+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | HOT | HoloToken | 0 | 0 |
220+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 0 | 0 |
221+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | Wrapped BTC | 0 | 0 |
222+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 0 | 0 |
223+
| XRP: rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | XRP | 0 | 0 |""" # noqa: E501
224224

225225
# No conversions of non-0 values; default Invoice currency is USD. Longest digits should be 2
226+
# Instead of querying BTC, ETH prices, provide a conversion (so our invoice pricing is static)
226227
shorter = '\n\n====\n\n'.join(
227228
f"{table}\n\n{sub}\n\n{tot}"
228229
for _,table,sub,tot in Invoice(
@@ -234,25 +235,29 @@ def test_tabulate():
234235
accounts = [
235236
account( SEED_ZOOS, crypto='Ethereum' ),
236237
],
237-
conversions = conversions,
238+
conversions = dict( conversions ) | {
239+
(eth,usd): 1500 for eth in ("ETH","WETH") for usd in ("USDC",)
240+
} | {
241+
(btc,eth): 15 for eth in ("ETH","WETH") for btc in ("BTC","WBTC")
242+
},
238243
).tables()
239244
)
240245
print( shorter )
241246
assert shorter == """\
242247
| Line | Description | Units | Price | Tax % | Taxes | Net | Amount | Currency | Symbol | Total ETH | Total USDC | Total WETH | Taxes ETH | Taxes USDC | Taxes WETH |
243248
|--------+-----------------------+---------+-----------+----------+---------+-----------+-----------+------------+----------+-------------+--------------+--------------+-------------+--------------+--------------|
244-
| 0 | Widgets for the Thing | 198 | 2.01 | 5% added | 19.90 | 397.98 | 417.88 | US Dollar | USDC | 0.26 | 417.88 | 0.26 | 0.01 | 19.90 | 0.01 |
245-
| 1 | Worthless | 1 | 12,345.68 | no tax | 0.00 | 12,346.00 | 12,346.00 | ZEENUS | ZEENUS | 0.26 | 417.88 | 0.26 | 0.01 | 19.90 | 0.01 |
246-
| 2 | Simple | 1 | 12,345.68 | no tax | 0.00 | 12,345.68 | 12,345.68 | USD | USDC | 8.01 | 12,763.56 | 8.01 | 0.01 | 19.90 | 0.01 |
249+
| 0 | Widgets for the Thing | 198 | 2.01 | 5% added | 19.90 | 397.98 | 417.88 | US Dollar | USDC | 0.28 | 417.88 | 0.28 | 0.01 | 19.90 | 0.01 |
250+
| 1 | Worthless | 1 | 12,345.68 | no tax | 0.00 | 12,346.00 | 12,346.00 | ZEENUS | ZEENUS | 0.28 | 417.88 | 0.28 | 0.01 | 19.90 | 0.01 |
251+
| 2 | Simple | 1 | 12,345.68 | no tax | 0.00 | 12,345.68 | 12,345.68 | USD | USDC | 8.51 | 12,763.56 | 8.51 | 0.01 | 19.90 | 0.01 |
247252
248-
| Account | Cryptocurrency | Subtotal 0 | Subtotal 0 Taxes |
249-
|-------------------------------------------------+------------------+--------------+--------------------|
250-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | 8.01 | 0.01 |
251-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | 12,763.56 | 19.90 |
252-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | 8.01 | 0.01 |
253+
| Account | Crypto | Currency | Subtotal 1/1 | Subtotal 1/1 Taxes |
254+
|-------------------------------------------------+----------+---------------+----------------+----------------------|
255+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Wrapped Ether | 8.50904 | 0.013267 |
256+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 12,763.6 | 19.9 |
257+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 8.50904 | 0.013267 |
253258
254-
| Account | Cryptocurrency | Total 0 | Total 0 Taxes |
255-
|-------------------------------------------------+------------------+-----------+-----------------|
256-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | 8.01 | 0.01 |
257-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | 12,763.56 | 19.90 |
258-
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | 8.01 | 0.01 |""" # noqa: E501
259+
| Account | Crypto | Currency | Total 1/1 | Total 1/1 Taxes |
260+
|-------------------------------------------------+----------+---------------+--------------+-------------------|
261+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Wrapped Ether | 8.50904 | 0.013267 |
262+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 12,763.6 | 19.9 |
263+
| ETH: 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 8.50904 | 0.013267 |""" # noqa: E501

0 commit comments

Comments
 (0)