Skip to content

Commit 2fc8ede

Browse files
committed
Correct proxied/aliased currencies to use native Cryptocurrency's decimals
1 parent 11e2298 commit 2fc8ede

File tree

5 files changed

+90
-39
lines changed

5 files changed

+90
-39
lines changed

slip39/api.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,23 @@ class Account:
299299
BNB = 'Binance',
300300
XRP = 'Ripple',
301301
)
302+
CRYPTO_DECIMALS = dict(
303+
# Ethereum-related Cryptocurrencies are denominated 10^18, typically default 6 decimals
304+
# precision. Bitcoin-related cryptocurrencies are typically 8 decimals precision (1 Sat is
305+
# 1/10^8 Bitcoin). For XRP 1 Drop = 1/10^6 Ripple: https://xrpl.org/currency-formats.html
306+
# For formatting, we'll typically default to decimals//3, which works out fairly well for
307+
# most Cryptocurrencies and ERC-20 Tokens. The exception are eg. WBTC (8 decimals) vs. BTC
308+
# (24 decimals); using the default 8 // 3 = 2 for WBTC would be dramatically too few
309+
# decimals of precision for practical use. So, we recommend defaulting to the underlying
310+
# known cryptocurrency, when a proxy Token is used for price calculations.
311+
ETH = 18,
312+
BTC = 24,
313+
LTC = 24,
314+
DOGE = 24,
315+
CRO = 18,
316+
BNB = 18,
317+
XRP = 6,
318+
)
302319
CRYPTO_NAMES = dict(
303320
# Currently supported (in order of visibility), and conversion of known Names to Symbol. By
304321
# convention, Cryptocurrency names are lower-cased to avoid collisions with symbols.

slip39/invoice/artifact.py

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from ..util import commas, is_listlike, is_mapping
3838
from ..defaults import INVOICE_CURRENCY, INVOICE_ROWS, INVOICE_STRFTIME, INVOICE_DUE, MM_IN, FILENAME_FORMAT
3939
from ..layout import Region, Text, Image, Box, Coordinate, layout_pdf
40-
from .ethereum import tokeninfo, tokenprices, TokenInfo # , tokenratio
40+
from .ethereum import tokeninfo, tokenprices, tokenknown
4141

4242
"""
4343
Invoice artifacts:
@@ -215,27 +215,7 @@ def cryptocurrency_proxy( name, decimals=None, chain=None, w3_url=None, use_prov
215215
except Exception as exc:
216216
log.info( f"Could not identify currency {name!r} as an ERC-20 Token: {exc}" )
217217
# 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-
"""
228-
try:
229-
symbol = Account.supported( name )
230-
except ValueError as exc:
231-
log.info( f"Failed to identify currency {name!r} as a supported Cryptocurrency: {exc}" )
232-
raise
233-
return TokenInfo(
234-
name = Account.CRYPTO_SYMBOLS[symbol],
235-
symbol = symbol,
236-
decimals = 24 if decimals is None else decimals,
237-
icon = next( ( Path( __file__ ).resolve().parent / "Cryptos" ).glob( symbol + '*.*' ), None ),
238-
)
218+
return tokenknown( name, decimals=decimals )
239219

240220

241221
class Invoice:
@@ -323,19 +303,31 @@ def __init__(
323303
# and an Ethereum account provided, the buyer can pay in BTC to the Bitcoin account, or in
324304
# ETH or WBTC to the Ethereum account. This will add the symbols for all Crypto proxies to
325305
# currencies_account.
326-
currencies_proxy = {}
306+
currencies_alias = {} # eg. { "WBTC": TokenInfo( "BTC", ... ), ... }
307+
currencies_proxy = {} # eg. { "BTC": TokenInfo( "WBTC", ... ), ... }
327308
for c in currencies:
328309
try:
329-
currencies_proxy[c] = tokeninfo( c, w3_url=w3_url, use_provider=use_provider )
310+
currencies_proxy[c] = alias = tokeninfo( c, w3_url=w3_url, use_provider=use_provider )
330311
except Exception as exc:
331-
log.info( f"Failed to find proxy for Invoice currency {c}: {exc}" )
312+
# May fail later, if no conversions provided for this currency
313+
log.warning( f"Failed to find proxy for Invoice currency {c}: {exc}" )
332314
else:
333-
# Yup; a proxy for a Crypto Eg. BTC -> WBTC was found, or a native ERC-20 eg. USDC
315+
# Yup; a proxy for a Crypto eg. BTC -> WBTC was found, or a native ERC-20 eg. USDC
334316
# was found; associate it with any ETH account provided.
335317
if eth := currencies_account.get( 'ETH' ):
336318
currencies_account[currencies_proxy[c].symbol] = eth
319+
try:
320+
known = tokenknown( c )
321+
except Exception:
322+
pass
323+
else:
324+
# Aliases; of course, the native symbol is included in known aliases
325+
currencies_alias[alias.symbol] = known
326+
currencies_alias[known.symbol] = known
327+
log.info( f"Currency {c}'s Proxy Symbol {alias.symbol} is an alias for {known.symbol}" )
337328
log.info( f"Found {len( currencies_proxy )} Invoice currency proxies: {commas( ( f'{c}: {p.symbol}' for c,p in currencies_proxy.items() ), final='and')}" )
338329
log.info( f"Added {len( set( currencies_account ) - currencies )} proxies: {commas( set( currencies_account ) - currencies, final='and' )}" )
330+
log.info( f"Alias {len( currencies_alias )} symbols to their native Crytocurrencies: {commas( ( f'{a}: {c.symbol}' for a,c in currencies_alias.items() ), final='and')}" )
339331
currencies = set( currencies_account )
340332

341333
# Find all LineItem.currency -> Invoice.currencies conversions required. This establishes
@@ -406,7 +398,8 @@ def __init__(
406398

407399
self.currencies = currencies # { "USDC", "BTC", ... }
408400
self.currencies_account = currencies_account # { "USDC": "0xaBc...12D", "BTC": "bc1...", ... }
409-
self.currencies_proxy = currencies_proxy # { "BTC": TokenInfo( ... ), ... }
401+
self.currencies_proxy = currencies_proxy # { "BTC": TokenInfo( "WBTC", ... ), ... }
402+
self.currencies_alias = currencies_alias # { "WBTC": TokenInfo( "BTC", ... ), ... }
410403
self.conversions = conversions # { ("BTC","ETH"): 14.3914, ... }
411404

412405
def headers( self ):
@@ -597,12 +590,17 @@ def can( c ):
597590
# {Sub}total for payment cryptocurrrencies are rounded to the individual
598591
# Cryptocurrency's designated decimals // 3. For typical ERC-20 tokens, this is
599592
# 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.
593+
# 3 == 8.
594+
#
595+
# If a symbol is a "proxy" token for some upstream known cryptocurrency, then the
596+
# upstream native cryptocurrency's decimals should be used, so that all lines match. We
597+
# keep track of each cryptocurrency_alias[<proxy-symbol>] -> <original-symbol>
598+
#
599+
# TODO: There should be a more sensible / less brittle way to do this.
601600
def deci( c ):
602-
try:
603-
return cryptocurrency_known( c ).decimals // 3
604-
except Exception:
605-
return self.currencies_proxy[c].decimals // 3
601+
if c in self.currencies_alias:
602+
return self.currencies_alias[c].decimals // 3
603+
return self.currencies_proxy[c].decimals // 3
606604

607605
def toti( c ):
608606
return headers_can.index( can( f'_Total {c}' ))

slip39/invoice/artifact_test.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -258,20 +258,20 @@ def test_tabulate( tmp_path ):
258258
| Account | Crypto | Currency | Taxes | Subtotal |
259259
|--------------------------------------------+----------+---------------+-------------+---------------|
260260
| bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | Bitcoin | 0.00088444 | 0.567269 |
261-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 0.0132667 | 8.50904 |
261+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 0.013267 | 8.50904 |
262262
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 19.9 | 12,763.6 |
263-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | Wrapped BTC | 0 | 0.57 |
263+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | Wrapped BTC | 0.00088444 | 0.567269 |
264264
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 0.013267 | 8.50904 |
265-
| rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | Ripple | 53.0667 | 34,036.2 |
265+
| rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | Ripple | 53.07 | 34,036.2 |
266266
267267
| Account | Crypto | Currency | Taxes | Total |
268268
|--------------------------------------------+----------+---------------+-------------+---------------|
269269
| bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2 | BTC | Bitcoin | 0.00088444 | 0.567269 |
270-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 0.0132667 | 8.50904 |
270+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | ETH | Ethereum | 0.013267 | 8.50904 |
271271
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | USDC | USD Coin | 19.9 | 12,763.6 |
272-
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | Wrapped BTC | 0 | 0.57 |
272+
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WBTC | Wrapped BTC | 0.00088444 | 0.567269 |
273273
| 0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E | WETH | Wrapped Ether | 0.013267 | 8.50904 |
274-
| rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | Ripple | 53.0667 | 34,036.2 |""" # noqa: E501
274+
| rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV | XRP | Ripple | 53.07 | 34,036.2 |""" # noqa: E501
275275

276276
this = Path( __file__ ).resolve()
277277
test = this.with_suffix( '' )

slip39/invoice/ethereum.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from web3 import Web3
4141
from web3.middleware import construct_sign_and_send_raw_middleware
4242

43+
from ..api import Account
4344
from ..util import memoize, retry, commas, into_bytes, timer
4445
from ..defaults import (
4546
ETHERSCAN_MEMO_MAXAGE, ETHERSCAN_MEMO_MAXSIZE,
@@ -1256,8 +1257,8 @@ def w3_provider( w3_url, use_provider ):
12561257
@dataclass( eq=True, frozen=True ) # Makes it hashable
12571258
class TokenInfo:
12581259
"""Represents a Crypto-currency or on-chain Token (eg. ERC-20)"""
1259-
name: str
12601260
symbol: str
1261+
name: str
12611262
decimals: int
12621263
contract: Optional[str] = None # If not an ERC-20 token contract, no contract address
12631264
icon: Optional[Union[str,Path]] = None
@@ -1368,6 +1369,27 @@ def aliasinfo( info ):
13681369
tokeninfo.ERC20s = {} # noqa: E305; { <chain>: {'contract': <info>, 'name': <info>, 'symbol': info, ... }}
13691370

13701371

1372+
def tokenknown( name, decimals=None ):
1373+
"""If name is a recognized core supported Cryptocurrency, return a TokenInfo useful for formatting.
1374+
1375+
Since Bitcoin (and other similar cryptocurrencies) are typically assumed to have 8 decimal
1376+
places (a Sat(oshi) is 1/10^8 of a Bitcoin), we'll make the default decimals//3 precision work
1377+
out to 8).
1378+
1379+
"""
1380+
try:
1381+
symbol = Account.supported( name )
1382+
except ValueError as exc:
1383+
log.info( f"Failed to identify currency {name!r} as a supported Cryptocurrency: {exc}" )
1384+
raise
1385+
return TokenInfo(
1386+
symbol = symbol,
1387+
name = Account.CRYPTO_SYMBOLS[symbol],
1388+
decimals = Account.CRYPTO_DECIMALS[symbol] if decimals is None else decimals,
1389+
icon = next( ( Path( __file__ ).resolve().parent / "Cryptos" ).glob( symbol + '*.*' ), None ),
1390+
)
1391+
1392+
13711393
@memoize( maxage=TOKPRICES_MEMO_MAXAGE, maxsize=TOKPRICES_MEMO_MAXSIZE, log_at=logging.INFO )
13721394
def tokenprice( w3_url, chain, token, base, use_wrappers=None, use_provider=None ):
13731395
"""Return memoized token address prices, in terms of a base token address. The resultant Fraction

slip39/util.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,20 @@ def log_level( adjust ):
9797
]
9898

9999

100+
def log_apis( func ):
101+
"""Decorator for logging function args, kwds, and results"""
102+
@wraps( func )
103+
def wrapper( *args, **kwds ):
104+
try:
105+
result = func(*args, **kwds)
106+
except Exception as exc:
107+
log.warning( f"{func.__name__}( {args!r} {kwds!r} ): {exc}" )
108+
else:
109+
log.info( f"{func.__name__}( {args!r} {kwds!r} ) == {result}" )
110+
return result
111+
return wrapper
112+
113+
100114
#
101115
# util.is_... -- Test for various object capabilities
102116
#

0 commit comments

Comments
 (0)