CoinAmt: improve delegation of arithmetic ops, cleanups, add unit test
Testing:
$ test/unit_tests.py -v obj.coinamt
This commit is contained in:
parent
af0bfc342c
commit
e719b5eb88
7 changed files with 145 additions and 34 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'] )
|
||||
|
||||
|
|
|
|||
65
mmgen/obj.py
65
mmgen/obj.py
|
|
@ -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
|
||||
|
|
|
|||
17
mmgen/tw.py
17
mmgen/tw.py
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
73
test/unit_tests_d/ut_obj.py
Executable 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue