Browse Source

mmgen-tool twimport,twexport: support Ethereum

The MMGen Project 2 years ago
parent
commit
72aef8b02f

+ 3 - 3
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')

+ 145 - 0
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 <mmgen@tuta.io>
+# 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')

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-13.2.dev6
+13.2.dev7

+ 60 - 0
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: