Browse Source

txsend: add `--wait` and `--txhex-idx` options; related cleanups

The MMGen Project 7 months ago
parent
commit
69a39fad5e

+ 3 - 0
mmgen/main_txbump.py

@@ -78,7 +78,10 @@ opts_data = {
 			-- -q, --quiet            Suppress warnings; overwrite files without prompting
 			-- -q, --quiet            Suppress warnings; overwrite files without prompting
 			-- -s, --send             Sign and send the transaction (the default if seed
 			-- -s, --send             Sign and send the transaction (the default if seed
 			+                         data is provided)
 			+                         data is provided)
+			-- -T, --txhex-idx=N      Send only part ‘N’ of a multi-part transaction.
+			+                         Indexing begins with one.
 			-- -v, --verbose          Produce more verbose output
 			-- -v, --verbose          Produce more verbose output
+			e- -w, --wait             Wait for transaction confirmation
 			-- -W, --allow-non-wallet-swap Allow signing of swap transactions that send funds
 			-- -W, --allow-non-wallet-swap Allow signing of swap transactions that send funds
 			+                         to non-wallet addresses
 			+                         to non-wallet addresses
 			-- -x, --proxy=P          Fetch the swap quote via SOCKS5 proxy ‘P’ (host:port)
 			-- -x, --proxy=P          Fetch the swap quote via SOCKS5 proxy ‘P’ (host:port)

+ 3 - 0
mmgen/main_txdo.py

@@ -90,11 +90,14 @@ opts_data = {
 			+                         according to BIP 125)
 			+                         according to BIP 125)
 			-s -s, --swap-proto       Swap protocol to use (Default: {x_dfl},
 			-s -s, --swap-proto       Swap protocol to use (Default: {x_dfl},
 			+                         Choices: {x_all})
 			+                         Choices: {x_all})
+			-- -T, --txhex-idx=N      Send only part ‘N’ of a multi-part transaction.
+			+                         Indexing begins with one.
 			-- -u, --subseeds=      n The number of subseed pairs to scan for (default: {ss},
 			-- -u, --subseeds=      n The number of subseed pairs to scan for (default: {ss},
 			+                         maximum: {ss_max}). Only the default or first supplied
 			+                         maximum: {ss_max}). Only the default or first supplied
 			+                         wallet is scanned for subseeds.
 			+                         wallet is scanned for subseeds.
 			-- -v, --verbose          Produce more verbose output
 			-- -v, --verbose          Produce more verbose output
 			b- -V, --vsize-adj=     f Adjust transaction's estimated vsize by factor 'f'
 			b- -V, --vsize-adj=     f Adjust transaction's estimated vsize by factor 'f'
+			e- -w, --wait             Wait for transaction confirmation
 			-s -x, --proxy=P          Fetch the swap quote via SOCKS5 proxy ‘P’ (host:port)
 			-s -x, --proxy=P          Fetch the swap quote via SOCKS5 proxy ‘P’ (host:port)
 			e- -X, --cached-balances  Use cached balances
 			e- -X, --cached-balances  Use cached balances
 			-- -y, --yes              Answer 'yes' to prompts, suppress non-essential output
 			-- -y, --yes              Answer 'yes' to prompts, suppress non-essential output

+ 13 - 17
mmgen/main_txsend.py

@@ -51,13 +51,16 @@ opts_data = {
                  action has been successfully sent out-of-band.
                  action has been successfully sent out-of-band.
 -n, --tx-proxy=P Send transaction via public TX proxy ‘P’ (supported proxies:
 -n, --tx-proxy=P Send transaction via public TX proxy ‘P’ (supported proxies:
                  {tx_proxies}).  This is done via a publicly accessible web
                  {tx_proxies}).  This is done via a publicly accessible web
-                 page, so no API key or registration is required
+                 page, so no API key or registration is required.
 -q, --quiet      Suppress warnings; overwrite files without prompting
 -q, --quiet      Suppress warnings; overwrite files without prompting
 -r, --receipt    Print the receipt of the sent transaction (Ethereum only)
 -r, --receipt    Print the receipt of the sent transaction (Ethereum only)
 -s, --status     Get status of a sent transaction (or current transaction,
 -s, --status     Get status of a sent transaction (or current transaction,
                  whether sent or unsent, when used with --autosign)
                  whether sent or unsent, when used with --autosign)
 -t, --test       Test whether the transaction can be sent without sending it
 -t, --test       Test whether the transaction can be sent without sending it
+-T, --txhex-idx=N Send only part ‘N’ of a multi-part transaction.  Indexing
+                 begins with one.
 -v, --verbose    Be more verbose
 -v, --verbose    Be more verbose
+-w, --wait       Wait for transaction confirmation (Ethereum only)
 -x, --proxy=P    Connect to TX proxy via SOCKS5 proxy ‘P’ (host:port)
 -x, --proxy=P    Connect to TX proxy via SOCKS5 proxy ‘P’ (host:port)
 -y, --yes        Answer 'yes' to prompts, suppress non-essential output
 -y, --yes        Answer 'yes' to prompts, suppress non-essential output
 """
 """
@@ -141,22 +144,15 @@ async def main():
 		await tx.post_send(asi)
 		await tx.post_send(asi)
 		sys.exit(0)
 		sys.exit(0)
 
 
-	if cfg.status:
-		if tx.coin_txid:
-			cfg._util.qmsg(f'{tx.proto.coin} txid: {tx.coin_txid.hl()}')
-		retval = await tx.status.display(usr_req=True, return_exit_val=True)
-		if cfg.verbose:
-			tx.info.view_with_prompt('View transaction details?', pause=False)
-		sys.exit(retval)
-
-	if tx.is_swap and not tx.check_swap_expiry():
-		die(1, 'Swap quote has expired. Please re-create the transaction')
-
-	if not (cfg.yes or cfg.receipt):
-		tx.info.view_with_prompt('View transaction details?')
-		if tx.add_comment(): # edits an existing comment, returns true if changed
-			if not cfg.autosign:
-				tx.file.write(ask_write_default_yes=True)
+	if not cfg.status or cfg.receipt:
+		if tx.is_swap and not tx.check_swap_expiry():
+			die(1, 'Swap quote has expired. Please re-create the transaction')
+
+		if not cfg.yes:
+			tx.info.view_with_prompt('View transaction details?')
+			if tx.add_comment(): # edits an existing comment, returns true if changed
+				if not cfg.autosign:
+					tx.file.write(ask_write_default_yes=True)
 
 
 	await tx.send(cfg, asi)
 	await tx.send(cfg, asi)
 
 

+ 2 - 2
mmgen/proto/btc/tx/online.py

@@ -76,8 +76,8 @@ class OnlineSigned(Signed, TxBase.OnlineSigned):
 			msg(orange('\n'+errmsg))
 			msg(orange('\n'+errmsg))
 			die(2, f'{m}{nl}Send of MMGen transaction {self.txid} failed')
 			die(2, f'{m}{nl}Send of MMGen transaction {self.txid} failed')
 
 
-	def post_write(self):
-		pass
+	async def post_network_send(self, coin_txid):
+		return True
 
 
 class Sent(TxBase.Sent, OnlineSigned):
 class Sent(TxBase.Sent, OnlineSigned):
 	pass
 	pass

+ 21 - 24
mmgen/proto/btc/tx/status.py

@@ -15,20 +15,19 @@ proto.btc.tx.status: Bitcoin transaction status class
 import time
 import time
 
 
 from ....tx import status as TxBase
 from ....tx import status as TxBase
-from ....util import msg, suf, die
+from ....util import msg, suf
 from ....util2 import format_elapsed_hr
 from ....util2 import format_elapsed_hr
 
 
 class Status(TxBase.Status):
 class Status(TxBase.Status):
 
 
-	async def display(self, *, usr_req=False, return_exit_val=False):
+	async def display(self, *, idx=''):
 
 
-		def do_exit(retval, message):
-			if return_exit_val:
+		def do_return(exitval, message):
+			if message:
 				msg(message)
 				msg(message)
-				return retval
-			else:
-				die(retval, message)
+			return exitval
 
 
+		assert idx == '', f'multiple txhex not supported for {self.tx.proto}'
 		tx = self.tx
 		tx = self.tx
 
 
 		class r:
 		class r:
@@ -82,25 +81,23 @@ class Status(TxBase.Status):
 					return False
 					return False
 
 
 		if await is_in_mempool():
 		if await is_in_mempool():
-			if usr_req:
-				d = await tx.rpc.icall(
-					'gettransaction',
-					txid              = tx.coin_txid,
-					include_watchonly = True,
-					verbose           = False)
-				rep = ('' if d.get('bip125-replaceable') == 'yes' else 'NOT ') + 'replaceable'
-				t = d['timereceived']
-				if tx.cfg.quiet:
-					msg('Transaction is in mempool')
-				else:
-					msg(f'TX status: in mempool, {rep}')
-					msg('Sent {} ({})'.format(time.strftime('%c', time.gmtime(t)), format_elapsed_hr(t)))
+			d = await tx.rpc.icall(
+				'gettransaction',
+				txid              = tx.coin_txid,
+				include_watchonly = True,
+				verbose           = False)
+			rep = ('' if d.get('bip125-replaceable') == 'yes' else 'NOT ') + 'replaceable'
+			t = d['timereceived']
+			if tx.cfg.quiet:
+				msg('Transaction is in mempool')
 			else:
 			else:
-				msg('Warning: transaction is in mempool!')
+				msg(f'TX status: in mempool, {rep}')
+				msg('Sent {} ({})'.format(time.strftime('%c', time.gmtime(t)), format_elapsed_hr(t)))
+			return do_return(0, '')
 		elif await is_in_wallet():
 		elif await is_in_wallet():
-			return do_exit(0, f'Transaction has {r.confs} confirmation{suf(r.confs)}')
+			return do_return(0, f'Transaction has {r.confs} confirmation{suf(r.confs)}')
 		elif await is_in_utxos():
 		elif await is_in_utxos():
-			return do_exit(4, 'ERROR: transaction is in the blockchain (but not in the tracking wallet)!')
+			return do_return(4, 'ERROR: transaction is in the blockchain (but not in the tracking wallet)!')
 		elif await is_replaced():
 		elif await is_replaced():
 			msg('Transaction has been replaced')
 			msg('Transaction has been replaced')
 			msg('Replacement transaction ' + (
 			msg('Replacement transaction ' + (
@@ -117,4 +114,4 @@ class Status(TxBase.Status):
 						d.append({})
 						d.append({})
 				for txid, mp_entry in zip(r.replacing_txs, d):
 				for txid, mp_entry in zip(r.replacing_txs, d):
 					msg(f'  {txid}' + (' in mempool' if 'height' in mp_entry else ''))
 					msg(f'  {txid}' + (' in mempool' if 'height' in mp_entry else ''))
-			return do_exit(0, '')
+			return do_return(0, '')

+ 22 - 4
mmgen/proto/eth/tx/base.py

@@ -54,11 +54,29 @@ class Base(TxBase):
 	def is_replaceable(self):
 	def is_replaceable(self):
 		return True
 		return True
 
 
-	# used for testing only:
-	async def get_receipt(self, txid):
-		rx = await self.rpc.call('eth_getTransactionReceipt', '0x'+txid) # -> null if pending
-		if not rx:
+	async def get_receipt(self, txid, *, receipt_only=False):
+		import asyncio
+		from ....util import msg, msg_r
+
+		for n in range(60):
+			rx = await self.rpc.call('eth_getTransactionReceipt', '0x'+txid) # -> null if pending
+			if rx or not self.cfg.wait:
+				break
+			if n == 0:
+				msg_r('Waiting for first confirmation..')
+			await asyncio.sleep(1)
+			msg_r('.')
+
+		if rx:
+			if n:
+				msg('OK')
+			if receipt_only:
+				return rx
+		else:
+			if self.cfg.wait:
+				msg('timeout exceeded!')
 			return None
 			return None
+
 		tx = await self.rpc.call('eth_getTransactionByHash', '0x'+txid)
 		tx = await self.rpc.call('eth_getTransactionByHash', '0x'+txid)
 		return namedtuple('exec_status',
 		return namedtuple('exec_status',
 				['status', 'gas_sent', 'gas_used', 'gas_price', 'contract_addr', 'tx', 'rx'])(
 				['status', 'gas_sent', 'gas_used', 'gas_price', 'contract_addr', 'tx', 'rx'])(

+ 20 - 4
mmgen/proto/eth/tx/online.py

@@ -13,7 +13,7 @@ proto.eth.tx.online: Ethereum online signed transaction class
 """
 """
 
 
 from ....util import msg, die
 from ....util import msg, die
-from ....color import orange
+from ....color import yellow, green, orange
 from ....tx import online as TxBase
 from ....tx import online as TxBase
 from .. import erigon_sleep
 from .. import erigon_sleep
 from .signed import Signed, TokenSigned
 from .signed import Signed, TokenSigned
@@ -42,9 +42,25 @@ class OnlineSigned(Signed, TxBase.OnlineSigned):
 		await erigon_sleep(self)
 		await erigon_sleep(self)
 		return ret.removeprefix('0x')
 		return ret.removeprefix('0x')
 
 
-	def post_write(self):
-		if 'token_addr' in self.txobj and not self.txobj['to']:
-			msg('Contract address: {}'.format(self.txobj['token_addr'].hl(0)))
+	async def post_network_send(self, coin_txid):
+		res = await self.get_receipt(coin_txid)
+		if not res:
+			if self.cfg.wait:
+				msg('{} {} {}'.format(
+					yellow('Send of'),
+					coin_txid.hl(),
+					yellow('failed? (failed to get receipt)')))
+			return False
+		msg(f'Gas sent: {res.gas_sent.hl()}\n'
+			f'Gas used: {res.gas_used.hl()}')
+		if res.status == 0:
+			if res.gas_used == res.gas_sent:
+				msg(yellow('All gas was used!'))
+			die(1, f'Send of {coin_txid.hl()} failed (status=0)')
+		msg(f'Status: {green(str(res.status))}')
+		if res.contract_addr:
+			msg('Contract address: {}'.format(res.contract_addr.hl(0)))
+		return True
 
 
 class TokenOnlineSigned(TokenSigned, OnlineSigned):
 class TokenOnlineSigned(TokenSigned, OnlineSigned):
 
 

+ 23 - 29
mmgen/proto/eth/tx/status.py

@@ -13,31 +13,33 @@ proto.eth.tx.status: Ethereum transaction status class
 """
 """
 
 
 from ....tx import status as TxBase
 from ....tx import status as TxBase
-from ....util import msg, Msg, die, suf, capfirst
+from ....util import msg, suf, capfirst
 
 
 class Status(TxBase.Status):
 class Status(TxBase.Status):
 
 
-	async def display(self, *, usr_req=False, return_exit_val=False, print_receipt=False, idx=''):
+	async def display(self, *, idx=''):
 
 
-		def do_exit(retval, message):
-			if return_exit_val:
+		def do_return(exitval, message):
+			if message:
 				msg(message)
 				msg(message)
-				return retval
-			else:
-				die(retval, message)
+			return exitval
 
 
 		tx = self.tx
 		tx = self.tx
 		coin_txid = '0x' + getattr(tx, f'coin_txid{idx}')
 		coin_txid = '0x' + getattr(tx, f'coin_txid{idx}')
+		tx_desc = 'transaction' + (f' {idx}' if idx else '')
 
 
 		async def is_in_mempool():
 		async def is_in_mempool():
 			if not 'full_node' in tx.rpc.caps:
 			if not 'full_node' in tx.rpc.caps:
 				return False
 				return False
 			if tx.rpc.daemon.id in ('parity', 'openethereum'):
 			if tx.rpc.daemon.id in ('parity', 'openethereum'):
-				pool = [x['hash'] for x in await tx.rpc.call('parity_pendingTransactions')]
+				return coin_txid in [x['hash'] for x in await tx.rpc.call('parity_pendingTransactions')]
 			elif tx.rpc.daemon.id in ('geth', 'reth', 'erigon'):
 			elif tx.rpc.daemon.id in ('geth', 'reth', 'erigon'):
+				def gen(key):
+					for e in res[key].values():
+						for v in e.values():
+							yield v['hash']
 				res = await tx.rpc.call('txpool_content')
 				res = await tx.rpc.call('txpool_content')
-				pool = list(res['pending']) + list(res['queued'])
-			return coin_txid in pool
+				return coin_txid in list(gen('queued')) + list(gen('pending'))
 
 
 		async def is_in_wallet():
 		async def is_in_wallet():
 			d = await tx.rpc.call('eth_getTransactionReceipt', coin_txid)
 			d = await tx.rpc.call('eth_getTransactionReceipt', coin_txid)
@@ -50,26 +52,18 @@ class Status(TxBase.Status):
 					rx = d)
 					rx = d)
 
 
 		if await is_in_mempool():
 		if await is_in_mempool():
-			msg(
-				'Transaction is in mempool' if usr_req else
-				'Warning: transaction is in mempool!')
-			return
+			return do_return(0, f'{capfirst(tx_desc)} is in mempool')
 
 
-		if usr_req or print_receipt:
-			ret = await is_in_wallet()
-			if print_receipt:
-				import json
-				Msg(json.dumps(ret.rx, indent=4))
-				return not ret.exec_status
-			if ret:
-				if tx.txobj['data'] and not tx.is_swap:
-					cd = capfirst(tx.contract_desc)
-					if ret.exec_status == 0:
-						msg(f'{cd} failed to execute!')
-					else:
-						msg(f'{cd} successfully executed with status {ret.exec_status}')
-				return do_exit(0, f'Transaction has {ret.confs} confirmation{suf(ret.confs)}')
-			return do_exit(1, 'Transaction is neither in mempool nor blockchain!')
+		if res := await is_in_wallet():
+			if tx.txobj['data'] and not tx.is_swap:
+				cd = capfirst(tx.contract_desc)
+				msg(f'{cd} failed to execute!' if res.exec_status == 0 else
+					f'{cd} successfully executed with status {res.exec_status}')
+			return do_return(
+				int(not res.exec_status),
+				f'{capfirst(tx_desc)} has {res.confs} confirmation{suf(res.confs)}')
+
+		return do_return(1, f'{capfirst(tx_desc)} is neither in mempool nor blockchain!')
 
 
 class TokenStatus(Status):
 class TokenStatus(Status):
 	pass
 	pass

+ 52 - 16
mmgen/tx/online.py

@@ -12,6 +12,11 @@
 tx.online: online signed transaction class
 tx.online: online signed transaction class
 """
 """
 
 
+import sys, time, asyncio
+
+from ..util import msg, Msg, ymsg, make_timestr, die
+from ..color import pink, yellow
+
 from .signed import Signed, AutomountSigned
 from .signed import Signed, AutomountSigned
 
 
 class OnlineSigned(Signed):
 class OnlineSigned(Signed):
@@ -22,10 +27,7 @@ class OnlineSigned(Signed):
 		return _base_proto_subclass('Status', 'status', self.proto)(self)
 		return _base_proto_subclass('Status', 'status', self.proto)(self)
 
 
 	def check_swap_expiry(self):
 	def check_swap_expiry(self):
-		import time
-		from ..util import msg, make_timestr
 		from ..util2 import format_elapsed_hr
 		from ..util2 import format_elapsed_hr
-		from ..color import pink, yellow
 		expiry = self.swap_quote_expiry
 		expiry = self.swap_quote_expiry
 		now = int(time.time())
 		now = int(time.time())
 		t_rem = expiry - now
 		t_rem = expiry - now
@@ -36,8 +38,7 @@ class OnlineSigned(Signed):
 			c = make_timestr(expiry)))
 			c = make_timestr(expiry)))
 		return t_rem >= 0
 		return t_rem >= 0
 
 
-	def confirm_send(self):
-		from ..util import msg
+	def confirm_send(self, idxs):
 		from ..ui import confirm_or_raise
 		from ..ui import confirm_or_raise
 		confirm_or_raise(
 		confirm_or_raise(
 			cfg     = self.cfg,
 			cfg     = self.cfg,
@@ -45,6 +46,8 @@ class OnlineSigned(Signed):
 			action  = f'broadcast this transaction to the {self.proto.coin} {self.proto.network.upper()} network',
 			action  = f'broadcast this transaction to the {self.proto.coin} {self.proto.network.upper()} network',
 			expect  = 'YES' if self.cfg.quiet or self.cfg.yes else 'YES, I REALLY WANT TO DO THIS')
 			expect  = 'YES' if self.cfg.quiet or self.cfg.yes else 'YES, I REALLY WANT TO DO THIS')
 		msg('Sending transaction')
 		msg('Sending transaction')
+		if len(idxs) > 1 and getattr(self, 'coin_txid2', None) and self.is_swap:
+			ymsg('Warning: two transactions (approval and router) will be broadcast to the network')
 
 
 	async def post_send(self, asi):
 	async def post_send(self, asi):
 		from . import SentTX
 		from . import SentTX
@@ -55,21 +58,40 @@ class OnlineSigned(Signed):
 			outdir        = asi.txauto_dir if asi else None,
 			outdir        = asi.txauto_dir if asi else None,
 			ask_overwrite = False,
 			ask_overwrite = False,
 			ask_write     = False)
 			ask_write     = False)
-		tx2.post_write()
 
 
 	async def send(self, cfg, asi):
 	async def send(self, cfg, asi):
 
 
-		if not (cfg.receipt or cfg.dump_hex or cfg.test):
-			self.confirm_send()
-
+		status_exitval = None
 		sent_status = None
 		sent_status = None
+		all_ok = True
+		idxs = ['', '2']
+
+		if cfg.txhex_idx:
+			if getattr(self, 'coin_txid2', None):
+				if cfg.txhex_idx in ('1', '2'):
+					idxs = ['' if cfg.txhex_idx == '1' else cfg.txhex_idx]
+				else:
+					die(1, f'{cfg.txhex_idx}: invalid parameter for --txhex-idx (must be 1 or 2)')
+			else:
+				die(1, 'Transaction has only one part, so --txhex-idx makes no sense')
 
 
-		for idx in ('', '2'):
+		if not (cfg.status or cfg.receipt or cfg.dump_hex or cfg.test):
+			self.confirm_send(idxs)
+
+		for idx in idxs:
 			if coin_txid := getattr(self, f'coin_txid{idx}', None):
 			if coin_txid := getattr(self, f'coin_txid{idx}', None):
 				txhex = getattr(self, f'serialized{idx}')
 				txhex = getattr(self, f'serialized{idx}')
-				if cfg.receipt:
-					import sys
-					sys.exit(await self.status.display(print_receipt=True, idx=idx))
+				if cfg.status:
+					cfg._util.qmsg(f'{self.proto.coin} txid: {coin_txid.hl()}')
+					if cfg.verbose:
+						await self.post_network_send(coin_txid)
+					status_exitval = await self.status.display(idx=idx)
+				elif cfg.receipt:
+					if res := await self.get_receipt(coin_txid, receipt_only=True):
+						import json
+						Msg(json.dumps(res, indent=4))
+					else:
+						msg(f'Unable to get receipt for TX {coin_txid.hl()}')
 				elif cfg.dump_hex:
 				elif cfg.dump_hex:
 					from ..fileutil import write_data_to_file
 					from ..fileutil import write_data_to_file
 					write_data_to_file(
 					write_data_to_file(
@@ -80,29 +102,43 @@ class OnlineSigned(Signed):
 							ask_overwrite = False,
 							ask_overwrite = False,
 							ask_tty = False)
 							ask_tty = False)
 				elif cfg.tx_proxy:
 				elif cfg.tx_proxy:
+					if idx != '' and not cfg.test_suite:
+						await asyncio.sleep(2)
 					from .tx_proxy import send_tx
 					from .tx_proxy import send_tx
+					msg(f'Sending TX: {coin_txid.hl()}')
 					if ret := send_tx(cfg, txhex):
 					if ret := send_tx(cfg, txhex):
 						if ret != coin_txid:
 						if ret != coin_txid:
-							from ..util import ymsg
 							ymsg(f'Warning: txid mismatch (after sending) ({ret} != {coin_txid})')
 							ymsg(f'Warning: txid mismatch (after sending) ({ret} != {coin_txid})')
 						sent_status = 'confirm_post_send'
 						sent_status = 'confirm_post_send'
 				elif cfg.test:
 				elif cfg.test:
 					await self.test_sendable(txhex)
 					await self.test_sendable(txhex)
 				else: # node send
 				else: # node send
+					msg(f'Sending TX: {coin_txid.hl()}')
 					if not cfg.bogus_send:
 					if not cfg.bogus_send:
+						if idx != '':
+							await asyncio.sleep(1)
 						ret = await self.send_with_node(txhex)
 						ret = await self.send_with_node(txhex)
 						assert ret == coin_txid, f'txid mismatch (after sending) ({ret} != {coin_txid})'
 						assert ret == coin_txid, f'txid mismatch (after sending) ({ret} != {coin_txid})'
 					desc = 'BOGUS transaction NOT' if cfg.bogus_send else 'Transaction'
 					desc = 'BOGUS transaction NOT' if cfg.bogus_send else 'Transaction'
-					from ..util import msg
 					msg(desc + ' sent: ' + coin_txid.hl())
 					msg(desc + ' sent: ' + coin_txid.hl())
 					sent_status = 'no_confirm_post_send'
 					sent_status = 'no_confirm_post_send'
 
 
-		if sent_status:
+				if cfg.wait and sent_status:
+					res = await self.post_network_send(coin_txid)
+					if all_ok:
+						all_ok = res
+
+		if not cfg.txhex_idx and sent_status and all_ok:
 			from ..ui import keypress_confirm
 			from ..ui import keypress_confirm
 			if sent_status == 'no_confirm_post_send' or not asi or keypress_confirm(
 			if sent_status == 'no_confirm_post_send' or not asi or keypress_confirm(
 					cfg, 'Mark transaction as sent on removable device?'):
 					cfg, 'Mark transaction as sent on removable device?'):
 				await self.post_send(asi)
 				await self.post_send(asi)
 
 
+		if status_exitval is not None:
+			if cfg.verbose:
+				self.info.view_with_prompt('View transaction details?', pause=False)
+			sys.exit(status_exitval)
+
 class AutomountOnlineSigned(AutomountSigned, OnlineSigned):
 class AutomountOnlineSigned(AutomountSigned, OnlineSigned):
 	pass
 	pass
 
 

+ 5 - 5
mmgen/tx/tx_proxy.py

@@ -71,14 +71,14 @@ class TxProxyClient:
 		assert len(res) == 1, 'more than one matching form!'
 		assert len(res) == 1, 'more than one matching form!'
 		return res[0]
 		return res[0]
 
 
-	def cache_fn(self, desc):
-		return f'{self.name}-{desc}.html'
+	def cache_fn(self, desc, *, extra_desc=None):
+		return '{}-{}{}.html'.format(self.name, desc, f'-{extra_desc}' if extra_desc else '')
 
 
-	def save_response(self, data, desc):
+	def save_response(self, data, desc, *, extra_desc=None):
 		from ..fileutil import write_data_to_file
 		from ..fileutil import write_data_to_file
 		write_data_to_file(
 		write_data_to_file(
 			self.cfg,
 			self.cfg,
-			self.cache_fn(desc),
+			self.cache_fn(desc, extra_desc=extra_desc),
 			data,
 			data,
 			desc = f'{desc} page from {orange(self.host)}')
 			desc = f'{desc} page from {orange(self.host)}')
 
 
@@ -200,7 +200,7 @@ def send_tx(cfg, txhex):
 	msg('done')
 	msg('done')
 
 
 	msg('Transaction ' + (f'sent: {txid.hl()}' if txid else 'send failed'))
 	msg('Transaction ' + (f'sent: {txid.hl()}' if txid else 'send failed'))
-	c.save_response(result_text, 'result')
+	c.save_response(result_text, 'result', extra_desc=txid)
 
 
 	return txid
 	return txid
 
 

+ 1 - 0
pyproject.toml

@@ -46,6 +46,7 @@ ignore = [
 "test/*.py"                     = [ "F401" ]         # imported but unused
 "test/*.py"                     = [ "F401" ]         # imported but unused
 "test/colortest.py"             = [ "F403", "F405" ] # `import *` used
 "test/colortest.py"             = [ "F403", "F405" ] # `import *` used
 "test/tooltest2.py"             = [ "F403", "F405" ] # `import *` used
 "test/tooltest2.py"             = [ "F403", "F405" ] # `import *` used
+"test/overlay/tree/*"           = [ "ALL" ]
 
 
 [tool.pylint.format]
 [tool.pylint.format]
 indent-string = "\t"
 indent-string = "\t"

+ 3 - 6
test/cmdtest_d/ethbump.py

@@ -208,9 +208,7 @@ class CmdTestEthBump(CmdTestEthBumpMethods, CmdTestEthSwapMethods, CmdTestSwapMe
 			('token_deploy_a',   'deploying ERC20 token MM1 (SafeMath)'),
 			('token_deploy_a',   'deploying ERC20 token MM1 (SafeMath)'),
 			('token_deploy_b',   'deploying ERC20 token MM1 (Owned)'),
 			('token_deploy_b',   'deploying ERC20 token MM1 (Owned)'),
 			('token_deploy_c',   'deploying ERC20 token MM1 (Token)'),
 			('token_deploy_c',   'deploying ERC20 token MM1 (Token)'),
-			('wait_reth2',       'waiting for block'),
 			('token_fund_user',  'transferring token funds from dev to user'),
 			('token_fund_user',  'transferring token funds from dev to user'),
-			('wait6',            'waiting for block'),
 			('token_addrgen',    'generating token addresses'),
 			('token_addrgen',    'generating token addresses'),
 			('token_addrimport', 'importing token addresses using token address (MM1)'),
 			('token_addrimport', 'importing token addresses using token address (MM1)'),
 			('token_bal1',       'the token balance'),
 			('token_bal1',       'the token balance'),
@@ -219,7 +217,7 @@ class CmdTestEthBump(CmdTestEthBumpMethods, CmdTestEthSwapMethods, CmdTestSwapMe
 			'creating, signing, sending, bumping and resending a token transaction (fee-bump only)',
 			'creating, signing, sending, bumping and resending a token transaction (fee-bump only)',
 			('token_txdo1',   'creating, signing and sending a token transaction'),
 			('token_txdo1',   'creating, signing and sending a token transaction'),
 			('token_txbump1', 'bumping the token transaction (fee-bump)'),
 			('token_txbump1', 'bumping the token transaction (fee-bump)'),
-			('wait7',         'waiting for block'),
+			('wait6',         'waiting for block'),
 			('token_bal2',    'the token balance'),
 			('token_bal2',    'the token balance'),
 		),
 		),
 		'token_new_outputs': (
 		'token_new_outputs': (
@@ -228,7 +226,7 @@ class CmdTestEthBump(CmdTestEthBumpMethods, CmdTestEthSwapMethods, CmdTestSwapMe
 			('token_txbump2',     'creating a replacement token transaction (new outputs)'),
 			('token_txbump2',     'creating a replacement token transaction (new outputs)'),
 			('token_txbump2sign', 'signing the replacement transaction'),
 			('token_txbump2sign', 'signing the replacement transaction'),
 			('token_txbump2send', 'sending the replacement transaction'),
 			('token_txbump2send', 'sending the replacement transaction'),
-			('wait8',             'waiting for block'),
+			('wait7',             'waiting for block'),
 			('token_bal3',        'the token balance'),
 			('token_bal3',        'the token balance'),
 		)
 		)
 	}
 	}
@@ -338,8 +336,7 @@ class CmdTestEthBump(CmdTestEthBumpMethods, CmdTestEthSwapMethods, CmdTestSwapMe
 	def wait_reth1(self):
 	def wait_reth1(self):
 		return self._wait_for_block() if self.daemon.id == 'reth' else 'silent'
 		return self._wait_for_block() if self.daemon.id == 'reth' else 'silent'
 
 
-	wait_reth2 = wait_reth1
-	wait1 = wait2 = wait3 = wait4 = wait5 = wait6 = wait7 = wait8 = CmdTestEthBumpMethods._wait_for_block
+	wait1 = wait2 = wait3 = wait4 = wait5 = wait6 = wait7 = CmdTestEthBumpMethods._wait_for_block
 
 
 	txsign1 = txsign2 = txbump1sign = txbump2sign = CmdTestEthBumpMethods._txsign
 	txsign1 = txsign2 = txbump1sign = txbump2sign = CmdTestEthBumpMethods._txsign
 	txsend1 = txsend2 = txbump1send = txbump2send = CmdTestEthBumpMethods._txsend
 	txsend1 = txsend2 = txbump1send = txbump2send = CmdTestEthBumpMethods._txsend

+ 28 - 42
test/cmdtest_d/ethdev.py

@@ -240,8 +240,8 @@ class CmdTestEthdevMethods:
 			gas,
 			gas,
 			mmgen_cmd = 'txdo',
 			mmgen_cmd = 'txdo',
 			gas_price = '8G',
 			gas_price = '8G',
-			num = None,
-			get_receipt = True):
+			num = None):
+
 		keyfile = joinpath(self.tmpdir, dfl_devkey_fn)
 		keyfile = joinpath(self.tmpdir, dfl_devkey_fn)
 		fn = joinpath(self.tmpdir, 'mm'+str(num), key+'.bin')
 		fn = joinpath(self.tmpdir, 'mm'+str(num), key+'.bin')
 		args = [
 		args = [
@@ -250,8 +250,8 @@ class CmdTestEthdevMethods:
 			f'--gas={gas}',
 			f'--gas={gas}',
 			f'--contract-data={fn}',
 			f'--contract-data={fn}',
 			f'--inputs={dfl_devaddr}',
 			f'--inputs={dfl_devaddr}',
-			'--yes',
-		]
+			'--yes'
+		] + (['--wait'] if mmgen_cmd == 'txdo' else [])
 
 
 		contract_addr = self._get_contract_address(dfl_devaddr)
 		contract_addr = self._get_contract_address(dfl_devaddr)
 		if key == 'Token':
 		if key == 'Token':
@@ -271,36 +271,30 @@ class CmdTestEthdevMethods:
 
 
 			txfile = txfile.replace('.rawtx', '.sigtx')
 			txfile = txfile.replace('.rawtx', '.sigtx')
 			t = self.spawn('mmgen-txsend',
 			t = self.spawn('mmgen-txsend',
-				self.eth_opts + [txfile], no_msg=True, no_passthru_opts=['coin'])
+				self.eth_opts + ['--wait', txfile], no_msg=True, no_passthru_opts=['coin'])
 
 
-		txid = self.txsend_ui_common(t,
+		self.txsend_ui_common(t,
 			caller = mmgen_cmd,
 			caller = mmgen_cmd,
 			quiet  = mmgen_cmd == 'txdo' or not self.cfg.debug,
 			quiet  = mmgen_cmd == 'txdo' or not self.cfg.debug,
+			contract_addr = contract_addr,
 			bogus_send = False)
 			bogus_send = False)
 
 
-		_ = strip_ansi_escapes(t.expect_getend('Contract address: '))
-		assert _ == contract_addr, f'Contract address mismatch: {_} != {contract_addr}'
-
-		if get_receipt:
-			if (await self.get_tx_receipt(txid)).status == 0:
-				die(2, f'Contract {num}:{key} failed to deploy. Aborting')
-
 		if key == 'Token':
 		if key == 'Token':
 			imsg(f'\nToken MM{num} deployed!')
 			imsg(f'\nToken MM{num} deployed!')
 
 
 		return t
 		return t
 
 
-	async def _token_deploy_math(self, *, num, get_receipt=True, mmgen_cmd='txdo'):
+	async def _token_deploy_math(self, *, num, mmgen_cmd='txdo'):
 		return await self._token_deploy(
 		return await self._token_deploy(
-			num=num, key='SafeMath', gas=500_000, get_receipt=get_receipt, mmgen_cmd=mmgen_cmd)
+			num=num, key='SafeMath', gas=500_000, mmgen_cmd=mmgen_cmd)
 
 
-	async def _token_deploy_owned(self, *, num, get_receipt=True):
+	async def _token_deploy_owned(self, *, num):
 		return await self._token_deploy(
 		return await self._token_deploy(
-			num=num, key='Owned', gas=1_000_000, get_receipt=get_receipt)
+			num=num, key='Owned', gas=1_000_000)
 
 
-	async def _token_deploy_token(self, *, num, get_receipt=True):
+	async def _token_deploy_token(self, *, num):
 		return await self._token_deploy(
 		return await self._token_deploy(
-			num=num, key='Token', gas=4_000_000, gas_price='7G', get_receipt=get_receipt)
+			num=num, key='Token', gas=4_000_000, gas_price='7G')
 
 
 	def _token_bal_check(self, *, pat):
 	def _token_bal_check(self, *, pat):
 		return self._bal_check(pat=pat, add_opts=['--token=MM1'])
 		return self._bal_check(pat=pat, add_opts=['--token=MM1'])
@@ -315,7 +309,7 @@ class CmdTestEthdevMethods:
 			caller            = cmd,
 			caller            = cmd,
 			file_desc         = 'Unsigned automount transaction')
 			file_desc         = 'Unsigned automount transaction')
 
 
-	async def _token_transfer_ops(self, *, op, mm_idxs, amt=1000, get_receipt=True, sid=dfl_sid):
+	async def _token_transfer_ops(self, *, op, mm_idxs, amt=1000, sid=dfl_sid):
 		self.spawn(msg_only=True)
 		self.spawn(msg_only=True)
 		from mmgen.tool.wallet import tool_cmd
 		from mmgen.tool.wallet import tool_cmd
 		usr_mmaddrs = [f'{sid}:E:{i}' for i in mm_idxs]
 		usr_mmaddrs = [f'{sid}:E:{i}' for i in mm_idxs]
@@ -338,9 +332,14 @@ class CmdTestEthdevMethods:
 					key       = dfl_devkey,
 					key       = dfl_devkey,
 					gas       = self.proto.coin_amt(120000, from_unit='wei'),
 					gas       = self.proto.coin_amt(120000, from_unit='wei'),
 					gasPrice  = self.proto.coin_amt(8, from_unit='Gwei'))
 					gasPrice  = self.proto.coin_amt(8, from_unit='Gwei'))
-
-				if get_receipt and (await self.get_tx_receipt(txid)).status == 0:
-					die(2, 'Transfer of token funds failed. Aborting')
+				rpc = await self.rpc
+				for n in range(50): # long delay for txbump
+					rx = await rpc.call('eth_getTransactionReceipt', '0x' + txid) # -> null if pending
+					if rx:
+						break
+					await asyncio.sleep(0.5)
+				if not rx:
+					die(1, 'tx receipt timeout exceeded')
 
 
 		async def show_bals(rpc):
 		async def show_bals(rpc):
 			for i in range(len(usr_mmaddrs)):
 			for i in range(len(usr_mmaddrs)):
@@ -1114,13 +1113,13 @@ class CmdTestEthdev(CmdTestEthdevMethods, CmdTestBase, CmdTestShared):
 	def bal3(self):
 	def bal3(self):
 		return self.bal(n='3')
 		return self.bal(n='3')
 
 
-	def tx_status(self, ext, expect_str, expect_str2='', exit_val=0):
+	def tx_status(self, ext, *, expect_str, expect_str2='', add_opts=[], exit_val=0):
 		self.mining_delay()
 		self.mining_delay()
 		ext = ext.format('-α' if self.cfg.debug_utf8 else '')
 		ext = ext.format('-α' if self.cfg.debug_utf8 else '')
 		txfile = self.get_file_with_ext(ext, no_dot=True)
 		txfile = self.get_file_with_ext(ext, no_dot=True)
 		t = self.spawn(
 		t = self.spawn(
 			'mmgen-txsend',
 			'mmgen-txsend',
-			self.eth_opts + ['--status', txfile],
+			self.eth_opts + add_opts + ['--status', txfile],
 			no_passthru_opts = ['coin'],
 			no_passthru_opts = ['coin'],
 			exit_val = exit_val)
 			exit_val = exit_val)
 		t.expect(expect_str)
 		t.expect(expect_str)
@@ -1129,7 +1128,10 @@ class CmdTestEthdev(CmdTestEthdevMethods, CmdTestBase, CmdTestShared):
 		return t
 		return t
 
 
 	def tx_status1(self):
 	def tx_status1(self):
-		return self.tx_status(ext='2.345,50000]{}.regtest.sigtx', expect_str='has 1 confirmation')
+		return self.tx_status(
+			ext        = '2.345,50000]{}.regtest.sigtx',
+			add_opts   = ['--verbose'],
+			expect_str = 'has 1 confirmation')
 
 
 	def tx_status1a(self):
 	def tx_status1a(self):
 		return self.tx_status(ext='2.345,50000]{}.regtest.sigtx', expect_str='has 2 confirmations')
 		return self.tx_status(ext='2.345,50000]{}.regtest.sigtx', expect_str='has 2 confirmations')
@@ -1333,22 +1335,6 @@ class CmdTestEthdev(CmdTestEthdevMethods, CmdTestBase, CmdTestShared):
 		token_data = {'name':'MMGen Token 2', 'symbol':'MM2', 'supply':10**18, 'decimals':10}
 		token_data = {'name':'MMGen Token 2', 'symbol':'MM2', 'supply':10**18, 'decimals':10}
 		return self.token_compile(token_data)
 		return self.token_compile(token_data)
 
 
-	async def get_tx_receipt(self, txid):
-		if self.daemon.id in ('geth', 'reth'): # workaround for mining race condition in dev mode
-			await asyncio.sleep(1 if self.daemon.id == 'reth' else 0.5)
-		from mmgen.tx import NewTX
-		tx = await NewTX(cfg=self.cfg, proto=self.proto, target='tx')
-		tx.rpc = await self.rpc
-		res = await tx.get_receipt(txid)
-		if not res:
-			die(1, f'Error getting receipt for transaction {txid}')
-		imsg(f'Gas sent:  {res.gas_sent.hl():<9} {(res.gas_sent*res.gas_price).hl2(encl="()")}')
-		imsg(f'Gas used:  {res.gas_used.hl():<9} {(res.gas_used*res.gas_price).hl2(encl="()")}')
-		imsg(f'Gas price: {res.gas_price.hl()}')
-		if res.gas_used == res.gas_sent:
-			omsg(yellow('Warning: all gas was used!'))
-		return res
-
 	async def token_deploy1a(self):
 	async def token_deploy1a(self):
 		return await self._token_deploy_math(num=1)
 		return await self._token_deploy_math(num=1)
 
 

+ 4 - 5
test/cmdtest_d/ethswap.py

@@ -34,20 +34,19 @@ def {name}(self):
 class CmdTestEthSwapMethods:
 class CmdTestEthSwapMethods:
 
 
 	async def token_deploy_a(self):
 	async def token_deploy_a(self):
-		return await self._token_deploy_math(num=1, get_receipt=False)
+		return await self._token_deploy_math(num=1)
 
 
 	async def token_deploy_b(self):
 	async def token_deploy_b(self):
-		return await self._token_deploy_owned(num=1, get_receipt=False)
+		return await self._token_deploy_owned(num=1)
 
 
 	async def token_deploy_c(self):
 	async def token_deploy_c(self):
-		return await self._token_deploy_token(num=1, get_receipt=False)
+		return await self._token_deploy_token(num=1)
 
 
 	def token_fund_user(self):
 	def token_fund_user(self):
 		return self._token_transfer_ops(
 		return self._token_transfer_ops(
 			op          = 'fund_user',
 			op          = 'fund_user',
 			mm_idxs     = [1],
 			mm_idxs     = [1],
-			amt         = self.token_fund_amt,
-			get_receipt = False)
+			amt         = self.token_fund_amt)
 
 
 	def token_addrgen(self):
 	def token_addrgen(self):
 		return self._token_addrgen(mm_idxs=[1], naddrs=5)
 		return self._token_addrgen(mm_idxs=[1], naddrs=5)