From 72aef8b02fd2fc62c1bf6f153f9159330cff1bbf Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 11 Jun 2022 16:11:02 +0000 Subject: [PATCH] mmgen-tool twimport,twexport: support Ethereum --- mmgen/base_proto/ethereum/tw/ctl.py | 6 +- mmgen/base_proto/ethereum/tw/json.py | 145 +++++++++++++++++++++++++++ mmgen/data/version | 2 +- test/test_py_d/ts_ethdev.py | 60 +++++++++++ 4 files changed, 209 insertions(+), 4 deletions(-) create mode 100755 mmgen/base_proto/ethereum/tw/json.py diff --git a/mmgen/base_proto/ethereum/tw/ctl.py b/mmgen/base_proto/ethereum/tw/ctl.py index 07fba160..56c799e7 100755 --- a/mmgen/base_proto/ethereum/tw/ctl.py +++ b/mmgen/base_proto/ethereum/tw/ctl.py @@ -91,6 +91,9 @@ class EthereumTrackingWallet(TrackingWallet): async def batch_import_address(self,args_list): return [await self.import_address(*a) for a in args_list] + async def rescan_addresses(self,coin_addrs): + pass + @write_mode async def import_address(self,addr,label): r = self.data_root @@ -229,6 +232,3 @@ class EthereumTokenTrackingWallet(EthereumTrackingWallet): 'decimals': t.decimals } } - - async def twimport_check_and_create_wallet(self,info_msg): - raise NotImplementedError('method not implemented for Ethereum') diff --git a/mmgen/base_proto/ethereum/tw/json.py b/mmgen/base_proto/ethereum/tw/json.py new file mode 100755 index 00000000..63d2faf4 --- /dev/null +++ b/mmgen/base_proto/ethereum/tw/json.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +base_proto.ethereum.tw.json: export and import tracking wallet to JSON format +""" + +from collections import namedtuple +from ....tw.json import TwJSON +from ....tw.common import TwMMGenID + +class EthereumTwJSON(TwJSON): + + class Base(TwJSON.Base): + + def __init__(self,proto,*args,**kwargs): + + self.params_keys = ['symbol','decimals'] + self.params_tuple = namedtuple('params_tuple',self.params_keys) + + super().__init__(proto,*args,**kwargs) + + @property + def mappings_json(self): + + def gen_mappings(data): + for d in data: + yield (d.mmgen_id,d.address) if hasattr(d,'mmgen_id') else d + + return self.json_dump({ + 'accounts': list(gen_mappings(self.entries['accounts'])), + 'tokens': {k:list(gen_mappings(v)) for k,v in self.entries['tokens'].items()} + }) + + @property + def num_entries(self): + return len(self.entries['accounts']) + len(self.entries['tokens']) + + class Import(TwJSON.Import,Base): + + info_msg = """ + This utility will recreate a new tracking wallet from the supplied JSON dump. + If the dump contains address balances, balances will be updated from it. + """ + + @property + async def tracking_wallet_exists(self): + return bool(self.tw.data['accounts'] or self.tw.data['tokens']) + + async def create_tracking_wallet(self): + return True + + async def get_entries(self): + + edata = self.data['data']['entries'] + + def gen_entries(data): + for d in data: + if len(d) == 2: + yield self.params_tuple(*d) + else: + e = self.entry_tuple_in(*d) + yield self.entry_tuple( + TwMMGenID(self.proto,e.mmgen_id), + e.address, + getattr(e,'amount','0'), + e.comment ) + + def gen_token_entries(): + for token_addr,token_data in edata['tokens'].items(): + yield ( + token_addr, + list(gen_entries(token_data)), + ) + + return { + 'accounts': list(gen_entries( edata['accounts'] )), + 'tokens': dict(list(gen_token_entries())) + } + + async def do_import(self,batch): + + def gen_data(data): + for d in data: + if hasattr(d,'address'): + if d.amount is None: # Python 3.9: {} | {} + yield (d.address, {'mmid':d.mmgen_id,'comment':d.comment}) + else: + yield (d.address, {'mmid':d.mmgen_id,'comment':d.comment,'balance':d.amount}) + else: + yield ('params', {'symbol':d.symbol,'decimals':d.decimals}) + + self.tw.data = { # keys must be in correct order + 'coin': self.coin.upper(), + 'network': self.network.upper(), + 'accounts': dict(gen_data(self.entries['accounts'])), + 'tokens': {k:dict(gen_data(v)) for k,v in self.entries['tokens'].items()}, + } + self.tw.write(quiet=False) + + class Export(TwJSON.Export,Base): + + async def get_entries(self,include_amts=True): + + def gen_data(data): + for k,v in data.items(): + if k == 'params': + yield self.params_tuple(**v) + elif include_amts: + yield self.entry_tuple(TwMMGenID(self.proto,v['mmid']), k, v.get('balance'), v['comment']) + else: + yield self.entry_tuple_in(TwMMGenID(self.proto,v['mmid']), k, v['comment']) + + def gen_token_data(): + for token_addr,token_data in self.tw.data['tokens'].items(): + yield ( + token_addr, + sorted( + gen_data(token_data), + key = lambda x: x.mmgen_id.sort_key if hasattr(x,'mmgen_id') else '+' + ) + ) + + return { + 'accounts': sorted( + gen_data(self.tw.data['accounts']), + key = lambda x: x.mmgen_id.sort_key ), + 'tokens': dict(sorted(gen_token_data())) + } + + @property + async def entries_out(self): + return await self.get_entries(include_amts='amount' in self.keys) + + @property + async def total(self): + from ....amt import ETHAmt + return sum(ETHAmt(i.amount) for i in self.entries['accounts']) or ETHAmt('0') diff --git a/mmgen/data/version b/mmgen/data/version index 49a804d9..5f22f526 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -13.2.dev6 +13.2.dev7 diff --git a/test/test_py_d/ts_ethdev.py b/test/test_py_d/ts_ethdev.py index 917a5872..28dca6df 100755 --- a/test/test_py_d/ts_ethdev.py +++ b/test/test_py_d/ts_ethdev.py @@ -307,6 +307,18 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): ('token_remove_addr1', f'removing addr #{del_addrs[0]} from {coin} token tracking wallet'), ('token_remove_addr2', f'removing addr #{del_addrs[1]} from {coin} token tracking wallet'), + ('twexport_noamt', 'exporting the tracking wallet (include_amts=0)'), + ('twmove', 'moving the tracking wallet'), + ('twimport', 'importing the tracking wallet'), + ('twview7', 'twview (cached_balances=1)'), + ('twview8', 'twview'), + ('twexport', 'exporting the tracking wallet'), + ('tw_chktotal', 'checking total value in tracking wallet dump'), + ('twmove', 'moving the tracking wallet'), + ('twimport', 'importing the tracking wallet'), + ('twcompare', 'comparing imported tracking wallet with original'), + ('twview9', 'twview (check balance)'), + ('stop', 'stopping daemon'), ) @@ -1205,6 +1217,12 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): return self.twview(tool_args=['wide=1','minconf=0']) def twview6(self): return self.twview(expect_str=vbal6) + def twview7(self): + return self.twview(args=['--cached-balances']) + def twview8(self): + return self.twview() + def twview9(self): + return self.twview(args=['--cached-balances'],expect_str=vbal6) def token_twview1(self): return self.twview(args=['--token=mm1']) @@ -1248,6 +1266,48 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): def token_remove_addr2(self): return self.edit_label(out_num=del_addrs[1],args=['--token=mm1'],action='D') + def twexport_noamt(self): + return self.twexport(add_args=['include_amts=0']) + + def twexport(self,add_args=[]): + t = self.spawn('mmgen-tool', self.eth_args + ['twexport'] + add_args) + t.written_to_file('JSON data') + return t + + async def twmove(self): + self.spawn('',msg_only=True) + from mmgen.tw.ctl import TrackingWallet + tw = await TrackingWallet(self.proto) + imsg(f'Moving tracking wallet') + os.rename( tw.tw_fn, tw.tw_fn+'.bak.json' ) + return 'ok' + + def twimport(self,add_args=[]): + from mmgen.tw.json import TwJSON + fn = joinpath( self.tmpdir, TwJSON.Base(self.proto).dump_fn ) + t = self.spawn('mmgen-tool',self.eth_args + ['twimport',fn] + add_args) + t.expect('(y/N): ','y') + t.written_to_file('tracking wallet data') + return t + + def tw_chktotal(self): + self.spawn('',msg_only=True) + from mmgen.tw.json import TwJSON + fn = joinpath( self.tmpdir, TwJSON.Base(self.proto).dump_fn ) + res = json.loads(read_from_file(fn)) + cmp_or_die( res['data']['value'], vbal6, 'value in tracking wallet JSON dump' ) + return 'ok' + + async def twcompare(self): + self.spawn('',msg_only=True) + from mmgen.tw.ctl import TrackingWallet + tw = await TrackingWallet(self.proto) + fn = tw.tw_fn + imsg(f'Comparing imported tracking wallet with original') + data = [json.dumps(json.loads(read_from_file(f)),sort_keys=True) for f in (fn,fn+'.bak.json')] + cmp_or_die(*data,'tracking wallets') + return 'ok' + def stop(self): self.spawn('',msg_only=True) if not opt.no_daemon_stop: