From 1b54d425bd2d85dea67bd599b68a1817acb2700d Mon Sep 17 00:00:00 2001 From: MMGen Date: Mon, 28 May 2018 09:39:49 +0000 Subject: [PATCH] addr.py,rpc.py,tw.py,tx.py,traceback.py: minor cleanups and refactoring --- mmgen/addr.py | 7 ++- mmgen/rpc.py | 5 +- mmgen/tw.py | 76 ++++++++++++------------- mmgen/tx.py | 129 +++++++++++++++++++++++++------------------ scripts/traceback.py | 23 +++++--- 5 files changed, 137 insertions(+), 103 deletions(-) diff --git a/mmgen/addr.py b/mmgen/addr.py index 29768f81..75089245 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -545,12 +545,15 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file return [d.addr for d in self.data if not getattr(d,key)] def generate_addrs_from_keys(self): - kg = KeyGenerator('std') - ag = AddrGenerator('p2pkh') + # assume that the first listed mmtype is valid for flat key list + t = MMGenAddrType(g.proto.mmtypes[0]) + kg = KeyGenerator(t.pubkey_type) + ag = AddrGenerator(t.gen_method) d = self.data for n,e in enumerate(d,1): qmsg_r('\rGenerating addresses from keylist: {}/{}'.format(n,len(d))) e.addr = ag.to_addr(kg.to_pubhex(e.sec)) + if g.debug_addrlist: Msg('generate_addrs_from_keys():\n{}'.format(e.pformat())) qmsg('\rGenerated addresses from keylist: {}/{} '.format(n,len(d))) def format(self,enable_comments=False): diff --git a/mmgen/rpc.py b/mmgen/rpc.py index 86823321..b3533e60 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -213,7 +213,6 @@ class EthereumRPCConnection(CoinDaemonRPCConnection): 'eth_blockNumber', 'eth_gasPrice', 'eth_getBalance', - 'eth_getBlock', 'eth_getBlockByHash', 'eth_getBlockByNumber', 'eth_getTransactionByHash', @@ -225,7 +224,11 @@ class EthereumRPCConnection(CoinDaemonRPCConnection): 'net_peerCount', 'net_version', 'parity_chain', + # Returns the EIP155 chain ID used for transaction signing at the current best block. + # Null is returned if not available. + 'parity_chainId', 'parity_chainStatus', + 'parity_composeTransaction', 'parity_gasCeilTarget', 'parity_gasFloorTarget', 'parity_localTransactions', diff --git a/mmgen/tw.py b/mmgen/tw.py index 8c35e96f..c12e9a82 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -29,8 +29,9 @@ CUR_HOME,ERASE_ALL = '\033[H','\033[0J' class TwUnspentOutputs(MMGenObject): txid_w = 64 - show_tx = True + show_txid = True can_group = True + hdr_fmt = 'UNSPENT OUTPUTS (sort order: {}) Total {}: {}' prompt = """ Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen @@ -175,21 +176,19 @@ watch-only wallet using '{}-addrimport' and then re-run this program. if self.sort_key == k and getattr(a,k) == getattr(b,k): b.skip = (k,'addr')[k=='twmmid'] - hdr_fmt = 'UNSPENT OUTPUTS (sort order: {}) Total {}: {}' - out = [hdr_fmt.format(' '.join(self.sort_info()),g.coin,self.total.hl())] + out = [self.hdr_fmt.format(' '.join(self.sort_info()),g.coin,self.total.hl())] if g.chain in ('testnet','regtest'): out += [green('Chain: {}'.format(g.chain.upper()))] - if self.show_tx: + if self.show_txid: fs = u' {n:%s} {t:%s} {v:2} {a} {A} {c:<}' % (col1_w,tx_w) else: fs = u' {n:%s} {a} {A} {c:<}' % col1_w - out += [fs.format( - n='Num', - t='TXid'.ljust(tx_w - 5) + ' Vout', - v='', - a='Address'.ljust(addr_w), - A='Amt({})'.format(g.coin).ljust(g.proto.coin_amt.max_prec+4), - c=('Confs','Age(d)')[self.show_days])] + out += [fs.format( n='Num', + t='TXid'.ljust(tx_w - 5) + ' Vout', + v='', + a='Address'.ljust(addr_w), + A='Amt({})'.format(g.coin).ljust(g.proto.coin_amt.max_prec+4), + c=('Confs','Age(d)')[self.show_days])] for n,i in enumerate(unsp): addr_dots = '|' + '.'*(addr_w-1) @@ -209,13 +208,12 @@ watch-only wallet using '{}-addrimport' and then re-run this program. tx = ' ' * (tx_w-4) + '|...' if i.skip == 'txid' \ else i.txid[:tx_w-len(txdots)]+txdots - out.append(fs.format( - n=str(n+1)+')', - t=tx, - v=i.vout, - a=addr_out, - A=i.amt.fmt(color=True), - c=i.days if self.show_days else i.confs)) + out.append(fs.format( n=str(n+1)+')', + t=tx, + v=i.vout, + a=addr_out, + A=i.amt.fmt(color=True), + c=i.days if self.show_days else i.confs)) self.fmt_display = '\n'.join(out) + '\n' # unsp.pdie() @@ -225,36 +223,34 @@ watch-only wallet using '{}-addrimport' and then re-run this program. addr_w = max(len(i.addr) for i in self.unspent) mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in self.unspent) or 12 # DEADBEEF:S:1 - if self.show_tx: + if self.show_txid: fs = ' {n:4} {t:%s} {a} {m} {A:%s} {c:<8} {g:<6} {l}' % (self.txid_w+3,g.proto.coin_amt.max_prec+4) else: fs = ' {n:4} {a} {m} {A:%s} {c:<8} {g:<6} {l}' % (g.proto.coin_amt.max_prec+4) - out = [fs.format( - n='Num', - t='Tx ID,Vout', - a='Address'.ljust(addr_w), - m='MMGen ID'.ljust(mmid_w+1), - A='Amount({})'.format(g.coin), - c='Confs', - g='Age(d)', - l='Label')] + out = [fs.format( n='Num', + t='Tx ID,Vout', + a='Address'.ljust(addr_w), + m='MMGen ID'.ljust(mmid_w+1), + A='Amount({})'.format(g.coin), + c='Confs', + g='Age(d)', + l='Label')] max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [1]) for n,i in enumerate(self.unspent): addr = '|'+'.' * addr_w if i.skip == 'addr' and self.group else i.addr.fmt(color=color,width=addr_w) tx = '|'+'.' * 63 if i.skip == 'txid' and self.group else str(i.txid) - out.append( - fs.format( - n=str(n+1)+')', - t=tx+','+str(i.vout), - a=addr, - m=MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen' - else 'Non-{}'.format(g.proj_name),width=mmid_w,color=color), - A=i.amt.fmt(color=color), - c=i.confs, - g=i.days, - l=i.label.hl(color=color) if i.label else - TwComment.fmtc('',color=color,nullrepl='-',width=max_lbl_len)).rstrip()) + out.append(fs.format( + n=str(n+1)+')', + t=tx+','+str(i.vout), + a=addr, + m=MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen' + else 'Non-{}'.format(g.proj_name),width=mmid_w,color=color), + A=i.amt.fmt(color=color), + c=i.confs, + g=i.days, + l=i.label.hl(color=color) if i.label else + TwComment.fmtc('',color=color,nullrepl='-',width=max_lbl_len)).rstrip()) fs = 'Unspent outputs ({} UTC)\nSort order: {}\n{}\n\nTotal {}: {}\n' self.fmt_print = fs.format( diff --git a/mmgen/tx.py b/mmgen/tx.py index 2e3d5a7a..23092d0b 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -80,16 +80,6 @@ def strfmt_locktime(num,terse=False): else: die(2,"'{}': invalid locktime value!".format(num)) -def select_unspent(unspent,prompt): - while True: - reply = my_raw_input(prompt).strip() - if reply: - selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return') - if selected: - if selected[-1] <= len(unspent): - return selected - msg('Unspent output number must be <= {}'.format(len(unspent))) - def mmaddr2coinaddr(mmaddr,ad_w,ad_f): # assume mmaddr has already been checked @@ -234,6 +224,8 @@ class MMGenTX(MMGenObject): sig_ext = 'sigtx' txid_ext = 'txid' desc = 'transaction' + chg_fs = 'Transaction produces {} {} in change' + no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change' class MMGenTxInput(MMGenListItem): for k in txio_attrs: locals()[k] = txio_attrs[k] # in lieu of inheritance @@ -275,6 +267,7 @@ class MMGenTX(MMGenObject): self.inputs = self.MMGenTxInputList() self.outputs = self.MMGenTxOutputList() self.send_amt = g.proto.coin_amt('0') # total amt minus change + self.fee = g.proto.coin_amt('0') self.hex = '' # raw serialized hex transaction self.label = MMGenTXLabel('') self.txid = '' @@ -295,11 +288,15 @@ class MMGenTX(MMGenObject): self.check_sigs() # marks the tx as signed # repeat with sign and send, because coin daemon could be restarted - self.die_if_incorrect_chain() + self.check_correct_chain(on_fail='die') - def die_if_incorrect_chain(self): - if self.chain and g.chain and self.chain != g.chain: - die(2,'Transaction is for {}, but current chain is {}!'.format(self.chain,g.chain)) + def check_correct_chain(self,on_fail='return'): + assert on_fail in ('return','die'),"'{}': invalid value for 'on_fail'".format(on_fail) + m = 'Transaction is for {}, but current chain is {}!'.format(self.chain,g.chain) + bad = self.chain and g.chain and self.chain != g.chain + if bad: + msg(m) if on_fail == 'return' else die(2,m) + return not bad def add_output(self,coinaddr,amt,is_chg=None): self.outputs.append(MMGenTX.MMGenTxOutput(addr=coinaddr,amt=amt,is_chg=is_chg)) @@ -564,8 +561,11 @@ class MMGenTX(MMGenObject): assert type(val) == int,'locktime value not an integer' self.hex = self.hex[:-8] + hexlify(unhexlify('{:08x}'.format(val))[::-1]) + def get_blockcount(self): + return int(g.rpch.getblockcount()) + def add_blockcount(self): - self.blockcount = int(g.rpch.getblockcount()) + self.blockcount = self.get_blockcount() def format(self): self.inputs.check_coin_mismatch() @@ -604,12 +604,15 @@ class MMGenTX(MMGenObject): def sign(self,tx_num_str,keys): if self.marked_signed(): - die(1,'Transaction is already signed!') + msg('Transaction is already signed!') + return False - self.die_if_incorrect_chain() + if not self.check_correct_chain(on_fail='return'): + return False if (self.has_segwit_inputs() or self.has_segwit_outputs()) and not g.proto.cap('segwit'): - die(2,yellow("TX has Segwit inputs or outputs, but {} doesn't support Segwit!".format(g.coin))) + ymsg("TX has Segwit inputs or outputs, but {} doesn't support Segwit!".format(g.coin)) + return False qmsg('Passing {} key{} to {}'.format(len(keys),suf(keys,'s'),g.proto.daemon_name)) @@ -793,7 +796,7 @@ class MMGenTX(MMGenObject): if not self.marked_signed(): die(1,'Transaction is not signed!') - self.die_if_incorrect_chain() + self.check_correct_chain(on_fail='die') self.check_hex_tx_matches_mmgen_tx(DeserializedTX(self.hex)) @@ -908,7 +911,7 @@ class MMGenTX(MMGenObject): def format_view(self,terse=False): try: rpc_init() - blockcount = g.rpch.getblockcount() + blockcount = self.get_blockcount() except: blockcount = None @@ -928,7 +931,7 @@ class MMGenTX(MMGenObject): addr_w = max(len(e.addr) for e in io) confs_per_day = 60*60*24 / g.proto.secs_per_block for n,e in enumerate(sorted(io,key=lambda o: o.mmid.sort_key if o.mmid else o.addr)): - if ip and blockcount != None: + if ip and blockcount: confs = e.confs + blockcount - self.blockcount days = int(confs / confs_per_day) if e.mmid: @@ -950,7 +953,7 @@ class MMGenTX(MMGenObject): ('','comment:',e.label.hl() if e.label else ''), ('','amount:','{} {}'.format(e.amt.hl(),g.coin))] items = [(n+1, 'tx,vout:','{},{}'.format(e.txid,e.vout))] + icommon + [ - ('','confirmations:','{} (around {} days)'.format(confs,days) if blockcount!=None else '') + ('','confirmations:','{} (around {} days)'.format(confs,days) if blockcount else '') ] if ip else icommon + [ ('','change:',green('True') if e.is_chg else '')] io_out += '\n'.join([(u'{:>3} {:<8} {}'.format(*d)) for d in items if d[2]]) + '\n\n' @@ -964,8 +967,7 @@ class MMGenTX(MMGenObject): self.send_amt.hl(), g.coin, self.timestamp, - (red('False'), - green('True'))[self.is_rbf()], + (red('False'),green('True'))[self.is_rbf()], self.marked_signed(color=True), (green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)]) if self.chain in ('testnet','regtest'): @@ -998,6 +1000,9 @@ class MMGenTX(MMGenObject): return out # TX label might contain non-ascii chars + def check_tx_hex_data(self): + self.hex = HexStr(self.hex,on_fail='raise') + def parse_tx_file(self,infile,md_only=False,silent_open=False): def eval_io_data(raw_data,desc): @@ -1069,7 +1074,7 @@ class MMGenTX(MMGenObject): desc = 'block count in metadata' self.blockcount = int(blockcount) desc = 'transaction hex data' - self.hex = HexStr(self.hex,on_fail='raise') + self.check_tx_hex_data() if md_only: return # the following ops will all fail if g.coin doesn't match self.coin desc = 'coin type in metadata' assert self.coin == g.coin,'invalid coin type' @@ -1093,38 +1098,27 @@ class MMGenTX(MMGenObject): try: ret = g.rpch.estimatesmartfee(opt.tx_confs,on_fail='raise') except: - fetype = 'estimatefee' + fe_type = 'estimatefee' fee_per_kb = g.rpch.estimatefee(opt.tx_confs) else: - fetype = 'estimatesmartfee' + fe_type = 'estimatesmartfee' fee_per_kb = ret['feerate'] if 'feerate' in ret else -2 if fee_per_kb < 0: if not have_estimate_fail: - msg('Network fee estimation for {} confirmations failed ({})'.format(opt.tx_confs,fetype)) + msg('Network fee estimation for {} confirmations failed ({})'.format(opt.tx_confs,fe_type)) have_estimate_fail.append(True) start_fee = None else: start_fee = g.proto.coin_amt(fee_per_kb) * opt.tx_fee_adj * self.estimate_size() / 1024 if opt.verbose: msg('{} fee for {} confirmations: {} {}/kB'.format( - fetype.upper(),opt.tx_confs,fee_per_kb,g.coin)) + fe_type.upper(),opt.tx_confs,fee_per_kb,g.coin)) msg('TX size (estimated): {}'.format(self.estimate_size())) return self.get_usr_fee_interactive(start_fee,desc=desc) - def get_outputs_from_cmdline(self,cmd_args): - from mmgen.addr import AddrList,AddrData - addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext] - cmd_args = set(cmd_args) - set(addrfiles) - - ad_f = AddrData() - for a in addrfiles: - check_infile(a) - ad_f.add(AddrList(a)) - - ad_w = AddrData(source='tw') - + def process_cmd_args(self,cmd_args,ad_f,ad_w): for a in cmd_args: if ',' in a: a1,a2 = a.split(',',1) @@ -1141,24 +1135,48 @@ class MMGenTX(MMGenObject): else: die(2,'{}: invalid command-line argument'.format(a)) - if not self.outputs: - die(2,'At least one output must be specified on the command line') - if self.get_chg_output_idx() == None: die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(self.outputs) == 1]) - self.add_mmaddrs_to_outputs(ad_w,ad_f) - self.check_dup_addrs('outputs') - if not segwit_is_active() and self.has_segwit_outputs(): fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain' rdie(2,fs.format(g.proj_name)) + def get_outputs_from_cmdline(self,cmd_args): + from mmgen.addr import AddrList,AddrData + addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext] + cmd_args = set(cmd_args) - set(addrfiles) + + ad_f = AddrData() + for a in addrfiles: + check_infile(a) + ad_f.add(AddrList(a)) + + ad_w = AddrData(source='tw') + + self.process_cmd_args(cmd_args,ad_f,ad_w) + + if not self.outputs: + die(2,'At least one output must be specified on the command line') + + self.add_mmaddrs_to_outputs(ad_w,ad_f) + self.check_dup_addrs('outputs') + + def select_unspent(self,unspent): + prompt = 'Enter a range or space-separated list of outputs to spend: ' + while True: + reply = my_raw_input(prompt).strip() + if reply: + selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return') + if selected: + if selected[-1] <= len(unspent): + return selected + msg('Unspent output number must be <= {}'.format(len(unspent))) + def get_inputs_from_user(self,tw): while True: - m = 'Enter a range or space-separated list of outputs to spend: ' - sel_nums = select_unspent(tw.unspent,m) + sel_nums = self.select_unspent(tw.unspent) msg('Selected output{}: {}'.format(suf(sel_nums,'s'),' '.join(map(str,sel_nums)))) sel_unspent = tw.MMGenTwOutputList([tw.unspent[i-1] for i in sel_nums]) @@ -1176,16 +1194,21 @@ class MMGenTX(MMGenObject): self.copy_inputs_from_tw(sel_unspent) # makes self.inputs - change_amt = self.sum_inputs() - self.send_amt - self.get_fee_from_user() + self.fee = self.get_fee_from_user() + + change_amt = self.sum_inputs() - self.send_amt - self.fee if change_amt >= 0: - p = 'Transaction produces {} {} in change'.format(change_amt.hl(),g.coin) + p = self.chg_fs.format(change_amt.hl(),g.coin) if opt.yes or keypress_confirm(p+'. OK?',default_yes=True): if opt.yes: msg(p) return change_amt else: msg(wmsg['not_enough_coin'].format(abs(change_amt))) + def check_fee(self): + assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee + def create(self,cmd_args,locktime,do_info=False): assert type(locktime) == int @@ -1217,7 +1240,7 @@ class MMGenTX(MMGenObject): chg_idx = self.get_chg_output_idx() if change_amt == 0: - msg('Warning: Change address will be deleted as transaction produces no change') + msg(self.no_chg_msg) self.del_output(chg_idx) else: self.update_output_amt(chg_idx,g.proto.coin_amt(change_amt)) @@ -1239,7 +1262,7 @@ class MMGenTX(MMGenObject): self.add_blockcount() self.chain = g.chain - assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee + self.check_fee() qmsg('Transaction successfully created') diff --git a/scripts/traceback.py b/scripts/traceback.py index bf3bc86a..1b1a18d5 100755 --- a/scripts/traceback.py +++ b/scripts/traceback.py @@ -6,7 +6,18 @@ if 'TMUX' in os.environ: del os.environ['TMUX'] os.environ['MMGEN_TRACEBACK'] = '1' tb_source = open(sys.argv[1]) -tb_file = open('my.err','w') +tb_file = os.path.join(os.environ['PWD'],'my.err') + +def process_exception(es): + l = traceback.format_exception(*es) + l_save = l[:] + exc = l.pop() + if exc[:11] == 'SystemExit:': l.pop() + def red(s): return '{e}[31;1m{}{e}[0m'.format(s,e='\033') + def yellow(s): return '{e}[33;1m{}{e}[0m'.format(s,e='\033') + sys.stdout.write('{}{}'.format(yellow(''.join(l)),red(exc))) + with open(tb_file,'w') as f: + f.write(''.join(l_save)) try: sys.argv.pop(0) @@ -14,12 +25,10 @@ try: except SystemExit: # pass e = sys.exc_info() + if int(str(e[1])) != 0: + process_exception(e) sys.exit(int(str(e[1]))) except: - l = traceback.format_exception(*sys.exc_info()) - exc = l.pop() - def red(s): return '{e}[31;1m{}{e}[0m'.format(s,e='\033') - def yellow(s): return '{e}[33;1m{}{e}[0m'.format(s,e='\033') - sys.stdout.write('{}{}'.format(yellow(''.join(l)),red(exc))) - traceback.print_exc(file=tb_file) + e = sys.exc_info() + process_exception(e) sys.exit(1)