Browse Source

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

Testing:

    $ test/unit_tests.py -v obj.coinamt
The MMGen Project 3 years ago
parent
commit
e719b5eb88
7 changed files with 145 additions and 34 deletions
  1. 1 1
      mmgen/altcoins/eth/contract.py
  2. 2 6
      mmgen/altcoins/eth/obj.py
  3. 1 1
      mmgen/altcoins/eth/tx.py
  4. 55 16
      mmgen/obj.py
  5. 10 7
      mmgen/tw.py
  6. 3 3
      mmgen/tx.py
  7. 73 0
      test/unit_tests_d/ut_obj.py

+ 1 - 1
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,

+ 2 - 6
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

+ 1 - 1
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'] )
 

+ 55 - 16
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))
-	__radd__ = __add__
-
-	def __sub__(self,other):
-		return type(self)(Decimal.__sub__(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))
 
-	def __mul__(self,other):
-		return type(self)('{:0.8f}'.format(Decimal.__mul__(self,Decimal(other))))
-
-	def __div__(self,other):
-		return type(self)('{:0.8f}'.format(Decimal.__div__(self,Decimal(other))))
+	__radd__ = __add__
 
-	def __neg__(self,other):
-		return type(self)(Decimal.__neg__(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 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 __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
+		))
+
+	__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

+ 10 - 7
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():

+ 3 - 3
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
 

+ 73 - 0
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