From 50e2e9b486d9e07f5a9cc9c2d994c3e46dee71b0 Mon Sep 17 00:00:00 2001 From: MMGen Date: Fri, 25 May 2018 14:28:12 +0000 Subject: [PATCH] EthereumTrackingWallet: addrimport,listaddresses,add_label --- mmgen/addr.py | 6 ++ mmgen/altcoins/__init__.py | 0 mmgen/altcoins/eth/__init__.py | 0 mmgen/altcoins/eth/tw.py | 137 +++++++++++++++++++++++++++++++++ mmgen/main_addrimport.py | 28 +++++-- mmgen/obj.py | 30 +++++++- mmgen/opts.py | 1 + mmgen/protocol.py | 3 +- mmgen/tw.py | 24 ++++-- mmgen/util.py | 1 + 10 files changed, 215 insertions(+), 15 deletions(-) create mode 100755 mmgen/altcoins/__init__.py create mode 100755 mmgen/altcoins/eth/__init__.py create mode 100755 mmgen/altcoins/eth/tw.py diff --git a/mmgen/addr.py b/mmgen/addr.py index 5c995728..5ee2e3da 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -862,6 +862,12 @@ re-import your addresses. """.strip().format(pnm=pnm) } + def __new__(cls,source=None): + if g.coin == 'ETH': + from mmgen.altcoins.eth.tw import EthereumAddrData + cls = EthereumAddrData + return MMGenObject.__new__(cls,source) + def __init__(self,source=None): self.al_ids = {} if source == 'tw': self.add_tw_data() diff --git a/mmgen/altcoins/__init__.py b/mmgen/altcoins/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/mmgen/altcoins/eth/__init__.py b/mmgen/altcoins/eth/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/mmgen/altcoins/eth/tw.py b/mmgen/altcoins/eth/tw.py new file mode 100755 index 00000000..82660a11 --- /dev/null +++ b/mmgen/altcoins/eth/tw.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2018 The MMGen Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +altcoins.eth.tw: ETH tracking wallet functions and methods for the MMGen suite +""" + +from mmgen.common import * +from mmgen.obj import * +import json + +# No file locking - 2 processes accessing the wallet at the same time will corrupt it +class EthereumTrackingWallet(object): + + data_dir = os.path.join(g.altcoin_data_dir,'eth') + tw_file = os.path.join(data_dir,'tracking-wallet.json') + + def __init__(self): + check_or_create_dir(self.data_dir) + try: + self.data = json.loads(get_data_from_file(self.tw_file,silent=True)) + except: + try: os.stat(self.tw_file) + except: self.data = {} + else: die(2,"File '{}' exists but does not contain valid json data") + else: + for d in self.data: + self.data[d]['mmid'] = TwMMGenID(self.data[d]['mmid'],on_fail='raise') + self.data[d]['comment'] = TwComment(self.data[d]['comment'],on_fail='raise') + + def import_address(self,addr,label): + if addr in self.data: + if not self.data[addr]['mmid'] and label.mmid: + msg("Warning: MMGen ID '{}' was missing in tracking wallet!".format(label.mmid)) + elif self.data[addr]['mmid'] != label.mmid: + die(3,"MMGen ID '{}' does not match tracking wallet!".format(label.mmid)) + self.data[addr] = { 'mmid': label.mmid, 'comment': label.comment } + + def write(self): + write_data_to_file( + self.tw_file, + json.dumps(self.data), + 'Ethereum address data', + ask_overwrite=False, + silent=True) + + def delete_all(self): + self.data = {} + self.write() + + def delete(self,addr): + if is_coin_addr(addr): + have_match = lambda k: k == addr + elif is_mmgen_id(addr): + have_match = lambda k: self.data[k]['mmid'] == addr + else: + die(1,"'{}' is not an Ethereum address or MMGen ID".format(addr)) + + for k in self.data: + if have_match(k): + del self.data[k] + break + else: + die(1,"Address '{}' not found in tracking wallet".format(addr)) + self.write() + + def sorted_list(self): + return sorted( + map(lambda x: {'addr':x[0], 'mmid':x[1]['mmid'], 'comment':x[1]['comment'] }, self.data.items()), + key=lambda x: x['mmid'].sort_key+x['addr'] + ) + + def mmid_ordered_dict(self): + from collections import OrderedDict + return OrderedDict(map(lambda x: (x['mmid'],{'addr':x['addr'],'comment':x['comment']}), self.sorted_list())) + + @classmethod + def import_label(cls,coinaddr,lbl): + tw = cls() + for addr,d in tw.data.items(): + if addr == coinaddr: + d['comment'] = lbl.comment + tw.write() + return None + else: # emulate RPC library + return ('rpcfail',(None,2,"Address '{}' not found in tracking wallet".format(coinaddr))) + + +from mmgen.tw import TwAddrList +class EthereumTwAddrList(TwAddrList): + + def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels): + tw = EthereumTrackingWallet().mmid_ordered_dict() + self.total = g.proto.coin_amt('0') + + rpc_init() +# cur_blk = int(g.rpch.eth_blockNumber(),16) + + for mmid,d in tw.items(): +# if d['confirmations'] < minconf: continue + label = TwLabel(mmid+' '+d['comment'],on_fail='raise') + if usr_addr_list and (label.mmid not in usr_addr_list): continue + bal = ETHAmt(int(g.rpch.eth_getBalance('0x'+d['addr']),16),fromWei=True) + if bal == 0 and not showempty: + if not label.comment: continue + if not all_labels: continue + self[label.mmid] = {'amt': g.proto.coin_amt('0'), 'lbl': label } + if showbtcaddrs: + self[label.mmid]['addr'] = CoinAddr(d['addr']) + self[label.mmid]['lbl'].mmid.confs = 9999 # TODO + self[label.mmid]['amt'] += bal + self.total += bal + +from mmgen.addr import AddrData +class EthereumAddrData(AddrData): + + @classmethod + def get_tw_data(cls): + vmsg('Getting address data from tracking wallet') + tw = EthereumTrackingWallet().mmid_ordered_dict() + # emulate the output of RPC 'listaccounts' and 'getaddressesbyaccount' + return [(mmid+' '+d['comment'],[d['addr']]) for mmid,d in tw.items()] diff --git a/mmgen/main_addrimport.py b/mmgen/main_addrimport.py index 19ba5311..4fdaa852 100755 --- a/mmgen/main_addrimport.py +++ b/mmgen/main_addrimport.py @@ -101,16 +101,25 @@ else: m = ' from Seed ID {}'.format(al.al_id.sid) if hasattr(al.al_id,'sid') else '' qmsg('OK. {} addresses{}'.format(al.num_addrs,m)) -if not opt.quiet: confirm_or_exit(ai_msgs('rescan'),'continue',expect='YES') - err_flag = False -def import_address(addr,label,rescan): - try: - g.rpch.importaddress(addr,label,rescan,timeout=(False,3600)[rescan]) - except: - global err_flag - err_flag = True +if g.coin == 'ETH': + if opt.rescan: + die('--rescan option meaningless for coin {}'.format(g.coin)) + from mmgen.altcoins.eth.tw import EthereumTrackingWallet + eth_tw = EthereumTrackingWallet() + + def import_address(addr,label,rescan): + eth_tw.import_address(addr,label) +else: + if not opt.quiet: confirm_or_exit(ai_msgs('rescan'),'continue',expect='YES') + + def import_address(addr,label,rescan): + try: + g.rpch.importaddress(addr,label,rescan,timeout=(False,3600)[rescan]) + except: + global err_flag + err_flag = True w_n_of_m = len(str(al.num_addrs)) * 2 + 2 w_mmid = 1 if opt.addrlist or opt.address else len(str(max(al.idxs()))) + 13 @@ -168,3 +177,6 @@ for n,e in enumerate(al.data): if opt.batch: ret = g.rpch.importaddress(arg_list,batch=True) msg('OK: {} addresses imported'.format(len(ret))) + +if g.coin == 'ETH': + eth_tw.write() diff --git a/mmgen/obj.py b/mmgen/obj.py index 698d9c2c..6d677278 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -369,6 +369,25 @@ class BTCAmt(Decimal,Hilite,InitErrors): class BCHAmt(BTCAmt): pass class B2XAmt(BTCAmt): pass class LTCAmt(BTCAmt): max_amt = 84000000 +class ETHAmt(BTCAmt): + max_prec = 18 + max_amt = 999999999 # TODO + min_coin_unit = Decimal('0.000000000000000001') # wei + + def __new__(cls,num,on_fail='die',fromWei=False): + if type(num) == cls: return num + cls.arg_chk(cls,on_fail) + try: + if fromWei: + assert type(num) in (int,long),'value is not an integer or long integer' + return super(cls,cls).__new__(cls,num * cls.min_coin_unit) + return super(cls,cls).__new__(cls,num) + except Exception as e: + m = "{!r}: value cannot be converted to {} ({})" + return cls.init_fail(m.format(num,cls.__name__,e[0]),on_fail) + + def toWei(self): + return int(Decimal(self) / self.min_coin_unit) class CoinAddr(str,Hilite,InitErrors,MMGenObject): color = 'cyan' @@ -403,13 +422,16 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject): def is_for_chain(self,chain): + from mmgen.globalvars import g + if g.coin in ('ETH','ETC'): + return True + def pfx_ok(pfx): if type(pfx) == tuple: if self[0] in pfx: return True elif self[:len(pfx)] == pfx: return True return False - from mmgen.globalvars import g proto = g.proto.get_protocol_by_chain(chain) vn = proto.addr_ver_num @@ -421,6 +443,12 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject): return pfx_ok(vn[self.addr_fmt][1]) def is_in_tracking_wallet(self): + + from mmgen.globalvars import g + if g.coin in ('ETH','ETC'): + from mmgen.altcoins.eth.tw import EthereumTrackingWallet + return self in EthereumTrackingWallet().data.keys() + from mmgen.rpc import rpc_init d = rpc_init().validateaddress(self) return d['iswatchonly'] and 'account' in d diff --git a/mmgen/opts.py b/mmgen/opts.py index 52920dc5..f4a6b2b7 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -331,6 +331,7 @@ def init(opts_f,add_opts=[],opt_filter=None): for k in ('prog_name','desc','usage','options','notes'): if k in opts_data: del opts_data[k] + g.altcoin_data_dir = os.path.join(g.data_dir,'altcoins') warn_altcoins(altcoin_trust_level) return args diff --git a/mmgen/protocol.py b/mmgen/protocol.py index 77bbe608..0e3f2bd9 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -23,7 +23,7 @@ protocol.py: Coin protocol functions, classes and methods import sys,os,hashlib from binascii import unhexlify from mmgen.util import msg,pmsg,Msg,pdie -from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt,B2XAmt +from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt,B2XAmt,ETHAmt from mmgen.globalvars import g import mmgen.bech32 as bech32 @@ -297,6 +297,7 @@ class EthereumProtocol(DummyWIF,BitcoinProtocolAddrgen): daemon_name = 'parity' rpc_port = 8545 mmcaps = ('key','addr','rpc') + coin_amt = ETHAmt @classmethod def verify_addr(cls,addr,hex_width,return_dict=False): diff --git a/mmgen/tw.py b/mmgen/tw.py index d36891e2..77eb3095 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -312,6 +312,14 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen self.display() msg(prompt) + @classmethod + def import_label(cls,coinaddr,lbl): + # NOTE: this works because importaddress() removes the old account before + # associating the new account with the address. + # Will be replaced by setlabel() with new RPC label API + # RPC args: addr,label,rescan[=true],p2sh[=none] + return g.rpch.importaddress(coinaddr,lbl,False,on_fail='return') + # returns on failure @classmethod def add_label(cls,arg1,label='',addr=None,silent=False,on_fail='return'): @@ -350,11 +358,11 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen lbl = TwLabel(mmaddr + ('',' '+cmt)[bool(cmt)],on_fail=on_fail) - # NOTE: this works because importaddress() removes the old account before - # associating the new account with the address. - # Will be replaced by setlabel() with new RPC label API - # RPC args: addr,label,rescan[=true],p2sh[=none] - ret = g.rpch.importaddress(coinaddr,lbl,False,on_fail='return') + if g.coin == 'ETH': + from mmgen.altcoins.eth.tw import EthereumTrackingWallet + cls = EthereumTrackingWallet + + ret = cls.import_label(coinaddr,lbl) from mmgen.rpc import rpc_error,rpc_errmsg if rpc_error(ret): @@ -375,6 +383,12 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen class TwAddrList(MMGenDict): + def __new__(cls,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels): + if g.coin == 'ETH': + from mmgen.altcoins.eth.tw import EthereumTwAddrList + cls = EthereumTwAddrList + return MMGenDict.__new__(cls,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels) + def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels): def check_dup_mmid(acct_labels): diff --git a/mmgen/util.py b/mmgen/util.py index 4b92221e..9ba39827 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -846,6 +846,7 @@ def rpc_init(reinit=False): auth=False) if not g.daemon_version: # First call g.daemon_version = conn.parity_versionInfo()['version'] # fail immediately if daemon is geth + g.chain = conn.parity_chain() else: cfg = get_daemon_cfg_options(('rpcuser','rpcpassword')) conn = mmgen.rpc.CoinDaemonRPCConnection(