From 854d6532dc3f4ec57c1a46bc481b9d96e768b374 Mon Sep 17 00:00:00 2001 From: MMGen Date: Sat, 26 May 2018 20:50:21 +0000 Subject: [PATCH] Ethereum twview support --- mmgen/altcoins/eth/tw.py | 38 ++++++++++++++- mmgen/obj.py | 5 +- mmgen/tw.py | 101 ++++++++++++++++++++++++++------------- 3 files changed, 109 insertions(+), 35 deletions(-) diff --git a/mmgen/altcoins/eth/tw.py b/mmgen/altcoins/eth/tw.py index 3c209a68..0efcd183 100755 --- a/mmgen/altcoins/eth/tw.py +++ b/mmgen/altcoins/eth/tw.py @@ -23,7 +23,7 @@ altcoins.eth.tw: ETH tracking wallet functions and methods for the MMGen suite import json from mmgen.common import * from mmgen.obj import * -from mmgen.tw import TrackingWallet,TwAddrList +from mmgen.tw import TrackingWallet,TwAddrList,TwUnspentOutputs from mmgen.addr import AddrData # No file locking - 2 processes accessing the wallet at the same time will corrupt it @@ -100,6 +100,42 @@ class EthereumTrackingWallet(TrackingWallet): else: # emulate RPC library return ('rpcfail',(None,2,"Address '{}' not found in tracking wallet".format(coinaddr))) +# Use consistent naming, even though Ethereum doesn't have unspent outputs +class EthereumTwUnspentOutputs(TwUnspentOutputs): + + show_tx = False + can_group = False + prompt = """ +Sort options: [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr +Display options: show [D]ays, show [m]mgen addr, r[e]draw screen +""" + + def do_sort(self,key=None,reverse=False): + if key == 'txid': return + super(type(self),self).do_sort(key=key,reverse=reverse) + + class MMGenTwUnspentOutput(MMGenListItem): + # attrs = 'txid','vout','amt','label','twmmid','addr','confs','days','skip' + txid = MMGenImmutableAttr('txid',str,typeconv=False) + vout = MMGenImmutableAttr('vout',str,typeconv=False) + amt = MMGenImmutableAttr('amt',g.proto.coin_amt.__name__) + label = MMGenListItemAttr('label','TwComment',reassign_ok=True) + twmmid = MMGenImmutableAttr('twmmid','TwMMGenID') + addr = MMGenImmutableAttr('addr','CoinAddr') + confs = MMGenImmutableAttr('confs',int,typeconv=False) + days = MMGenListItemAttr('days',int,typeconv=False) + skip = MMGenListItemAttr('skip',str,typeconv=False,reassign_ok=True) + + def get_unspent_rpc(self): + rpc_init() + return map(lambda d: { + 'txid': 'N/A', + 'vout': '', + 'account': TwLabel(d['mmid']+' '+d['comment'],on_fail='raise'), + 'address': d['addr'], + 'amount': ETHAmt(int(g.rpch.eth_getBalance('0x'+d['addr']),16),fromWei=True), + 'confirmations': 0, # TODO + }, EthereumTrackingWallet().sorted_list()) class EthereumTwAddrList(TwAddrList): diff --git a/mmgen/obj.py b/mmgen/obj.py index eba85f40..48647ea3 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -308,6 +308,7 @@ class BTCAmt(Decimal,Hilite,InitErrors): max_prec = 8 max_amt = 21000000 min_coin_unit = Decimal('0.00000001') # satoshi + amt_fs = '4.8' def __new__(cls,num,on_fail='die'): if type(num) == cls: return num @@ -328,7 +329,8 @@ class BTCAmt(Decimal,Hilite,InitErrors): def fmtc(cls): raise NotImplementedError - def fmt(self,fs='4.8',color=False,suf=''): + def fmt(self,fs=None,color=False,suf=''): + if fs == None: fs = self.amt_fs s = str(int(self)) if int(self) == self else self.normalize().__format__('f') if '.' in fs: p1,p2 = map(int,fs.split('.',1)) @@ -376,6 +378,7 @@ class ETHAmt(BTCAmt): max_prec = 18 max_amt = 999999999 # TODO min_coin_unit = Decimal('0.000000000000000001') # wei + amt_fs = '4.18' def __new__(cls,num,on_fail='die',fromWei=False): if type(num) == cls: return num diff --git a/mmgen/tw.py b/mmgen/tw.py index afa84dad..8c35e96f 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -28,6 +28,14 @@ CUR_HOME,ERASE_ALL = '\033[H','\033[0J' class TwUnspentOutputs(MMGenObject): + txid_w = 64 + show_tx = True + can_group = True + 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 +""" + class MMGenTwOutputList(list,MMGenObject): pass class MMGenTwUnspentOutput(MMGenListItem): @@ -50,6 +58,12 @@ watch-only wallet using '{}-addrimport' and then re-run this program. """.strip().format(g.proj_name.lower()) } + def __new__(cls,*args,**kwargs): + if g.coin == 'ETH': + from mmgen.altcoins.eth.tw import EthereumTwUnspentOutputs + cls = EthereumTwUnspentOutputs + return MMGenObject.__new__(cls,*args,**kwargs) + def __init__(self,minconf=1): self.unspent = self.MMGenTwOutputList() self.fmt_display = '' @@ -68,11 +82,14 @@ watch-only wallet using '{}-addrimport' and then re-run this program. def get_total_coin(self): return sum(i.amt for i in self.unspent) + def get_unspent_rpc(self): + return g.rpch.listunspent(self.minconf) + def get_unspent_data(self): if g.bogus_wallet_data: # for debugging purposes only us_rpc = eval(get_data_from_file(g.bogus_wallet_data)) # testing, so ok else: - us_rpc = g.rpch.listunspent(self.minconf) + us_rpc = self.get_unspent_rpc() # write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data') # sys.exit(0) @@ -130,7 +147,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program. my_raw_input((m1+m2).format(g.min_screen_width)) def display(self): - if not opt.no_blank: msg(CUR_HOME+ERASE_ALL) + if not opt.no_blank: msg_r(CUR_HOME+ERASE_ALL) msg(self.format_for_display()) def format_for_display(self): @@ -148,8 +165,8 @@ watch-only wallet using '{}-addrimport' and then re-run this program. acct_w = min(max_acct_w, max(24,addr_w-10)) btaddr_w = addr_w - acct_w - 1 label_w = acct_w - mmid_w - 1 - tx_w = min(64,self.cols-addr_w-28-col1_w) # min=7 - txdots = ('','..')[tx_w < 64] + tx_w = min(self.txid_w,self.cols-addr_w-28-col1_w) # min=7 + txdots = ('','..')[tx_w < self.txid_w] for i in unsp: i.skip = '' if self.group and (self.sort_key in ('addr','txid','twmmid')): @@ -158,16 +175,21 @@ 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 {}: {}' + hdr_fmt = 'UNSPENT OUTPUTS (sort order: {}) Total {}: {}' out = [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()))] - fs = u' {:%s} {:%s} {:2} {} {} {:<}' % (col1_w,tx_w) - out += [fs.format('Num', - 'TXid'.ljust(tx_w - 5) + ' Vout', '', - 'Address'.ljust(addr_w), - 'Amt({})'.format(g.coin).ljust(12), - ('Confs','Age(d)')[self.show_days])] + if self.show_tx: + 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])] for n,i in enumerate(unsp): addr_dots = '|' + '.'*(addr_w-1) @@ -187,8 +209,13 @@ 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(str(n+1)+')',tx,i.vout,addr_out,i.amt.fmt(color=True), - 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() @@ -198,29 +225,38 @@ 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 - fs = ' {:4} {:67} {} {} {:12} {:<8} {:<6} {}' - out = [fs.format('Num','Tx ID,Vout', - 'Address'.ljust(addr_w), - 'MMGen ID'.ljust(mmid_w+1), - 'Amount({})'.format(g.coin), - 'Confs','Age(d)', - 'Label')] + if self.show_tx: + 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')] 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(str(n+1)+')', tx+','+str(i.vout), - addr, - MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen' + 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), - i.amt.fmt(color=color), - i.confs,i.days, - i.label.hl(color=color) if i.label else + 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{}\n\nTotal {}: {}\n' + fs = 'Unspent outputs ({} UTC)\nSort order: {}\n{}\n\nTotal {}: {}\n' self.fmt_print = fs.format( make_timestr(), ' '.join(self.sort_info(include_group=False)), @@ -259,23 +295,22 @@ watch-only wallet using '{}-addrimport' and then re-run this program. def view_and_sort(self,tx): fs = 'Total to spend, excluding fees: {} {}\n\n' txos = fs.format(tx.sum_outputs().hl(),g.coin) if tx.outputs else '' - 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 - """.format(txos).strip() + prompt = txos + self.prompt.strip() self.display() msg(prompt) from mmgen.term import get_char p = "'q'=quit view, 'p'=print to file, 'v'=pager view, 'w'=wide view, 'l'=add label:\b" while True: - reply = get_char(p, immed_chars='atDdAMrgmeqpvw') + reply = get_char(p,immed_chars='atDdAMrgmeqpvw') if reply == 'a': self.do_sort('amt') elif reply == 'A': self.do_sort('age') elif reply == 'd': self.do_sort('addr') elif reply == 'D': self.show_days = not self.show_days elif reply == 'e': msg('\n{}\n{}\n{}'.format(self.fmt_display,prompt,p)) - elif reply == 'g': self.group = not self.group + elif reply == 'g': + if self.can_group: + self.group = not self.group elif reply == 'l': idx,lbl = self.get_idx_and_label_from_user() if idx: