Browse Source

xmrwallet: `get_info()`, chksum cleanups

The MMGen Project 1 year ago
parent
commit
1a0b674585
4 changed files with 97 additions and 71 deletions
  1. 7 2
      mmgen/help/xmrwallet.py
  2. 7 4
      mmgen/main_xmrwallet.py
  3. 81 63
      mmgen/xmrwallet.py
  4. 2 2
      test/test_py_d/ts_misc.py

+ 7 - 2
mmgen/help/xmrwallet.py

@@ -47,8 +47,7 @@ sweep     - sweep funds in specified wallet:account to new address in same
 relay     - relay a transaction from a transaction file created using ‘sweep’
             or ‘transfer’ with the --no-relay option
 submit    - submit an autosigned transaction to a wallet and the network
-txview    - view a transaction file or files created using ‘sweep’ or
-            ‘transfer’ with the --no-relay option
+txview    - display detailed information about a transaction file or files
 dump      - produce JSON dumps of wallet metadata (accounts, addresses and
             labels) for a list or range of wallets
 restore   - same as ‘create’, but additionally restore wallet metadata from
@@ -135,6 +134,12 @@ wallets, creating the dumps is as easy as executing ‘mmgen-xmrwallet
 JSON and thus suitable for efficient incremental backup using git.
 
 
+                          ‘TXVIEW’ OPERATION NOTES
+
+Transactions are displayed in chronological order based on submit time or
+creation time.
+
+
                               SECURITY WARNING
 
 If you have an existing MMGen Monero hot wallet setup, you’re strongly

+ 7 - 4
mmgen/main_xmrwallet.py

@@ -61,9 +61,9 @@ opts_data = {
                                  ‘export-outputs’ operation
 -a, --autosign                   Use appropriate outdir and other params for
                                  autosigning operations (implies --watch-only).
-                                 When this option is in effect, the viewkey-
-                                 address file is located automatically, so the
-                                 xmr_keyaddrfile argument must be omitted.
+                                 When this option is in effect, filename argu-
+                                 ments must be omitted, as files are located
+                                 automatically.
 -m, --autosign-mountpoint=P      Specify the autosign mountpoint (defaults to
                                  ‘/mnt/mmgen_autosign’, implies --autosign)
 -b, --rescan-blockchain          Rescan the blockchain if wallet fails to sync
@@ -111,7 +111,10 @@ cfg = Config(opts_data=opts_data)
 cmd_args = cfg._args
 
 if cmd_args and cfg.autosign and (
-		cmd_args[0] in (MoneroWalletOps.kafile_arg_ops + ('export-outputs','import-key-images'))
+		cmd_args[0] in (
+			MoneroWalletOps.kafile_arg_ops
+			+ ('export-outputs','import-key-images')
+		)
 		or len(cmd_args) == 1 and cmd_args[0] == 'submit'
 	):
 	cmd_args.insert(1,None)

+ 81 - 63
mmgen/xmrwallet.py

@@ -43,6 +43,7 @@ from .util import (
 	make_timestr,
 	make_chksum_N,
 	capfirst,
+	list_gen,
 )
 from .fileutil import get_data_from_file
 from .seed import SeedID
@@ -119,6 +120,8 @@ def is_xmr_tx_file(cfg,fn):
 
 class MoneroMMGenFile:
 
+	silent_load = False
+
 	def make_chksum(self,keys=None):
 		res = json.dumps(
 			dict( (k,v) for k,v in self.data._asdict().items() if (not keys or k in keys) ),
@@ -157,15 +160,36 @@ class MoneroMMGenFile:
 		)
 
 	def extract_data_from_file(self,cfg,fn):
-		return json.loads( get_data_from_file( cfg, str(fn), self.desc ))[self.data_label]
+		return json.loads(
+			get_data_from_file( cfg, str(fn), self.desc, silent=self.silent_load )
+		)[self.data_label]
 
 class MoneroMMGenTX:
 
 	class Base(MoneroMMGenFile):
 
 		data_label = 'MoneroMMGenTX'
-		base_chksum_fields = ('op','create_time','network','seed_id','source','dest','amount')
-		full_chksum_fields = ('op','create_time','network','seed_id','source','dest','amount','fee','blob')
+
+		# both base_chksum and full_chksum are used to make the filename stem, so we must not include
+		# fields that change when TX is signed and submitted (e.g. ‘sign_time’)
+		base_chksum_fields = {
+			'op',
+			'create_time',
+			'network',
+			'seed_id',
+			'source',
+			'dest',
+			'amount' }
+		full_chksum_fields = {
+			'op',
+			'create_time',
+			'network',
+			'seed_id',
+			'source',
+			'dest',
+			'amount',
+			'fee',
+			'blob' }
 		chksum_nchars = 6
 		xmrwallet_tx_data = namedtuple('xmrwallet_tx_data',[
 			'op',
@@ -195,66 +219,58 @@ class MoneroMMGenTX:
 
 		def get_info(self,indent=''):
 			d = self.data
-			if d.dest:
-				to_entry = f'\n{indent}  To:      ' + (
-					'Wallet {}, account {}, address {}'.format(
-						d.dest.wallet.hl(),
-						red(f'#{d.dest.account}'),
-						red(f'#{d.dest.account_address}')
-					)
-				)
-
-			fs = """
-				Info for transaction {a} [Seed ID: {b}. Network: {c}]:
-				  TxID:    {d}
-				  Created: {e:19} [{f}]
-				  Signed:  {g:19} [{h}]
-				  Type:    {i}
-				  From:    Wallet {j}, account {k}{l}
-				  Amount:  {m} XMR
-				  Fee:     {n} XMR
-				  Dest:    {o}
-			"""
-
-			pmid = d.dest_address.parsed.payment_id
-			if pmid:
-				fs += '  Payment ID: {pmid}'
-
-			coldsign_status = (
-				pink(' [cold signed{}]'.format(', submitted' if d.complete else ''))
-				if d.signed_txset else '' )
+			pmt_id = d.dest_address.parsed.payment_id
+			fs = '\n'.join(list_gen(
+				['Info for transaction {a} [Seed ID: {b}. Network: {c}]:'],
+				['  TxID:      {d}'],
+				['  Created:   {e:19} [{f}]'],
+				['  Signed:    {g:19} [{h}]', d.sign_time],
+				['  Type:      {i}{S}'],
+				['  From:      Wallet {j}, account {k}'],
+				['  To:        Wallet {x}, account {y}, address {z}', d.dest],
+				['  Amount:    {m} XMR'],
+				['  Fee:       {n} XMR'],
+				['  Dest:      {o}'],
+				['  Payment ID: {P}', pmt_id],
+			))
 
 			from .util2 import format_elapsed_hr
 			return fmt(fs,strip_char='\t',indent=indent).format(
-					a = orange(self.base_chksum.upper()),
+					a = orange(self.file_id),
 					b = d.seed_id.hl(),
 					c = yellow(d.network.upper()),
 					d = d.txid.hl(),
 					e = make_timestr(d.create_time),
 					f = format_elapsed_hr(d.create_time),
-					g = make_timestr(d.sign_time) if d.sign_time else '-',
-					h = format_elapsed_hr(d.sign_time) if d.sign_time else '-',
-					i = blue(capfirst(d.op)) + coldsign_status,
+					g = make_timestr(d.sign_time) if d.sign_time else None,
+					h = format_elapsed_hr(d.sign_time) if d.sign_time else None,
+					i = blue(capfirst(d.op)),
 					j = d.source.wallet.hl(),
 					k = red(f'#{d.source.account}'),
-					l = to_entry if d.dest else '',
 					m = d.amount.hl(),
 					n = d.fee.hl(),
 					o = d.dest_address.hl(),
-					pmid = pink(pmid.hex()) if pmid else None
+					P = pink(pmt_id.hex()) if pmt_id else None,
+					S = pink(f" [cold signed{', submitted' if d.complete else ''}]") if d.signed_txset else '',
+					x = d.dest.wallet.hl() if d.dest else None,
+					y = red(f'#{d.dest.account}') if d.dest else None,
+					z = red(f'#{d.dest.account_address}') if d.dest else None,
 				)
 
+		@property
+		def file_id(self):
+			return (self.base_chksum + ('-' + self.full_chksum if self.full_chksum else '')).upper()
+
 		def write(self,delete_metadata=False,ask_write=True,ask_overwrite=True):
 			dict_data = self.data._asdict()
 			if delete_metadata:
 				dict_data['metadata'] = None
 
-			fn = '{a}{b}-XMR[{c!s}]{d}.{e}'.format(
-				a = self.base_chksum.upper(),
-				b = (lambda s: f'-{s.upper()}' if s else '')(self.full_chksum),
-				c = self.data.amount,
-				d = (lambda s: '' if s == 'mainnet' else f'.{s}')(self.data.network),
-				e = self.ext
+			fn = '{a}-XMR[{b!s}]{c}.{d}'.format(
+				a = self.file_id,
+				b = self.data.amount,
+				c = (lambda s: '' if s == 'mainnet' else f'.{s}')(self.data.network),
+				d = self.ext
 			)
 
 			if self.cfg.autosign:
@@ -294,8 +310,8 @@ class MoneroMMGenTX:
 
 			self.data = self.xmrwallet_tx_data(
 				op             = d.op,
-				create_time    = getattr(d,'create_time',now),
-				sign_time      = (getattr(d,'sign_time',None) or now) if self.signed else None,
+				create_time    = now if self.name in ('NewSigned','NewUnsigned') else getattr(d,'create_time',None),
+				sign_time      = now if self.name in ('NewSigned','NewColdSigned') else getattr(d,'sign_time',None),
 				network        = d.network,
 				seed_id        = SeedID(sid=d.seed_id),
 				source         = XMRWalletAddrSpec(d.source),
@@ -308,7 +324,7 @@ class MoneroMMGenTX:
 				metadata       = d.metadata,
 				unsigned_txset = d.unsigned_txset,
 				signed_txset   = getattr(d,'signed_txset',None),
-				complete       = True if self.name == 'NewSigned' else getattr(d,'complete',False),
+				complete       = self.name in ('NewSigned','NewSubmitted'),
 			)
 
 	class NewUnsigned(New):
@@ -337,17 +353,17 @@ class MoneroMMGenTX:
 			super().__init__()
 
 			self.cfg = cfg
-			self.fn = fn
+			self.fn = Path(fn)
 
 			try:
 				d_wrap = self.extract_data_from_file( cfg, fn )
 			except Exception as e:
 				die( 'MoneroMMGenTXFileParseError', f'{type(e).__name__}: {e}\nCould not load transaction file' )
 
-			if not 'unsigned_txset' in d_wrap['data']: # backwards compat: use old checksum fields
-				self.full_chksum_fields = (
-					set(self.xmrwallet_tx_data._fields) -
-					{'metadata','unsigned_txset','signed_txset','complete'} )
+			if 'unsigned_txset' in d_wrap['data']: # post-autosign
+				self.full_chksum_fields &= set(d_wrap['data']) # allow for added chksum fields in future
+			else:
+				self.full_chksum_fields = set(d_wrap['data']) - {'metadata'}
 
 			for key in self.xmrwallet_tx_data._fields: # backwards compat: fill in missing fields
 				if not key in d_wrap['data']:
@@ -355,7 +371,7 @@ class MoneroMMGenTX:
 
 			d = self.xmrwallet_tx_data(**d_wrap['data'])
 
-			if self.name != 'Completed':
+			if self.name not in ('View','Completed'):
 				assert fn.name.endswith('.'+self.ext), 'TX filename {fn} has incorrect extension (not {self.ext!r})'
 				assert getattr(d,self.req_field), f'{self.name} TX missing required field {self.req_field!r}'
 				assert bool(d.sign_time)==self.signed,'{} has {}sign time!'.format(self.desc,'no 'if self.signed else'')
@@ -407,14 +423,17 @@ class MoneroMMGenTX:
 		desc = 'submitted transaction'
 		ext = 'subtx'
 
+	class View(Completed):
+		silent_load = True
+
 class MoneroWalletOutputsFile:
 
 	class Base(MoneroMMGenFile):
 
 		desc = 'wallet outputs'
 		data_label = 'MoneroMMGenWalletOutputsFile'
-		base_chksum_fields = ('seed_id','wallet_index','outputs_data_hex',)
-		full_chksum_fields = ('seed_id','wallet_index','outputs_data_hex','signed_key_images')
+		base_chksum_fields = {'seed_id','wallet_index','outputs_data_hex',}
+		full_chksum_fields = {'seed_id','wallet_index','outputs_data_hex','signed_key_images'}
 		fn_fs = '{a}-outputs-{b}.{c}'
 		ext_offset = 25 # len('-outputs-') + len(chksum) ({b})
 		chksum_nchars = 16
@@ -529,7 +548,7 @@ class MoneroWalletDumpFile:
 	class Base:
 		desc = 'Monero wallet dump'
 		data_label = 'MoneroMMGenWalletDumpFile'
-		base_chksum_fields = ('seed_id','wallet_index','wallet_metadata')
+		base_chksum_fields = {'seed_id','wallet_index','wallet_metadata'}
 		full_chksum_fields = None
 		ext = 'dump'
 		ext_offset = 0
@@ -833,7 +852,7 @@ class MoneroWalletOps:
 					c = 'WatchOnly' if watch_only else '',
 					d = f'.{self.cfg.network}' if self.cfg.network != 'mainnet' else '')
 			)
-		
+
 		@property
 		def add_wallet_desc(self):
 			return 'offline signing ' if self.offline else 'watch-only ' if self.cfg.watch_only else ''
@@ -1644,7 +1663,6 @@ class MoneroWalletOps:
 
 			new_tx = MoneroMMGenTX.NewSubmitted(
 				cfg          = self.cfg,
-				complete     = True,
 				_in_tx       = tx,
 			)
 			gmsg('\nOK')
@@ -1796,10 +1814,10 @@ class MoneroWalletOps:
 	class txview(base):
 
 		async def main(self):
+			txs = sorted(
+				(MoneroMMGenTX.View( self.cfg, Path(fn) ) for fn in uarg.infile),
+					key = lambda x: x.data.create_time
+			)
 			self.cfg._util.stdout_or_pager(
-				'\n'.join(
-					tx.get_info() for tx in
-					sorted(
-						(MoneroMMGenTX.Completed( self.cfg, Path(fn) ) for fn in uarg.infile),
-						key = lambda x: x.data.sign_time or x.data.create_time )
-			))
+				'\n'.join(tx.get_info() for tx in txs)
+			)

+ 2 - 2
test/test_py_d/ts_misc.py

@@ -51,8 +51,8 @@ class TestSuiteMisc(TestSuiteBase):
 		t = self.spawn(f'mmgen-xmrwallet',['txview','test/ref/monero/3EBD06-2D6E3B-XMR[0.74].testnet.sigtx'])
 		res = strip_ansi_escapes(t.read()).replace('\r','')
 		for s in (
-			'Amount:  0.74 XMR',
-			'Dest:    56VQ9M6k',
+			'Amount:    0.74 XMR',
+			'Dest:      56VQ9M6k',
 		):
 			assert s in res, f'{s} not in {res}'
 		return t