2323from collections import namedtuple
2424from typing import Dict , Union , Optional , Sequence
2525from fractions import Fraction
26+ from pathlib import Path
2627
2728from tabulate import tabulate
2829
2930from ..api import Account
3031from ..util import commas , is_listlike
3132from ..defaults import INVOICE_CURRENCY
3233from ..layout import Region , Text , Image , Box , Coordinate
33- from .ethereum import tokeninfo , tokenprices # , tokenratio
34+ from .ethereum import tokeninfo , tokenprices , TokenInfo # , tokenratio
3435
3536"""
3637Invoice 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+
202226class 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
0 commit comments