2 Commits 6e728b7f4d ... 4ffe5c48d2

Author SHA1 Message Date
  The MMGen Project 4ffe5c48d2 tx.file: new transaction file format 1 month ago
  The MMGen Project a38f4e4fd9 tx.file: cleanups; other minor cleanups 1 month ago

+ 4 - 2
mmgen/cfg.py

@@ -492,8 +492,10 @@ class Config(Lockable):
 		#         class attribute, if it exists:
 		auto_opts = tuple(self._autoset_opts) + tuple(self._auto_typeset_opts)
 		for key,val in self._uopts.items():
-			assert key.isascii() and key.isidentifier() and key[0] != '_', '{key!r}: malformed configuration option'
-			assert key not in self._forbidden_opts, '{key!r}: forbidden configuration option'
+			assert key.isascii() and key.isidentifier() and key[0] != '_', (
+				f'{key!r}: malformed configuration option')
+			assert key not in self._forbidden_opts, (
+				f'{key!r}: forbidden configuration option')
 			if key not in auto_opts:
 				if hasattr(type(self), key):
 					setattr(

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev4
+15.1.dev5

+ 1 - 0
mmgen/tx/base.py

@@ -92,6 +92,7 @@ class Base(MMGenObject):
 		Non-{gc.proj_name} addresses found in inputs:
 		    {{}}
 	"""
+	file_format = 'json'
 
 	class Input(MMGenTxIO):
 		scriptPubKey = ListItemAttr(HexStr)

+ 158 - 78
mmgen/tx/file.py

@@ -19,58 +19,122 @@
 """
 tx.file: Transaction file operations for the MMGen suite
 """
-import os
+
+import os, json
 
 from ..util import ymsg,make_chksum_6,die
 from ..obj import MMGenObject,HexStr,MMGenTxID,CoinTxID,MMGenTxComment
+from ..rpc import json_encoder
+
+def json_dumps(data):
+	return json.dumps(data, separators = (',', ':'), cls=json_encoder)
+
+def get_proto_from_coin_id(tx, coin_id, chain):
+	coin, tokensym = coin_id.split(':') if ':' in coin_id else (coin_id, None)
+
+	from ..protocol import CoinProtocol, init_proto
+	network = CoinProtocol.Base.chain_name_to_network(tx.cfg, coin, chain)
+
+	proto = init_proto(tx.cfg, coin, network=network, need_amt=True)
+
+	if tokensym:
+		proto.tokensym = tokensym
+
+	return proto
+
+def eval_io_data(tx, data, desc):
+	if not (desc == 'outputs' and tx.proto.base_coin == 'ETH'): # ETH txs can have no outputs
+		assert len(data), f'no {desc}!'
+	for d in data:
+		d['amt'] = tx.proto.coin_amt(d['amt'])
+	io, io_list = {
+		'inputs':  (tx.Input, tx.InputList),
+		'outputs': (tx.Output, tx.OutputList),
+	}[desc]
+	return io_list(parent=tx, data=[io(tx.proto,**d) for d in data])
 
 class MMGenTxFile(MMGenObject):
+	data_label = 'MMGenTransaction'
+	attrs = {
+		'chain': None,
+		'txid': MMGenTxID,
+		'send_amt': 'skip',
+		'timestamp': None,
+		'blockcount': None,
+		'serialized': None,
+	}
+	extra_attrs = {
+		'locktime': None,
+		'comment': MMGenTxComment,
+		'coin_txid': CoinTxID,
+		'sent_timestamp': None,
+	}
 
 	def __init__(self,tx):
 		self.tx       = tx
-		self.chksum   = None
 		self.fmt_data = None
 		self.filename = None
 
-	def parse(self,infile,metadata_only=False,quiet_open=False):
+	def parse(self, infile, metadata_only=False, quiet_open=False):
+		tx = self.tx
+		from ..fileutil import get_data_from_file
+		data = get_data_from_file(tx.cfg, infile, f'{tx.desc} data', quiet=quiet_open)
+		if len(data) > tx.cfg.max_tx_file_size:
+			die('MaxFileSizeExceeded',
+				f'Transaction file size exceeds limit ({tx.cfg.max_tx_file_size} bytes)')
+		return (self.parse_data_json if data[0] == '{' else self.parse_data_legacy)(data, metadata_only)
+
+	def parse_data_json(self, data, metadata_only):
 		tx = self.tx
+		tx.file_format = 'json'
+		outer_data = json.loads(data)
+		data = outer_data[self.data_label]
+		if outer_data['chksum'] != make_chksum_6(json_dumps(data)):
+			chk = make_chksum_6(json_dumps(data))
+			die(3, f'{self.data_label}: invalid checksum for TxID {data["txid"]} ({chk} != {outer_data["chksum"]})')
+
+		tx.proto = get_proto_from_coin_id(tx, data['coin_id'], data['chain'])
+
+		for k, v in self.attrs.items():
+			if v != 'skip':
+				setattr(tx, k, v(data[k]) if v else data[k])
 
-		def eval_io_data(raw_data,desc):
+		if metadata_only:
+			return
+
+		for k, v in self.extra_attrs.items():
+			if k in data:
+				setattr(tx, k, v(data[k]) if v else data[k])
+
+		for k in ('inputs', 'outputs'):
+			setattr(tx, k, eval_io_data(tx, data[k], k))
+
+		tx.check_txfile_hex_data()
+
+		tx.parse_txfile_serialized_data() # Ethereum RLP or JSON data
+
+		assert tx.proto.coin_amt(data['send_amt']) == tx.send_amt, f'{data["send_amt"]} != {tx.send_amt}'
+
+	def parse_data_legacy(self, data, metadata_only):
+		tx = self.tx
+		tx.file_format = 'legacy'
+
+		def deserialize(raw_data, desc):
 			from ast import literal_eval
 			try:
-				d = literal_eval(raw_data)
+				return literal_eval(raw_data)
 			except:
-				if desc == 'inputs' and not quiet_open:
+				if desc == 'inputs':
 					ymsg('Warning: transaction data appears to be in old format')
 				import re
-				d = literal_eval(re.sub(r"[A-Za-z]+?\(('.+?')\)",r'\1',raw_data))
-			assert isinstance(d,list), f'{desc} data not a list!'
-			if not (desc == 'outputs' and tx.proto.base_coin == 'ETH'): # ETH txs can have no outputs
-				assert len(d), f'no {desc}!'
-			for e in d:
-				e['amt'] = tx.proto.coin_amt(e['amt'])
-				if 'label' in e:
-					e['comment'] = e['label']
-					del e['label']
-			io,io_list = {
-				'inputs':  ( tx.Input, tx.InputList ),
-				'outputs': ( tx.Output, tx.OutputList ),
-			}[desc]
-			return io_list( parent=tx, data=[io(tx.proto,**e) for e in d] )
-
-		from ..fileutil import get_data_from_file
-		tx_data = get_data_from_file( tx.cfg, infile, tx.desc+' data', quiet=quiet_open )
+				return literal_eval(re.sub(r"[A-Za-z]+?\(('.+?')\)",r'\1', raw_data))
 
 		desc = 'data'
 		try:
-			if len(tx_data) > tx.cfg.max_tx_file_size:
-				die('MaxFileSizeExceeded',
-					f'Transaction file size exceeds limit ({tx.cfg.max_tx_file_size} bytes)')
-			tx_data = tx_data.splitlines()
+			tx_data = data.splitlines()
 			assert len(tx_data) >= 5,'number of lines less than 5'
 			assert len(tx_data[0]) == 6,'invalid length of first line'
-			self.chksum = HexStr(tx_data.pop(0))
-			assert self.chksum == make_chksum_6(' '.join(tx_data)),'file data does not match checksum'
+			assert HexStr(tx_data.pop(0)) == make_chksum_6(' '.join(tx_data)), 'file data does not match checksum'
 
 			if len(tx_data) == 7:
 				desc = 'sent timestamp'
@@ -95,7 +159,8 @@ class MMGenTxFile(MMGenObject):
 					tx.comment = MMGenTxComment(comment)
 
 			desc = 'number of lines' # four required lines
-			( metadata, tx.serialized, inputs_data, outputs_data ) = tx_data
+			io_data = {}
+			(metadata, tx.serialized, io_data['inputs'], io_data['outputs']) = tx_data
 			assert len(metadata) < 100,'invalid metadata length' # rough check
 			metadata = metadata.split()
 
@@ -104,19 +169,13 @@ class MMGenTxFile(MMGenObject):
 				tx.locktime = int(metadata.pop()[3:])
 
 			desc = 'coin token in metadata'
-			coin = metadata.pop(0) if len(metadata) == 6 else 'BTC'
-			coin,tokensym = coin.split(':') if ':' in coin else (coin,None)
+			coin_id = metadata.pop(0) if len(metadata) == 6 else 'BTC'
 
 			desc = 'chain token in metadata'
 			tx.chain = metadata.pop(0).lower() if len(metadata) == 5 else 'mainnet'
 
-			from ..protocol import CoinProtocol,init_proto
-			network = CoinProtocol.Base.chain_name_to_network(tx.cfg,coin,tx.chain)
-
-			desc = 'initialization of protocol'
-			tx.proto = init_proto( tx.cfg, coin, network=network, need_amt=True )
-			if tokensym:
-				tx.proto.tokensym = tokensym
+			desc = 'coin_id or chain'
+			tx.proto = get_proto_from_coin_id(tx, coin_id, tx.chain)
 
 			desc = 'metadata (4 items)'
 			(txid, send_amt, tx.timestamp, blockcount) = metadata
@@ -133,13 +192,16 @@ class MMGenTxFile(MMGenObject):
 			tx.check_txfile_hex_data()
 			desc = 'Ethereum RLP or JSON data'
 			tx.parse_txfile_serialized_data()
-			desc = 'inputs data'
-			tx.inputs  = eval_io_data(inputs_data,'inputs')
-			desc = 'outputs data'
-			tx.outputs = eval_io_data(outputs_data,'outputs')
+			for k in ('inputs', 'outputs'):
+				desc = f'{k} data'
+				res = deserialize(io_data[k], k)
+				for d in res:
+					if 'label' in d:
+						d['comment'] = d['label']
+						del d['label']
+				setattr(tx, k, eval_io_data(tx, res, k))
 			desc = 'send amount in metadata'
-			from decimal import Decimal
-			assert Decimal(send_amt) == tx.send_amt, f'{send_amt} != {tx.send_amt}'
+			assert tx.proto.coin_amt(send_amt) == tx.send_amt, f'{send_amt} != {tx.send_amt}'
 		except Exception as e:
 			die(2,f'Invalid {desc} in transaction file: {e!s}')
 
@@ -162,42 +224,60 @@ class MMGenTxFile(MMGenObject):
 
 	def format(self):
 		tx = self.tx
+		coin_id = tx.coin + ('' if tx.coin == tx.dcoin else ':'+tx.dcoin)
+
+		def format_data_legacy():
+
+			def amt_to_str(d):
+				return {k: (str(d[k]) if k == 'amt' else d[k]) for k in d}
+
+			lines = [
+				'{}{} {} {} {} {}{}'.format(
+					(f'{coin_id} ' if coin_id and tx.coin != 'BTC' else ''),
+					tx.chain.upper(),
+					tx.txid,
+					tx.send_amt,
+					tx.timestamp,
+					tx.blockcount,
+					(f' LT={tx.locktime}' if tx.locktime else ''),
+				),
+				tx.serialized,
+				ascii([amt_to_str(e._asdict()) for e in tx.inputs]),
+				ascii([amt_to_str(e._asdict()) for e in tx.outputs])
+			]
+
+			if tx.comment:
+				from ..baseconv import baseconv
+				lines.append(baseconv('b58').frombytes(tx.comment.encode(),tostr=True))
+
+			if tx.coin_txid:
+				if not tx.comment:
+					lines.append('-') # keep old tx files backwards compatible
+				lines.append(tx.coin_txid)
+
+			if tx.sent_timestamp:
+				lines.append(f'Sent {tx.sent_timestamp}')
+
+			return '\n'.join([make_chksum_6(' '.join(lines))] + lines) + '\n'
+
+		def format_data_json():
+			data = json_dumps({
+					'coin_id': coin_id
+				} | {
+					k: getattr(tx, k) for k in self.attrs
+				} | {
+					'inputs':  [e._asdict() for e in tx.inputs],
+					'outputs': [e._asdict() for e in tx.outputs]
+				} | {
+					k: getattr(tx, k) for k in self.extra_attrs if getattr(tx, k)
+				})
+			return '{{"{}":{},"chksum":"{}"}}'.format(self.data_label, data, make_chksum_6(data))
+
+		fmt_data = {'json': format_data_json, 'legacy': format_data_legacy}[tx.file_format]()
 
-		def amt_to_str(d):
-			return {k: (str(d[k]) if k == 'amt' else d[k]) for k in d}
-
-		coin_id = '' if tx.coin == 'BTC' else tx.coin + ('' if tx.coin == tx.dcoin else ':'+tx.dcoin)
-		lines = [
-			'{}{} {} {} {} {}{}'.format(
-				(coin_id+' ' if coin_id else ''),
-				tx.chain.upper(),
-				tx.txid,
-				tx.send_amt,
-				tx.timestamp,
-				tx.blockcount,
-				(f' LT={tx.locktime}' if tx.locktime else ''),
-			),
-			tx.serialized,
-			ascii([amt_to_str(e._asdict()) for e in tx.inputs]),
-			ascii([amt_to_str(e._asdict()) for e in tx.outputs])
-		]
-
-		if tx.comment:
-			from ..baseconv import baseconv
-			lines.append(baseconv('b58').frombytes(tx.comment.encode(),tostr=True))
-
-		if tx.coin_txid:
-			if not tx.comment:
-				lines.append('-') # keep old tx files backwards compatible
-			lines.append(tx.coin_txid)
-
-		if tx.sent_timestamp:
-			lines.append(f'Sent {tx.sent_timestamp}')
-
-		self.chksum = make_chksum_6(' '.join(lines))
-		fmt_data = '\n'.join([self.chksum] + lines) + '\n'
 		if len(fmt_data) > tx.cfg.max_tx_file_size:
 			die( 'MaxFileSizeExceeded', f'Transaction file size exceeds limit ({tx.cfg.max_tx_file_size} bytes)' )
+
 		return fmt_data
 
 	def write(self,

+ 1 - 0
test/ref/data_dir/altcoins/etc/tracking-wallet.json

@@ -0,0 +1 @@
+{"coin":"ETC","network":"MAINNET","accounts":{"65f56389a1c702ab62a32c13d43d87a734b5e11f":{"mmid":"98831F3A:E:12","comment":"","balance":"99.997088092"},"d4ab1fecf420cbdc3d551c1935acdeaa1fb5b181":{"mmid":"98831F3A:E:13","comment":"","balance":"0"}},"tokens":{"492934308e98b590a626666b703a6ddf2120e85e":{"params":{"symbol":"MM1","decimals":18},"65f56389a1c702ab62a32c13d43d87a734b5e11f":{"mmid":"98831F3A:E:12","comment":"","balance":"43.21"},"d4ab1fecf420cbdc3d551c1935acdeaa1fb5b181":{"mmid":"98831F3A:E:13","comment":"","balance":"1.23456"}}}}

+ 1 - 0
test/ref/tx/0A869F[1.23456,32].regtest.asubtx

@@ -0,0 +1 @@
+{"MMGenTransaction":{"coin_id":"BTC","chain":"regtest","txid":"0A869F","send_amt":"1.23456","timestamp":"20241011_142209","blockcount":395,"serialized":"0200000000010116ee956bfc0e1e902f2fdf9c92e4cfeb129856efb30bb94563792fdac83258eb0000000017160014e665d2746915d246d8ee50fcbd092778107657a6fdffffff0200ca5b07000000001976a914000000000000000000000000000000000000000088ace094df9c0b0000001976a9142d1fd5841f0c217662f01a97a1fdf34de1d233f888ac024730440220278901a54dd4834ead1b3896a97eedb546a61f1df239a0e807f11e4aa00ee2ac022033a50b5e5e76835aa2eaa14df2ff5f8985d617dffc66d0f9841b5a2943077599012103fba670aa3ae2be0a8b65b3eca71760aff9026a81d61046c0e1c2fb237140381700000000","inputs":[{"txid":"eb5832c8da2f796345b90bb3ef569812ebcfe4929cdf2f2f901e0efc6b95ee16","vout":0,"scriptPubKey":"a91487d16a346909c7aeb90ca2ac8cfeb24784f4193887","comment":"","amt":"500","addr":"2N5dN5ady9UzR3qYdRSmTPUWpHhAmVbHoRD","confs":2,"mmid":"DE05F9A1:S:1","sequence":4294967293}],"outputs":[{"addr":"mfWxJ45yp2SFn7UciZyNpvDKrzbhyfKrY8","amt":"1.23456","is_chg":false},{"addr":"mjdYqZ8VzXawMSu2hFPYn18EC8Y6WEs3BC","amt":"498.76538592","is_chg":true,"mmid":"DE05F9A1:C:5"}],"comment":"This one\u2019s worth a comment","coin_txid":"a7b9b1a3607334e8ff5dc0bc9902bbfb7493b8070aa020748ecb237a9aca9785","sent_timestamp":"20241011_142211"},"chksum":"e22da6"}

+ 1 - 0
test/ref/tx/7A8157[6.65227,34].rawtx

@@ -0,0 +1 @@
+{"MMGenTransaction":{"coin_id":"BTC","chain":"mainnet","txid":"7A8157","send_amt":"6.65227","timestamp":"20241010_154753","blockcount":1000000,"serialized":"02000000011a7537162621fc98a191fdb96e1dbcfa290935ff7661a3d9e60525f3d1b703a30000000000fdffffff0388e1b913000000001976a91471402298f14e70277ab549ccb6a5a34eb862faa888ac70adec130000000017a914a7c818791e3c19d62459901e7a94c566d1f397e08738eb94af000000001976a9147819b0332bb615808965de645d488d242c82194f88ac00000000","inputs":[{"vout":0,"txid":"a303b7d1f32505e6d9a36176ff350929fabc1d6eb9fd91a198fc21261637751a","scriptPubKey":"76a91429d8b6a52a93730cb125db5984debd38528b0d7c88ac","comment":"House purchase","amt":"36.11009344","addr":"14pGKP3YbVg1XHLh523QxR2xHuA3iy9p1U","confs":316150,"mmid":"10175B2D:L:5","sequence":4294967293}],"outputs":[{"addr":"1BKpBsAb4fF8NkhWni17aEGTNJsCQJ3yCZ","amt":"3.30949","is_chg":false,"mmid":"10175B2D:L:12"},{"addr":"3GzAUTwc8j8MuKsVs6qfCbNKVeuLgXmt6Q","amt":"3.34278","is_chg":false},{"addr":"1Bx2rscjX7w8PwFuY3Nn2GyJUZXcXam1mE","amt":"29.45772344","is_chg":true,"mmid":"10175B2D:L:99"}]},"chksum":"95d646"}

+ 1 - 0
test/ref/tx/91060A-BCH[1.23456].regtest.arawtx

@@ -0,0 +1 @@
+{"MMGenTransaction":{"coin_id":"BCH","chain":"regtest","txid":"91060A","send_amt":"1.23456","timestamp":"20241011_143137","blockcount":395,"serialized":"0200000001f5615164c22c221122649c7076b30a815a25ed125fb89b8c15902e56c939b64f0100000000ffffffff0200ca5b07000000001976a914000000000000000000000000000000000000000088ac008adf9c0b0000001976a914e9bd3f5df1e2621e8d69fa5a7fb4d48f12a1f5ac88ac00000000","inputs":[{"txid":"4fb639c9562e90158c9bb85f12ed255a810ab376709c642211222cc2645161f5","vout":1,"scriptPubKey":"76a914c330f16a6618930f9de900813855135da8f5f89788ac","comment":"","amt":"500","addr":"bchreg:qrpnput2vcvfxruaayqgzwz4zdw63a0cjujruft7h6","confs":2,"mmid":"D1432478:L:1","sequence":4294967295}],"outputs":[{"addr":"bchreg:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqha9s37tt","amt":"1.23456","is_chg":false},{"addr":"bchreg:qr5m606a783xy85dd8a95la56j839g044s7gcgjq60","amt":"498.76535808","is_chg":true,"mmid":"D1432478:C:5"}]},"chksum":"41eb4f"}

File diff suppressed because it is too large
+ 0 - 0
test/ref/tx/BB3FD2[7.57134314,123].sigtx


+ 1 - 0
test/ref/tx/C09D73-LTC[981.73747,2000].testnet.rawtx

@@ -0,0 +1 @@
+{"MMGenTransaction":{"coin_id":"LTC","chain":"testnet","txid":"C09D73","send_amt":"981.73747","timestamp":"20241011_142938","blockcount":1000000,"serialized":"02000000023088a0bfd661b1102f25d4963cb44027855166bed946f4286c509969c8b298130300000000fdffffff395fc6e667bc2c84d199b618f6b309facd975837c983ad44f2425ed94bc26a7c0100000000fdffffff04e0d5e949020000001976a914cfceadbbbcc160a35e0cff9e90039451495cb57588ace0eeb56e0700000017a914714a56e6b30326420f358d5bf130caaee27609358778bafc220d0000001976a914bed52df4b7fdfedae9470239c1a80e17c8b400b488ac5f150922660000001976a914bc5d121e221457b98343fe69fa7e2e855b740fb488ac00000000","inputs":[{"vout":3,"txid":"1398b2c86999506c28f446d9be6651852740b43c96d4252f10b161d6bfa08830","scriptPubKey":"76a91497c85adaaf5bd12acde85b79311c1f5712bff23b88ac","comment":"","amt":"1484.89524648","addr":"muMWLvUo85eFpgkprNUFiZCfoWGKo2duj9","confs":821544,"mmid":"D59AC957:L:2","sequence":4294967293},{"vout":1,"txid":"7c6ac24bd95e42f244ad83c9375897cdfa09b3f618b699d1842cbc67e6c65f39","scriptPubKey":"76a91479db077835922a8c8da6cea87a6e4ea5f527fdc588ac","comment":"\u6240\u4ee5\uff0c\u6211\u5011\u975e\u5e38\u9700\u8981\u9019\u6a23\u4e00\u7a2e\u96fb\u5b50\u652f\u4ed8\u7cfb\u7d71\uff0c\u5b83\u57fa\u65bc\u5bc6\u78bc\u5b78\u539f\u7406\u800c\u4e0d\u57fa\u65bc\u4fe1\u7528\uff0c\u4f7f\u5f97\u4efb\u4f55\u9054","amt":"3883.42907183","addr":"mrdGSgNP1NVCJxPHCzYHDGQz9LcSezrNHJ","confs":775261,"mmid":"5E75A2D0:L:5","sequence":4294967293}],"outputs":[{"addr":"mzTjm3gKT44wAWHo1bJzqrQ6A7BoHs8L3P","amt":"98.29996","is_chg":false,"mmid":"5E75A2D0:L:12"},{"addr":"QWw1XAvBMcnUUvUUgYHQFx9KLWUnjrTnPq","amt":"319.22188","is_chg":false},{"addr":"mxuz4UTTehBrH6GYUNJYJYoy31UaaXAD8W","amt":"564.21563","is_chg":false,"mmid":"D59AC957:L:1022"},{"addr":"mxgvpnhfdZQEMNEdCkvYoT7WiLfKARa8Qa","amt":"4386.57684831","is_chg":true,"mmid":"D59AC957:L:1023"}]},"chksum":"bad2aa"}

+ 1 - 0
test/ref/tx/D850C6-MM1[43.21,50000].subtx

@@ -0,0 +1 @@
+{"MMGenTransaction":{"coin_id":"ETC:MM1","chain":"classic","txid":"D850C6","send_amt":"43.21","timestamp":"20241011_144051","blockcount":20,"serialized":"f8a902850ba43b740082ea6094492934308e98b590a626666b703a6ddf2120e85e80b844a9059cbb00000000000000000000000065f56389a1c702ab62a32c13d43d87a734b5e11f00000000000000000000000000000000000000000000000257a8c21048a1000046a064093d1ddd4ee3bbb3f88e27388272bf62d744ef77d03a307a68ed674d671eeba07d7332481c857c9f030386c68e95b4d8e688abe77b938d93d62d83c8c61b2269","inputs":[{"comment":"","amt":"110.654317776666555545","addr":"1f5c9ee4a60d4a3c8b89eec978438bb6b53abf4a","confs":0,"mmid":"98831F3A:E:11"}],"outputs":[{"addr":"65f56389a1c702ab62a32c13d43d87a734b5e11f","amt":"43.21","is_chg":false,"mmid":"98831F3A:E:12"}],"comment":"\u5fc5\u8981\u306a\u306e\u306f\u3001\u4fe1\u7528\u3067\u306f\u306a\u304f\u6697\u53f7\u5316\u3055\u308c\u305f\u8a3c\u660e\u306b\u57fa\u3065\u304f\u96fb\u5b50\u53d6\u5f15\u30b7\u30b9\u30c6\u30e0\u3067\u3042\u308a\u3001\u3053\u308c\u306b\u3088\u308a\u5e0c\u671b\u3059\u308b\u4e8c\u8005\u304c\u4fe1\u7528\u3067\u304d\u308b\u7b2c\u4e09\u8005\u6a5f\u95a2\u3092\u4ecb\u3055\u305a\u306b\u76f4\u63a5\u53d6\u5f15\u3067\u304d\u308b\u3088\u3046","coin_txid":"61f624d8a279e566055294e6a3d17f4c7438e80f50a509e145df150b0447b53c","sent_timestamp":"20241011_144051"},"chksum":"974b90"}

+ 38 - 26
test/unit_tests_d/ut_tx.py

@@ -11,17 +11,18 @@ from mmgen.tx import NewTX,CompletedTX,UnsignedTX
 from mmgen.tx.file import MMGenTxFile
 from mmgen.daemon import CoinDaemon
 from mmgen.protocol import init_proto
+from mmgen.cfg import Config
 
-from ..include.common import cfg,qmsg,vmsg
+from ..include.common import cfg, qmsg, vmsg
 
-async def do_txfile_test(desc,fns):
+async def do_txfile_test(desc, fns, cfg=cfg, check=False):
 	qmsg(f'  Testing CompletedTX initializer ({desc})')
 	for fn in fns:
 		qmsg(f'     parsing: {os.path.basename(fn)}')
 		fpath = os.path.join('test','ref',fn)
-		tx = await CompletedTX( cfg=cfg, filename=fpath, quiet_open=True )
+		tx = await CompletedTX(cfg=cfg, filename=fpath, quiet_open=True)
 
-		vmsg(tx.info.format())
+		vmsg('\n' + tx.info.format())
 
 		f = MMGenTxFile(tx)
 		fn_gen = f.make_filename()
@@ -31,31 +32,18 @@ async def do_txfile_test(desc,fns):
 
 		assert fn_gen == os.path.basename(fn), f'{fn_gen} != {fn}'
 
-		text = f.format()
-
-		continue # TODO: check disabled after label -> comment patch
-
-		with open(fpath) as fp:
-			chk = fp.read()
-
-		# remove Python2 'u' string prefixes from ref files:
-		#   New in version 3.3: Support for the unicode legacy literal (u'value') was
-		#   reintroduced to simplify the maintenance of dual Python 2.x and 3.x codebases.
-		#   See PEP 414 for more information.
-		chk = chk.replace("'label':","'comment':") # TODO
-		chk = re.subn( r"\bu(['\"])", r'\1', chk )[0]
-
-		diff = get_ndiff(chk,text)
-		print(get_diff(chk,text,from_json=False))
-		nLines = len([i for i in diff if i.startswith('-')])
-		assert nLines in (0,1), f'{nLines} lines differ: only checksum line may differ'
+		if check:
+			text = f.format()
+			with open(fpath) as fh:
+				text_chk = fh.read()
+			assert text == text_chk, f'\nformatted text:\n{text}\n  !=\noriginal file:\n{text_chk}'
 
 	qmsg('  OK')
 	return True
 
 class unit_tests:
 
-	altcoin_deps = ('txfile_alt',)
+	altcoin_deps = ('txfile_alt', 'txfile_alt_legacy')
 
 	async def tx(self,name,ut):
 		qmsg('  Testing NewTX initializer')
@@ -70,9 +58,33 @@ class unit_tests:
 		qmsg('  OK')
 		return True
 
-	async def txfile(self,name,ut):
+	async def txfile(self, name, ut):
 		return await do_txfile_test(
 			'Bitcoin',
+			(
+				'tx/7A8157[6.65227,34].rawtx',
+				'tx/BB3FD2[7.57134314,123].sigtx',
+				'tx/0A869F[1.23456,32].regtest.asubtx',
+			),
+			check = True
+		)
+
+	async def txfile_alt(self, name, ut):
+		return await do_txfile_test(
+			'altcoins',
+			(
+				'tx/C09D73-LTC[981.73747,2000].testnet.rawtx',
+				'tx/91060A-BCH[1.23456].regtest.arawtx',
+				'tx/D850C6-MM1[43.21,50000].subtx', # token tx
+			),
+			# token resolved by tracking wallet under data_dir:
+			cfg = Config({'data_dir': 'test/ref/data_dir'}),
+			check = True
+		)
+
+	async def txfile_legacy(self, name, ut):
+		return await do_txfile_test(
+			'Bitcoin - legacy file format',
 			(
 				'0B8D5A[15.31789,14,tl=1320969600].rawtx',
 				'542169[5.68152,34].sigtx',
@@ -81,9 +93,9 @@ class unit_tests:
 			)
 		)
 
-	async def txfile_alt(self,name,ut):
+	async def txfile_alt_legacy(self, name, ut):
 		return await do_txfile_test(
-			'altcoins',
+			'altcoins - legacy file format',
 			(
 				'460D4D-BCH[10.19764,tl=1320969600].rawtx',
 				'ethereum/5881D2-MM1[1.23456,50000].rawtx',

Some files were not shown because too many files changed in this diff