From e719b5eb886034df0e42648be9afa67bec250dd5 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Wed, 1 Sep 2021 16:56:47 +0000 Subject: [PATCH] CoinAmt: improve delegation of arithmetic ops, cleanups, add unit test Testing: $ test/unit_tests.py -v obj.coinamt --- mmgen/altcoins/eth/contract.py | 2 +- mmgen/altcoins/eth/obj.py | 8 +--- mmgen/altcoins/eth/tx.py | 2 +- mmgen/obj.py | 65 ++++++++++++++++++++++++------ mmgen/tw.py | 17 ++++---- mmgen/tx.py | 6 +-- test/unit_tests_d/ut_obj.py | 73 ++++++++++++++++++++++++++++++++++ 7 files changed, 142 insertions(+), 31 deletions(-) create mode 100755 test/unit_tests_d/ut_obj.py diff --git a/mmgen/altcoins/eth/contract.py b/mmgen/altcoins/eth/contract.py index d542b6ac..19cdde14 100755 --- a/mmgen/altcoins/eth/contract.py +++ b/mmgen/altcoins/eth/contract.py @@ -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, diff --git a/mmgen/altcoins/eth/obj.py b/mmgen/altcoins/eth/obj.py index ac260782..46898d11 100755 --- a/mmgen/altcoins/eth/obj.py +++ b/mmgen/altcoins/eth/obj.py @@ -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 diff --git a/mmgen/altcoins/eth/tx.py b/mmgen/altcoins/eth/tx.py index 87474783..be7032cb 100755 --- a/mmgen/altcoins/eth/tx.py +++ b/mmgen/altcoins/eth/tx.py @@ -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'] ) diff --git a/mmgen/obj.py b/mmgen/obj.py index 8a49b2a6..09af9ef2 100755 --- a/mmgen/obj.py +++ b/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 diff --git a/mmgen/tw.py b/mmgen/tw.py index d28a7186..43132640 100755 --- a/mmgen/tw.py +++ b/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(): diff --git a/mmgen/tx.py b/mmgen/tx.py index 3f0808c5..33cf0649 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -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 diff --git a/test/unit_tests_d/ut_obj.py b/test/unit_tests_d/ut_obj.py new file mode 100755 index 00000000..5f0673e9 --- /dev/null +++ b/test/unit_tests_d/ut_obj.py @@ -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