CoinAmt: improve delegation of arithmetic ops, cleanups, add unit test

Testing:

    $ test/unit_tests.py -v obj.coinamt
This commit is contained in:
The MMGen Project 2021-09-01 16:56:47 +00:00
commit e719b5eb88
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
7 changed files with 145 additions and 34 deletions

View file

@ -96,7 +96,7 @@ class TokenBase(MMGenObject): # ERC20
def create_data(self,to_addr,amt,method_sig='transfer(address,uint256)',from_addr=None):
from_arg = from_addr.rjust(64,'0') if from_addr else ''
to_arg = to_addr.rjust(64,'0')
amt_arg = '{:064x}'.format(int(amt//self.base_unit))
amt_arg = '{:064x}'.format(int(amt / self.base_unit))
return create_method_id(method_sig) + from_arg + to_arg + amt_arg
def make_tx_in( self,from_addr,to_addr,amt,start_gas,gasPrice,nonce,

View file

@ -35,12 +35,8 @@ class ETHAmt(CoinAmt):
units = ('wei','Kwei','Mwei','Gwei','szabo','finney')
amt_fs = '4.18'
def toWei(self): return int(Decimal(self) // self.wei)
def toKwei(self): return int(Decimal(self) // self.Kwei)
def toMwei(self): return int(Decimal(self) // self.Mwei)
def toGwei(self): return int(Decimal(self) // self.Gwei)
def toSzabo(self): return int(Decimal(self) // self.szabo)
def toFinney(self): return int(Decimal(self) // self.finney)
def toWei(self):
return int(Decimal(self) // self.wei)
class ETHNonce(Int):
min_val = 0

View file

@ -261,7 +261,7 @@ class EthereumMMGenTX:
d = '{}... ({} bytes)'.format(td[:40],len(td)//2) if len(td) else Str('None'),
c = self.proto.dcoin if len(self.outputs) else '',
g = yellow(str(t['gasPrice'].to_unit('Gwei',show_decimal=True))),
G = yellow(str(t['startGas'].toKwei())),
G = yellow(str(t['startGas'].to_unit('Kwei'))),
t_mmid = m['outputs'] if len(self.outputs) else '',
f_mmid = m['inputs'] )

View file

@ -451,7 +451,15 @@ class SubSeedIdxRange(MMGenRange):
class UnknownCoinAmt(Decimal): pass
class DecimalNegateResult(Decimal): pass
class CoinAmt(Decimal,Hilite,InitErrors): # abstract class
"""
Instantiating with 'from_decimal' rounds value down to 'max_prec' precision.
For addition and subtraction, operand types must match.
For multiplication and division, operand types may differ.
Negative amounts, floor division and modulus operation are unimplemented.
"""
color = 'yellow'
forbidden_types = (float,int)
@ -483,9 +491,6 @@ class CoinAmt(Decimal,Hilite,InitErrors): # abstract class
except Exception as e:
return cls.init_fail(e,num)
def toSatoshi(self):
return int(Decimal(self) // self.satoshi)
def to_unit(self,unit,show_decimal=False):
ret = Decimal(self) // getattr(self,unit)
if show_decimal and ret < 1:
@ -524,21 +529,55 @@ class CoinAmt(Decimal,Hilite,InitErrors): # abstract class
def __repr__(self):
return "{}('{}')".format(type(self).__name__,self.__str__())
def __add__(self,other):
return type(self)(Decimal.__add__(self,other))
def __add__(self,other,*args,**kwargs):
"""
we must allow other to be int(0) to use the sum() builtin
"""
if other != 0 and type(other) not in ( type(self), DecimalNegateResult ):
raise ValueError(
f'operand {other} of incorrect type ({type(other).__name__} != {type(self).__name__})')
return type(self)(Decimal.__add__(self,other,*args,**kwargs))
__radd__ = __add__
def __sub__(self,other):
return type(self)(Decimal.__sub__(self,other))
def __sub__(self,other,*args,**kwargs):
if type(other) is not type(self):
raise ValueError(
f'operand {other} of incorrect type ({type(other).__name__} != {type(self).__name__})')
return type(self)(Decimal.__sub__(self,other,*args,**kwargs))
def __mul__(self,other):
return type(self)('{:0.8f}'.format(Decimal.__mul__(self,Decimal(other))))
def copy_negate(self,*args,**kwargs):
"""
We implement this so that __add__() can check type, because:
class Decimal:
def __sub__(self, other, ...):
...
return self.__add__(other.copy_negate(), ...)
"""
return DecimalNegateResult(Decimal.copy_negate(self,*args,**kwargs))
def __div__(self,other):
return type(self)('{:0.8f}'.format(Decimal.__div__(self,Decimal(other))))
def __mul__(self,other,*args,**kwargs):
return type(self)('{:0.{p}f}'.format(
Decimal.__mul__(self,Decimal(other,*args,**kwargs),*args,**kwargs),
p = self.max_prec
))
def __neg__(self,other):
return type(self)(Decimal.__neg__(self,other))
__rmul__ = __mul__
def __truediv__(self,other,*args,**kwargs):
return type(self)('{:0.{p}f}'.format(
Decimal.__truediv__(self,Decimal(other,*args,**kwargs),*args,**kwargs),
p = self.max_prec
))
def __neg__(self,*args,**kwargs):
self.method_not_implemented()
def __floordiv__(self,*args,**kwargs):
self.method_not_implemented()
def __mod__(self,*args,**kwargs):
self.method_not_implemented()
class BTCAmt(CoinAmt):
max_prec = 8

View file

@ -597,8 +597,9 @@ class TwAddrList(MMGenDict,metaclass=AsyncInit):
'amt': proto.coin_amt('0'),
'lbl': label,
'addr': CoinAddr(proto,d['address']) }
self[lm]['amt'] += d['amount']
self.total += d['amount']
amt = proto.coin_amt(d['amount'])
self[lm]['amt'] += amt
self.total += amt
# We use listaccounts only for empty addresses, as it shows false positive balances
if showempty or all_labels:
@ -1014,17 +1015,19 @@ class TwGetBalance(MMGenObject,metaclass=AsyncInit):
else:
lbl,key = None,'Non-wallet'
amt = self.proto.coin_amt(d['amount'])
if not d['confirmations']:
self.data['TOTAL'][0] += d['amount']
self.data[key][0] += d['amount']
self.data['TOTAL'][0] += amt
self.data[key][0] += amt
conf_level = (1,2)[d['confirmations'] >= self.minconf]
self.data['TOTAL'][conf_level] += d['amount']
self.data[key][conf_level] += d['amount']
self.data['TOTAL'][conf_level] += amt
self.data[key][conf_level] += amt
if d['spendable']:
self.data[key][3] += d['amount']
self.data[key][3] += amt
def format(self):
def gen_output():

View file

@ -283,7 +283,7 @@ class MMGenTxOutputList(MMGenTxIOList):
def sort_bip69(self):
def sort_func(a):
return (
int.to_bytes(a.amt.toSatoshi(),8,'big')
int.to_bytes(a.amt.to_unit('satoshi'),8,'big')
+ bytes.fromhex(addr2scriptPubKey(self.parent.proto,a.addr)) )
self.sort(key=sort_func)
@ -462,7 +462,7 @@ class MMGenTX:
# convert absolute BTC fee to satoshis-per-byte using estimated size
def fee_abs2rel(self,abs_fee,to_unit=None):
unit = getattr(self.proto.coin_amt,to_unit or 'satoshi')
return int(abs_fee // unit // self.estimate_size())
return int(abs_fee / unit / self.estimate_size())
def get_hex_locktime(self):
return int(bytes.fromhex(self.hex[-8:])[::-1].hex(),16)
@ -556,7 +556,7 @@ class MMGenTX:
@property
def relay_fee(self):
kb_fee = self.proto.coin_amt(self.rpc.cached['networkinfo']['relayfee'])
ret = kb_fee * self.estimate_size() // 1024
ret = kb_fee * self.estimate_size() / 1024
vmsg('Relay fee: {} {c}/kB, for transaction: {} {c}'.format(kb_fee,ret,c=self.coin))
return ret

73
test/unit_tests_d/ut_obj.py Executable file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""
test.unit_tests_d.ut_obj: data object unit tests for the MMGen suite
"""
from mmgen.common import *
class unit_tests:
def coinamt(self,name,ut):
from mmgen.obj import BTCAmt,LTCAmt,XMRAmt,ETHAmt
for cls,aa,bb in (
( BTCAmt, '1.2345', '11234567.897' ),
( LTCAmt, '1.2345', '44938271.588' ),
( XMRAmt, '1.2345', '11234567.98765432' ),
( ETHAmt, '1.2345', '11234567.98765432123456' ),
):
def do(desc,res,chk):
vmsg(f'{desc:10} = {res:<{cls.max_prec+10}} [{type(res).__name__}]')
if chk is not None:
assert res == chk, f'{res} != {chk}'
assert type(res) == cls, f'{type(res).__name__} != {cls.__name__}'
qmsg_r(f'Testing {cls.__name__} arithmetic operations...')
vmsg('')
A,B = ( Decimal(aa), Decimal(bb) )
a,b = ( cls(aa), cls(bb) )
do('A', A, None)
do('B', B, None)
do('a', a, A)
do('b', b, B)
do('b + a', b + a, B + A)
do('sum([b,a])', sum([b,a]), B + A)
do('b - a', b - a, B - A)
do('b * a', b * a, B * A)
do('b * A', b * A, B * A)
do('B * a', B * a, B * A)
do('b / a', b / a, cls( B / A, from_decimal=True ))
do('b / A', b / A, cls( B / A, from_decimal=True ))
do('a / b', a / b, cls( A / B, from_decimal=True ))
do('a * a / a', a * a / a, A * A / A)
do('a * b / a', a * b / a, A * B / A)
do('a * b / b', a * b / b, A * B / B)
qmsg('OK')
qmsg_r(f'Checking {cls.__name__} error handling...')
vmsg('')
bad_data = (
('negation', 'NotImplementedError', 'not implemented', lambda: -a ),
('modulus', 'NotImplementedError', 'not implemented', lambda: b % a ),
('floor division', 'NotImplementedError', 'not implemented', lambda: b // a ),
('negative result', 'ObjectInitError', 'cannot be negative', lambda: a - b ),
('operand type', 'ValueError', 'incorrect type', lambda: a + B ),
('operand type', 'ValueError', 'incorrect type', lambda: b - A ),
)
if cls.max_amt is not None:
bad_data += (
('result', 'ObjectInitError', 'too large', lambda: b + b ),
('result', 'ObjectInitError', 'too large', lambda: b * b ),
)
ut.process_bad_data(bad_data)
qmsg('OK')
return True