From 50fc415282bfcb3a405be71bdfce00afec8ce136 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Fri, 18 Oct 2024 10:32:05 +0000 Subject: [PATCH] CoinAmt: improvements - do strict type checking in initializer, forbid double initialization - add dynamic decimal precision based on protocol - dunder method fixes, cleanups - JSON-RPC library now returns floats (i.e. amounts) as strings instead of Decimal, eliminating an extra conversion step --- examples/halving-calculator.py | 2 +- mmgen/amt.py | 60 ++++---- mmgen/obj.py | 6 +- mmgen/proto/btc/regtest.py | 2 +- mmgen/proto/btc/tw/addresses.py | 5 +- mmgen/proto/btc/tw/rpc.py | 4 +- mmgen/proto/btc/tw/txhistory.py | 5 +- mmgen/proto/btc/tx/base.py | 6 +- mmgen/proto/btc/tx/info.py | 2 +- mmgen/proto/btc/tx/new.py | 7 +- mmgen/proto/etc/params.py | 1 + mmgen/proto/eth/contract.py | 6 +- mmgen/proto/eth/params.py | 1 + mmgen/proto/eth/tw/unspent.py | 2 +- mmgen/proto/eth/tx/bump.py | 2 +- mmgen/proto/eth/tx/new.py | 13 +- mmgen/protocol.py | 3 + mmgen/rpc.py | 9 +- mmgen/tw/addresses.py | 13 +- mmgen/tw/unspent.py | 16 +-- mmgen/tx/base.py | 10 +- mmgen/tx/new.py | 3 + mmgen/xmrwallet.py | 12 +- test/include/common.py | 2 +- .../fakemods/mmgen/proto/btc/tw/unspent.py | 2 +- test/unit_tests_d/ut_obj.py | 131 +++++++++++++++++- test/unit_tests_d/ut_tx_deserialize.py | 4 +- 27 files changed, 220 insertions(+), 109 deletions(-) diff --git a/examples/halving-calculator.py b/examples/halving-calculator.py index 91791ba5..24218010 100755 --- a/examples/halving-calculator.py +++ b/examples/halving-calculator.py @@ -74,7 +74,7 @@ async def main(): bdr = (cur['time'] - old['time']) / sample_size t_rem = remaining * int(bdr) - sub = cur['subsidy'] * proto.coin_amt.satoshi + sub = proto.coin_amt(cur['subsidy'], from_unit='satoshi' if isinstance(cur['subsidy'], int) else None) print( f'Current block: {tip}\n' diff --git a/mmgen/amt.py b/mmgen/amt.py index 14cc35fc..6643bed0 100755 --- a/mmgen/amt.py +++ b/mmgen/amt.py @@ -23,19 +23,17 @@ amt: MMGen CoinAmt and related classes from decimal import Decimal from .objmethods import Hilite, InitErrors -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. + + Decimal precision is set in init_proto() """ coin = 'Coin' color = 'yellow' - forbidden_types = (float,int) max_prec = 0 # number of decimal places for this coin max_amt = None # coin supply if known, otherwise None @@ -43,8 +41,8 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class def __new__(cls, num, from_unit=None, from_decimal=False): - if isinstance(num, cls): - return num + if isinstance(num, CoinAmt): + raise TypeError(f'CoinAmt: {num} is instance of {cls.__name__}') try: if from_unit: @@ -55,9 +53,8 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class assert isinstance(num, Decimal), f'number must be of type Decimal, not {type(num).__name__})' me = Decimal.__new__(cls, num.quantize(Decimal('10') ** -cls.max_prec)) else: - for bad_type in cls.forbidden_types: - assert not isinstance(num, bad_type), f'number is of forbidden type {bad_type.__name__}' - me = Decimal.__new__(cls, str(num)) + assert isinstance(num, str), f'non-string passed to {cls.__name__} initializer' + me = Decimal.__new__(cls, num) assert me.normalize().as_tuple()[-1] >= -cls.max_prec, 'too many decimal places in coin amount' if cls.max_amt: assert me <= cls.max_amt, f'{me}: coin amount too large (>{cls.max_amt})' @@ -74,11 +71,8 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class cls.method_not_implemented() def fmt(self, color=False, iwidth=1, prec=None): # iwidth: width of the integer part - - s = str(self) prec = prec or self.max_prec - - if '.' in s: + if '.' in (s := str(self)): a, b = s.split('.', 1) return self.colorize( a.rjust(iwidth) + '.' + b.ljust(prec)[:prec], # truncation, not rounding! @@ -113,28 +107,24 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class """ we must allow other to be int(0) to use the sum() builtin """ - if type(other) not in ( type(self), DecimalNegateResult ) and other != 0: - raise ValueError( - f'operand {other} of incorrect type ({type(other).__name__} != {type(self).__name__})') - return type(self)(Decimal.__add__(self, other, *args, **kwargs)) + if type(other) is type(self) or (other == 0 and isinstance(other, int)): + return type(self)(Decimal.__add__(self, other, *args, **kwargs), from_decimal=True) + raise TypeError( + f'operand {other} is of incorrect type ({type(other).__name__} != {type(self).__name__})') __radd__ = __add__ 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)) + if type(other) is type(self): + return type(self)(Decimal.__sub__(self, other, *args, **kwargs), from_decimal=True) + raise TypeError( + f'operand {other} is of incorrect type ({type(other).__name__} != {type(self).__name__})') - 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 __rsub__(self, other, *args, **kwargs): + if type(other) is type(self): + return type(self)(Decimal.__rsub__(self, other, *args, **kwargs), from_decimal=True) + raise TypeError( + f'operand {other} is of incorrect type ({type(other).__name__} != {type(self).__name__})') def __mul__(self, other, *args, **kwargs): return type(self)('{:0.{p}f}'.format( @@ -150,6 +140,12 @@ class CoinAmt(Decimal, Hilite, InitErrors): # abstract class p = self.max_prec )) + def __rtruediv__(self, other, *args, **kwargs): + return type(self)('{:0.{p}f}'.format( + Decimal.__rtruediv__(self, Decimal(other), *args, **kwargs), + p = self.max_prec + )) + def __neg__(self, *args, **kwargs): self.method_not_implemented() @@ -196,3 +192,7 @@ class ETHAmt(CoinAmt): class ETCAmt(ETHAmt): coin = 'ETC' + +def CoinAmtChk(proto, num): + assert type(num) is proto.coin_amt, f'CoinAmtChk: {type(num)} != {proto.coin_amt}' + return num diff --git a/mmgen/obj.py b/mmgen/obj.py index 2bd131e8..8aeb7f63 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -112,10 +112,7 @@ class ImmutableAttr: # Descriptor if set_none_ok: assert typeconv and not isinstance(dtype,str), 'ImmutableAttr_check3' - if dtype is None: - # use instance-defined conversion function for this attribute - self.conv = lambda instance,value: getattr(instance.conv_funcs,self.name)(instance,value) - elif typeconv: + if typeconv: # convert this attribute's type if set_none_ok: self.conv = lambda instance,value: None if value is None else dtype(value) @@ -190,7 +187,6 @@ class MMGenListItem(MMGenObject): 'valid_attrs', 'invalid_attrs', 'immutable_attr_init_check', - 'conv_funcs', } def __init__(self,*args,**kwargs): diff --git a/mmgen/proto/btc/regtest.py b/mmgen/proto/btc/regtest.py index 47c9dc2b..f50b0d99 100755 --- a/mmgen/proto/btc/regtest.py +++ b/mmgen/proto/btc/regtest.py @@ -231,7 +231,7 @@ class MMGenRegtest(MMGenObject): users = ('bob','alice') for user in users: out = await self.rpc_call('listunspent',0,wallet=user) - bal[user] = sum(e['amount'] for e in out) + bal[user] = sum(self.proto.coin_amt(e['amount']) for e in out) fs = '{:<16} {:18.8f}' for user in users: diff --git a/mmgen/proto/btc/tw/addresses.py b/mmgen/proto/btc/tw/addresses.py index 1f90cb3d..740dc242 100755 --- a/mmgen/proto/btc/tw/addresses.py +++ b/mmgen/proto/btc/tw/addresses.py @@ -51,7 +51,8 @@ class BitcoinTwAddresses(TwAddresses,BitcoinTwRPC): addrs = await self.get_unspent_by_mmid(self.minconf) msg('done') - amt0 = self.proto.coin_amt('0') + coin_amt = self.proto.coin_amt + amt0 = coin_amt('0') self.total = sum((v['amt'] for v in addrs.values()), start=amt0) msg_r('Getting labels and associated addresses...') @@ -71,7 +72,7 @@ class BitcoinTwAddresses(TwAddresses,BitcoinTwRPC): label = get_obj( TwLabel, proto=self.proto, text=d['label'] ) if label: assert label.mmid in addrs, f'{label.mmid!r} not found in addrlist!' - addrs[label.mmid]['recvd'] = d['amount'] + addrs[label.mmid]['recvd'] = coin_amt(d['amount']) addrs[label.mmid]['confs'] = d['confirmations'] msg('done') diff --git a/mmgen/proto/btc/tw/rpc.py b/mmgen/proto/btc/tw/rpc.py index 4ccca953..2e9e4c5e 100755 --- a/mmgen/proto/btc/tw/rpc.py +++ b/mmgen/proto/btc/tw/rpc.py @@ -102,7 +102,7 @@ class BitcoinTwRPC(TwRPC): 'amt': amt0, 'lbl': label, 'addr': CoinAddr(self.proto,d['address']) } - amt = self.proto.coin_amt(d['amount']) - data[lm]['amt'] += amt + + data[lm]['amt'] += self.proto.coin_amt(d['amount']) return data diff --git a/mmgen/proto/btc/tw/txhistory.py b/mmgen/proto/btc/tw/txhistory.py index 4a476aa6..f4fdf218 100755 --- a/mmgen/proto/btc/tw/txhistory.py +++ b/mmgen/proto/btc/tw/txhistory.py @@ -86,7 +86,7 @@ class BitcoinTwTransaction: yield e.coin_addr def total(data): - return self.proto.coin_amt( sum(d.data['value'] for d in data) ) + return sum(coin_amt(d.data['value']) for d in data) def get_best_comment(): """ @@ -97,6 +97,7 @@ class BitcoinTwTransaction: ret = vouts_labels('outputs') or vouts_labels('inputs') return ret[0] if ret else TwComment('') + coin_amt = self.proto.coin_amt # 'outputs' refers to wallet-related outputs only self.vouts_info = { 'inputs': gen_vouts_info( gen_prevouts_data() ), @@ -107,7 +108,7 @@ class BitcoinTwTransaction: 'outputs': max(len(addr) for addr in gen_all_addrs('outputs')) } self.inputs_total = total(self.vouts_info['inputs']) - self.outputs_total = self.proto.coin_amt(sum(i['value'] for i in self.tx['decoded']['vout'])) + self.outputs_total = sum(coin_amt(i['value']) for i in self.tx['decoded']['vout']) self.wallet_outputs_total = total(self.vouts_info['outputs']) self.fee = self.inputs_total - self.outputs_total self.nOutputs = len(self.tx['decoded']['vout']) diff --git a/mmgen/proto/btc/tx/base.py b/mmgen/proto/btc/tx/base.py index 283511f9..d3a788ba 100755 --- a/mmgen/proto/btc/tx/base.py +++ b/mmgen/proto/btc/tx/base.py @@ -51,7 +51,7 @@ def DeserializeTX(proto, txhex): return int(bytes_le[::-1].hex(), 16) def bytes2coin_amt(bytes_le): - return proto.coin_amt(bytes2int(bytes_le) * proto.coin_amt.satoshi) + return proto.coin_amt(bytes2int(bytes_le), from_unit='satoshi') def bshift(n, skip=False, sub_null=False): nonlocal idx, raw_tx @@ -114,7 +114,7 @@ def DeserializeTX(proto, txhex): d['num_txouts'] = readVInt() d['txouts'] = MMGenList([{ - 'amount': bytes2coin_amt(bshift(8)), + 'amt': bytes2coin_amt(bshift(8)), 'scriptPubKey': bshift(readVInt()).hex() } for i in range(d['num_txouts'])]) @@ -317,7 +317,7 @@ class Base(TxBase.Base): check_equal( 'outputs', - sorted((o['address'], self.proto.coin_amt(o['amount'])) for o in dtx.txouts), + sorted((o['address'], o['amt']) for o in dtx.txouts), sorted((o.addr, o.amt) for o in self.outputs)) if str(self.txid) != make_chksum_6(bytes.fromhex(dtx.unsigned_hex)).upper(): diff --git a/mmgen/proto/btc/tx/info.py b/mmgen/proto/btc/tx/info.py index ffd58215..e97d0571 100755 --- a/mmgen/proto/btc/tx/info.py +++ b/mmgen/proto/btc/tx/info.py @@ -37,7 +37,7 @@ class TxInfo(TxInfo): ) def format_abs_fee(self,color,iwidth): - return self.tx.proto.coin_amt(self.tx.fee).fmt(color=color,iwidth=iwidth) + return self.tx.fee.fmt(color=color, iwidth=iwidth) def format_verbose_footer(self): tx = self.tx diff --git a/mmgen/proto/btc/tx/new.py b/mmgen/proto/btc/tx/new.py index cf185595..295fd052 100755 --- a/mmgen/proto/btc/tx/new.py +++ b/mmgen/proto/btc/tx/new.py @@ -66,8 +66,7 @@ class New(Base,TxBase.New): # given tx size, rel fee and units, return absolute fee def fee_rel2abs(self, tx_size, units, amt_in_units, unit): if tx_size: - return self.proto.coin_amt( - amt_in_units * tx_size * getattr(self.proto.coin_amt, units[unit])) + return self.proto.coin_amt(amt_in_units * tx_size, from_unit=units[unit]) else: return None @@ -110,7 +109,7 @@ class New(Base,TxBase.New): msg(self.no_chg_msg) self.outputs.pop(self.chg_idx) else: - self.update_output_amt(self.chg_idx, self.proto.coin_amt(funds_left)) + self.update_output_amt(self.chg_idx, funds_left) def check_fee(self): fee = self.sum_inputs() - self.sum_outputs() @@ -119,7 +118,7 @@ class New(Base,TxBase.New): die( 'MaxFeeExceeded', f'Transaction fee of {fee} {c} too high! (> {self.proto.max_tx_fee} {c})' ) def final_inputs_ok_msg(self,funds_left): - return 'Transaction produces {} {} in change'.format(self.proto.coin_amt(funds_left).hl(), self.coin) + return 'Transaction produces {} {} in change'.format(funds_left.hl(), self.coin) async def create_serialized(self,locktime=None,bump=None): diff --git a/mmgen/proto/etc/params.py b/mmgen/proto/etc/params.py index 24bef4f3..c983fa88 100755 --- a/mmgen/proto/etc/params.py +++ b/mmgen/proto/etc/params.py @@ -25,3 +25,4 @@ class testnet(mainnet): class regtest(testnet): chain_names = ['developmentchain'] + decimal_prec = 64 diff --git a/mmgen/proto/eth/contract.py b/mmgen/proto/eth/contract.py index 091d3f56..34fdfee0 100755 --- a/mmgen/proto/eth/contract.py +++ b/mmgen/proto/eth/contract.py @@ -42,7 +42,8 @@ class TokenCommon(MMGenObject): def transferdata2amt(self,data): # online return self.proto.coin_amt( - int(parse_abi(data)[-1], 16) * self.base_unit) + int(parse_abi(data)[-1], 16) * self.base_unit, + from_decimal = True) async def do_call(self,method_sig,method_args='',toUnit=False): data = self.create_method_id(method_sig) + method_args @@ -59,7 +60,8 @@ class TokenCommon(MMGenObject): async def get_balance(self,acct_addr): return self.proto.coin_amt( - await self.do_call('balanceOf(address)', acct_addr.rjust(64, '0'), toUnit=True)) + await self.do_call('balanceOf(address)', acct_addr.rjust(64, '0'), toUnit=True), + from_decimal = True) def strip(self,s): return ''.join([chr(b) for b in s if 32 <= b <= 127]).strip() diff --git a/mmgen/proto/eth/params.py b/mmgen/proto/eth/params.py index c79d8e78..1b7afe2c 100755 --- a/mmgen/proto/eth/params.py +++ b/mmgen/proto/eth/params.py @@ -36,6 +36,7 @@ class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1): base_coin = 'ETH' avg_bdi = 15 ignore_daemon_version = False + decimal_prec = 36 chain_ids = { 1: 'ethereum', # ethereum mainnet diff --git a/mmgen/proto/eth/tw/unspent.py b/mmgen/proto/eth/tw/unspent.py index d08c52b1..efb20e4e 100755 --- a/mmgen/proto/eth/tw/unspent.py +++ b/mmgen/proto/eth/tw/unspent.py @@ -107,7 +107,7 @@ class EthereumTwUnspentOutputs(EthereumTwView,TwUnspentOutputs): return [{ 'account': TwLabel(self.proto,d['mmid']+' '+d['comment']), 'address': d['addr'], - 'amount': await self.twctl.get_balance(d['addr']), + 'amt': await self.twctl.get_balance(d['addr']), 'confirmations': 0, # TODO } for d in wl] diff --git a/mmgen/proto/eth/tx/bump.py b/mmgen/proto/eth/tx/bump.py index b11764f1..c127d1c0 100755 --- a/mmgen/proto/eth/tx/bump.py +++ b/mmgen/proto/eth/tx/bump.py @@ -23,7 +23,7 @@ class Bump(Completed,New,TxBase.Bump): @property def min_fee(self): - return self.proto.coin_amt(self.fee * Decimal('1.101')) + return self.fee * Decimal('1.101') def bump_fee(self,idx,fee): self.txobj['gasPrice'] = self.fee_abs2gas(fee) diff --git a/mmgen/proto/eth/tx/new.py b/mmgen/proto/eth/tx/new.py index ba70351e..ba7bb199 100755 --- a/mmgen/proto/eth/tx/new.py +++ b/mmgen/proto/eth/tx/new.py @@ -124,9 +124,7 @@ class New(Base,TxBase.New): # given rel fee and units, return absolute fee using self.gas def fee_rel2abs(self, tx_size, units, amt_in_units, unit): - return self.proto.coin_amt( - self.proto.coin_amt(amt_in_units, units[unit]).toWei() * self.gas.toWei(), - from_unit = 'wei') + return self.proto.coin_amt(amt_in_units, from_unit=units[unit]) * self.gas.toWei() # given fee estimate (gas price) in wei, return absolute fee, adjusting by self.cfg.fee_adjust def fee_est2abs(self,rel_fee,fe_type=None): @@ -151,7 +149,7 @@ class New(Base,TxBase.New): def update_change_output(self,funds_left): if self.outputs and self.outputs[0].is_chg: - self.update_output_amt(0,self.proto.coin_amt(funds_left)) + self.update_output_amt(0, funds_left) async def get_input_addrs_from_cmdline(self): ret = [] @@ -175,11 +173,8 @@ class New(Base,TxBase.New): return ret def final_inputs_ok_msg(self, funds_left): - chg = '0' if (self.outputs and self.outputs[0].is_chg) else funds_left - return 'Transaction leaves {} {} in the sender’s account'.format( - self.proto.coin_amt(chg).hl(), - self.proto.coin - ) + chg = self.proto.coin_amt('0') if (self.outputs and self.outputs[0].is_chg) else funds_left + return 'Transaction leaves {} {} in the sender’s account'.format(chg.hl(), self.proto.coin) class TokenNew(TokenBase,New): desc = 'transaction' diff --git a/mmgen/protocol.py b/mmgen/protocol.py index 0c56f258..d255cbfc 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -55,6 +55,7 @@ class CoinProtocol(MMGenObject): is_fork_of = None chain_names = None networks = ('mainnet','testnet','regtest') + decimal_prec = 28 def __init__(self,cfg,coin,name,network,tokensym=None,need_amt=False): self.cfg = cfg @@ -105,8 +106,10 @@ class CoinProtocol(MMGenObject): if need_amt: from . import amt + from decimal import getcontext self.coin_amt = getattr(amt,self.coin_amt) self.max_tx_fee = self.coin_amt(self.max_tx_fee) if hasattr(self,'max_tx_fee') else None + getcontext().prec = self.decimal_prec else: self.coin_amt = None self.max_tx_fee = None diff --git a/mmgen/rpc.py b/mmgen/rpc.py index 00d5f894..0d5f83b7 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -379,17 +379,20 @@ class RPCClient(MMGenObject): def process_http_resp(self,run_ret,batch=False,json_rpc=True): + def float_parser(n): + return n + text, status = run_ret if status == 200: dmsg_rpc(' RPC RESPONSE data ==>\n{}\n',text,is_json=True) m = None if batch: - return [r['result'] for r in json.loads(text,parse_float=Decimal)] + return [r['result'] for r in json.loads(text,parse_float=float_parser)] else: try: if json_rpc: - ret = json.loads(text,parse_float=Decimal)['result'] + ret = json.loads(text,parse_float=float_parser)['result'] if isinstance(ret,list) and ret and type(ret[0]) == dict and 'success' in ret[0]: for res in ret: if not res['success']: @@ -397,7 +400,7 @@ class RPCClient(MMGenObject): assert False return ret else: - return json.loads(text,parse_float=Decimal) + return json.loads(text,parse_float=float_parser) except: if not m: t = json.loads(text) diff --git a/mmgen/tw/addresses.py b/mmgen/tw/addresses.py index e5337259..1b70555e 100755 --- a/mmgen/tw/addresses.py +++ b/mmgen/tw/addresses.py @@ -15,6 +15,7 @@ tw.addresses: Tracking wallet listaddresses class for the MMGen suite from ..util import msg,suf,is_int from ..obj import MMGenListItem,ImmutableAttr,ListItemAttr,TwComment,NonNegativeInt from ..addr import CoinAddr,MMGenID,MMGenAddrType +from ..amt import CoinAmtChk from ..color import red,green,yellow from .view import TwView from .shared import TwMMGenID @@ -54,8 +55,8 @@ class TwAddresses(TwView): al_id = ImmutableAttr(str) # set to '_' for non-MMGen addresses confs = ImmutableAttr(int,typeconv=False) comment = ListItemAttr(TwComment,reassign_ok=True) - amt = ImmutableAttr(None) - recvd = ImmutableAttr(None) + amt = ImmutableAttr(CoinAmtChk, include_proto=True) + recvd = ImmutableAttr(CoinAmtChk, include_proto=True) date = ListItemAttr(int,typeconv=False,reassign_ok=True) skip = ListItemAttr(str,typeconv=False,reassign_ok=True) @@ -63,14 +64,6 @@ class TwAddresses(TwView): self.__dict__['proto'] = proto MMGenListItem.__init__(self,**kwargs) - class conv_funcs: - @staticmethod - def amt(instance,value): - return instance.proto.coin_amt(value) - @staticmethod - def recvd(instance,value): - return instance.proto.coin_amt(value) - @property def coinaddr_list(self): return [d.addr for d in self.data] diff --git a/mmgen/tw/unspent.py b/mmgen/tw/unspent.py index 7e19c147..366e423e 100755 --- a/mmgen/tw/unspent.py +++ b/mmgen/tw/unspent.py @@ -30,6 +30,7 @@ from ..obj import ( CoinTxID, NonNegativeInt ) from ..addr import CoinAddr +from ..amt import CoinAmtChk from .shared import TwMMGenID,get_tw_label from .view import TwView @@ -55,8 +56,8 @@ class TwUnspentOutputs(TwView): class MMGenTwUnspentOutput(MMGenListItem): txid = ListItemAttr(CoinTxID) vout = ListItemAttr(NonNegativeInt) - amt = ImmutableAttr(None) - amt2 = ListItemAttr(None) # the ETH balance for token account + amt = ImmutableAttr(CoinAmtChk, include_proto=True) + amt2 = ListItemAttr(CoinAmtChk, include_proto=True) # the ETH balance for token account comment = ListItemAttr(TwComment,reassign_ok=True) twmmid = ImmutableAttr(TwMMGenID,include_proto=True) addr = ImmutableAttr(CoinAddr,include_proto=True) @@ -69,14 +70,6 @@ class TwUnspentOutputs(TwView): self.__dict__['proto'] = proto MMGenListItem.__init__(self,**kwargs) - class conv_funcs: - @staticmethod - def amt(instance,value): - return instance.proto.coin_amt(value) - @staticmethod - def amt2(instance,value): - return instance.proto.coin_amt(value) - async def __init__(self,cfg,proto,minconf=1,addrs=[]): await super().__init__(cfg,proto) self.minconf = minconf @@ -94,10 +87,11 @@ class TwUnspentOutputs(TwView): continue # coinbase outputs have no account field l = get_tw_label(self.proto,o[lbl_id]) if l: + if not 'amt' in o: + o['amt'] = self.proto.coin_amt(o['amount']) o.update({ 'twmmid': l.mmid, 'comment': l.comment or '', - 'amt': self.proto.coin_amt(o['amount']), 'addr': CoinAddr(self.proto,o['address']), 'confs': o['confirmations'] }) diff --git a/mmgen/tx/base.py b/mmgen/tx/base.py index 47a62fd8..9934fb26 100755 --- a/mmgen/tx/base.py +++ b/mmgen/tx/base.py @@ -24,12 +24,13 @@ from ..obj import ( HexStr, NonNegativeInt ) +from ..amt import CoinAmtChk from ..addr import MMGenID,CoinAddr from ..util import msg,ymsg,fmt,remove_dups,make_timestamp,die class MMGenTxIO(MMGenListItem): vout = ListItemAttr(NonNegativeInt) - amt = ImmutableAttr(None) + amt = ImmutableAttr(CoinAmtChk, include_proto=True) comment = ListItemAttr(TwComment,reassign_ok=True) mmid = ListItemAttr(MMGenID,include_proto=True) addr = ImmutableAttr(CoinAddr,include_proto=True) @@ -56,11 +57,6 @@ class MMGenTxIO(MMGenListItem): 'S' if self.addr.addr_fmt == 'p2sh' else None ) - class conv_funcs: - @staticmethod - def amt(instance,value): - return instance.proto.coin_amt(value) - class MMGenTxIOList(list,MMGenObject): def __init__(self,parent,data=None): @@ -145,7 +141,7 @@ class Base(MMGenObject): olist = self.outputs[:exclude] + self.outputs[exclude+1:] if not olist: return self.proto.coin_amt('0') - return self.proto.coin_amt(sum(e.amt for e in olist)) + return sum(e.amt for e in olist) def _chg_output_ops(self,op): is_chgs = [x.is_chg for x in self.outputs] diff --git a/mmgen/tx/new.py b/mmgen/tx/new.py index cdf3d023..f1312802 100755 --- a/mmgen/tx/new.py +++ b/mmgen/tx/new.py @@ -111,6 +111,9 @@ class New(Base): # relative fee is N+ def feespec2abs(self, fee_arg, tx_size): + if type(fee_arg) is self.proto.coin_amt: + return fee_arg + if fee := get_obj(self.proto.coin_amt, num=fee_arg, silent=True): return fee diff --git a/mmgen/xmrwallet.py b/mmgen/xmrwallet.py index 5f61fc33..9d4f5767 100755 --- a/mmgen/xmrwallet.py +++ b/mmgen/xmrwallet.py @@ -391,9 +391,9 @@ class MoneroMMGenTX: dest = None if d.dest is None else XMRWalletAddrSpec(d.dest), dest_address = CoinAddr(proto,d.dest_address), txid = CoinTxID(d.txid), - amount = proto.coin_amt(d.amount,from_unit='atomic'), + amount = d.amount, priority = self.cfg.priority if self.name in ('NewSigned','NewUnsigned') else d.priority, - fee = proto.coin_amt(d.fee,from_unit='atomic'), + fee = d.fee, blob = d.blob, metadata = d.metadata, unsigned_txset = d.unsigned_txset, @@ -1161,8 +1161,8 @@ class MoneroWalletOps: dest = None, dest_address = addr, txid = res['tx_hash'], - amount = res['amount'], - fee = res['fee'], + amount = self.proto.coin_amt(res['amount'], from_unit='atomic'), + fee = self.proto.coin_amt(res['fee'], from_unit='atomic'), blob = res['tx_blob'], metadata = res['tx_metadata'], unsigned_txset = res['unsigned_txset'] if self.cfg.watch_only else None, @@ -1196,8 +1196,8 @@ class MoneroWalletOps: dest_addr_idx), dest_address = addr, txid = res['tx_hash_list'][0], - amount = res['amount_list'][0], - fee = res['fee_list'][0], + amount = self.proto.coin_amt(res['amount_list'][0], from_unit='atomic'), + fee = self.proto.coin_amt(res['fee_list'][0], from_unit='atomic'), blob = res['tx_blob_list'][0], metadata = res['tx_metadata_list'][0], unsigned_txset = res['unsigned_txset'] if self.cfg.watch_only else None, diff --git a/test/include/common.py b/test/include/common.py index f96d307f..5213b679 100755 --- a/test/include/common.py +++ b/test/include/common.py @@ -61,7 +61,7 @@ def strip_ansi_escapes(s): cmdtest_py_log_fn = 'cmdtest.py.log' cmdtest_py_error_fn = 'cmdtest.py.err' - +parity_dev_amt = 1606938044258990275541962092341162602522202993782792835301376 ascii_uc = ''.join(map(chr,list(range(65,91)))) # 26 chars ascii_lc = ''.join(map(chr,list(range(97,123)))) # 26 chars lat_accent = ''.join(map(chr,list(range(192,383)))) # 191 chars, L,S diff --git a/test/overlay/fakemods/mmgen/proto/btc/tw/unspent.py b/test/overlay/fakemods/mmgen/proto/btc/tw/unspent.py index 7632e7b0..2cd9e0a1 100644 --- a/test/overlay/fakemods/mmgen/proto/btc/tw/unspent.py +++ b/test/overlay/fakemods/mmgen/proto/btc/tw/unspent.py @@ -12,6 +12,6 @@ if overlay_fake_os.getenv('MMGEN_BOGUS_UNSPENT_DATA'): return json.loads(get_data_from_file( self.cfg, overlay_fake_os.getenv('MMGEN_BOGUS_UNSPENT_DATA') - ), parse_float=Decimal) + )) BitcoinTwUnspentOutputs.get_rpc_data = overlay_fake_BitcoinTwUnspentOutputs.get_rpc_data diff --git a/test/unit_tests_d/ut_obj.py b/test/unit_tests_d/ut_obj.py index 9ee5138a..e641fd9f 100755 --- a/test/unit_tests_d/ut_obj.py +++ b/test/unit_tests_d/ut_obj.py @@ -4,9 +4,17 @@ test.unit_tests_d.ut_obj: data object unit tests for the MMGen suite """ -from decimal import Decimal +from decimal import Decimal, getcontext -from ..include.common import vmsg +from ..include.common import vmsg, cfg, parity_dev_amt +from mmgen.protocol import init_proto + +def test_equal(res, chk): + vmsg(f' checking {res}') + if type(res) is type: + assert res is chk, f'{res} != {chk}' + else: + assert res == chk, f'{res} != {chk}' def coinamt_test(cls, aa, bb, ut): @@ -46,8 +54,8 @@ def coinamt_test(cls, aa, bb, ut): ('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), + ('operand type', 'TypeError', 'incorrect type', lambda: a + B), + ('operand type', 'TypeError', 'incorrect type', lambda: b - A), ) if cls.max_amt is not None: @@ -81,3 +89,118 @@ class unit_tests: ): coinamt_test(cls, aa, bb, ut) return True + + def coinamt2(self, name, ut, desc='CoinAmt class'): + from decimal import Decimal + proto = init_proto(cfg, 'btc', network='testnet', need_amt=True) + + test_equal(getcontext().prec, proto.decimal_prec) + + coin_amt = proto.coin_amt + test_equal(coin_amt.__name__, 'BTCAmt') + a = coin_amt('1.234') + a2 = coin_amt('2.468') + + # addition with integer zero: + b = a + 0 # __add__ + b = 0 + a # __radd__ + test_equal(sum([a, a]), a2) # __radd__ (sum() starts with integer 0) + + # __add__ + b = coin_amt('333.2456') + test_equal(a + b, coin_amt('334.4796')) + + # __sub__ + test_equal(a - coin_amt('1'), coin_amt('0.234')) + test_equal(coin_amt('2') - a, coin_amt('0.766')) + + # __mul__ + b = a * 2 + test_equal(type(b), coin_amt) + test_equal(b, a2) + + # __rmul__ + b = 2 * a + test_equal(type(b), coin_amt) + test_equal(b, a2) + + # __truediv__ + b = a / 2 + test_equal(type(b), coin_amt) + test_equal(b, coin_amt('0.617')) + + # __rtruediv__ + b = 2 / a + test_equal(type(b), coin_amt) + test_equal(b, coin_amt('1.62074554')) + + def bad1(): b = a + 1 + def bad2(): b = a - 1 + def bad3(): a + Decimal(1) + def bad4(): b = a + 0.0 + def bad5(): b = a - 0.0 + + def bad1r(): b = 1 + a + def bad2r(): b = 3 - a + def bad3r(): Decimal(1) + a + def bad4r(): b = 0.0 + a + def bad5r(): b = 0.0 - a + + def bad10(): b = coin_amt('1') - a + def bad11(): b = a * -2 + def bad12(): b = a / -2 + def bad13(): b = a - coin_amt('2') + def bad14(): b = -2 * a + def bad15(): b = -2 / a + + def bad16(): b = coin_amt(a) + + vmsg('Testing error handling:') + + ut.process_bad_data( + ( + ('addition with int', 'TypeError', 'incorrect type', bad1), + ('subtraction with int', 'TypeError', 'incorrect type', bad2), + ('addition with Decimal', 'TypeError', 'incorrect type', bad3), + ('addition with float', 'TypeError', 'incorrect type', bad4), + ('subtraction with float', 'TypeError', 'incorrect type', bad5), + + ('addition with int', 'TypeError', 'incorrect type', bad1r), + ('subtraction with int', 'TypeError', 'incorrect type', bad2r), + ('addition with Decimal', 'TypeError', 'incorrect type', bad3r), + ('addition with float', 'TypeError', 'incorrect type', bad4r), + ('subtraction with float', 'TypeError', 'incorrect type', bad5r), + + ('negative result', 'ObjectInitError', 'cannot be negative', bad10), + ('negative result', 'ObjectInitError', 'cannot be negative', bad11), + ('negative result', 'ObjectInitError', 'cannot be negative', bad12), + ('negative result', 'ObjectInitError', 'cannot be negative', bad13), + ('negative result', 'ObjectInitError', 'cannot be negative', bad14), + ('negative result', 'ObjectInitError', 'cannot be negative', bad15), + + ('double initialization', 'TypeError', 'is instance', bad16), + ), + pfx = '') + + + return True + + def coinamt_alt2(self, name, ut, desc='CoinAmt class (altcoins)'): + proto = init_proto(cfg, 'etc', network='regtest', need_amt=True) + test_equal(getcontext().prec, proto.decimal_prec) + coin_amt = proto.coin_amt + dev_amt = coin_amt(parity_dev_amt, from_unit='wei') + dev_amt_s = '1606938044258990275541962092341162602522202.993782792835301376' + dev_amt_a1 = '1606938044258990275541962092341162602522203.993782792835301377' + dev_amt_s1 = '1606938044258990275541962092341162602522201.993782792835301375' + dev_amt_d2 = '803469022129495137770981046170581301261101.496891396417650688' + dev_amt_d10 = '160693804425899027554196209234116260252220.299378279283530138' + test_equal(str(dev_amt), dev_amt_s) + addend = coin_amt('1.000000000000000001') + test_equal(dev_amt + addend, coin_amt(dev_amt_a1)) + test_equal(dev_amt - addend, coin_amt(dev_amt_s1)) + test_equal(dev_amt / coin_amt('2'), coin_amt(dev_amt_d2)) + test_equal(dev_amt / coin_amt('10'), coin_amt(dev_amt_d10)) + test_equal(2 / coin_amt('0.3456'), coin_amt('5.787037037037037037')) + test_equal(2.345 * coin_amt('2.3456'), coin_amt('5.500432000000000458')) + return True diff --git a/test/unit_tests_d/ut_tx_deserialize.py b/test/unit_tests_d/ut_tx_deserialize.py index db92d092..133211c1 100755 --- a/test/unit_tests_d/ut_tx_deserialize.py +++ b/test/unit_tests_d/ut_tx_deserialize.py @@ -77,8 +77,8 @@ async def test_tx(tx_proto,tx_hex,desc,n): fs = 'address of output {} does not match\nA: {}\nB: {}' assert A == B, fs.format(i,A,B) - A = a[i]['value'] - B = b[i]['amount'] + A = tx_proto.coin_amt(a[i]['value']) + B = b[i]['amt'] fs = 'value of output {} does not match\nA: {}\nB: {}' assert A == B, fs.format(i,A,B)