Browse Source

tx.online.OnlineSigned: new send() method

- common exit point for all scripts that broadcast transactions to the network

- supports transaction files with multiple txhex, required for upcoming ERC20
  THORChain swap support
The MMGen Project 8 months ago
parent
commit
48660c3189

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev27
+15.1.dev28

+ 1 - 2
mmgen/main_txbump.py

@@ -205,8 +205,7 @@ async def main():
 		if tx3:
 		if tx3:
 			tx4 = await OnlineSignedTX(cfg=cfg, data=tx3.__dict__)
 			tx4 = await OnlineSignedTX(cfg=cfg, data=tx3.__dict__)
 			tx4.file.write(ask_write=False)
 			tx4.file.write(ask_write=False)
-			if await tx4.send():
-				tx4.file.write(ask_write=False)
+			await tx4.send(cfg, asi if cfg.autosign else None)
 		else:
 		else:
 			die(2, 'Transaction could not be signed')
 			die(2, 'Transaction could not be signed')
 	else:
 	else:

+ 1 - 3
mmgen/main_txdo.py

@@ -176,9 +176,7 @@ async def main():
 	if tx3:
 	if tx3:
 		tx3.file.write(ask_write=False)
 		tx3.file.write(ask_write=False)
 		tx4 = await SentTX(cfg=cfg, data=tx3.__dict__)
 		tx4 = await SentTX(cfg=cfg, data=tx3.__dict__)
-		if await tx4.send():
-			tx4.file.write(ask_overwrite=False, ask_write=False)
-			tx4.post_write()
+		await tx4.send(cfg, asi=None)
 	else:
 	else:
 		die(2, 'Transaction could not be signed')
 		die(2, 'Transaction could not be signed')
 
 

+ 6 - 39
mmgen/main_txsend.py

@@ -83,6 +83,8 @@ if cfg.dump_hex and cfg.dump_hex != '-':
 	from .fileutil import check_outfile_dir
 	from .fileutil import check_outfile_dir
 	check_outfile_dir(cfg.dump_hex)
 	check_outfile_dir(cfg.dump_hex)
 
 
+asi = None
+
 if len(cfg._args) == 1:
 if len(cfg._args) == 1:
 	infile = cfg._args[0]
 	infile = cfg._args[0]
 	from .fileutil import check_infile
 	from .fileutil import check_infile
@@ -109,16 +111,7 @@ if not cfg.status:
 	from .ui import do_license_msg
 	from .ui import do_license_msg
 	do_license_msg(cfg)
 	do_license_msg(cfg)
 
 
-from .tx import OnlineSignedTX, SentTX
-from .ui import keypress_confirm
-
-async def post_send(tx):
-	tx2 = await SentTX(cfg=cfg, data=tx.__dict__, automount=cfg.autosign)
-	tx2.file.write(
-		outdir        = asi.txauto_dir if cfg.autosign else None,
-		ask_overwrite = False,
-		ask_write     = False)
-	tx2.post_write()
+from .tx import OnlineSignedTX
 
 
 async def main():
 async def main():
 
 
@@ -145,12 +138,9 @@ async def main():
 	cfg._util.vmsg(f'Getting {tx.desc} ‘{tx.infile}’')
 	cfg._util.vmsg(f'Getting {tx.desc} ‘{tx.infile}’')
 
 
 	if cfg.mark_sent:
 	if cfg.mark_sent:
-		await post_send(tx)
+		await tx.post_send(asi)
 		sys.exit(0)
 		sys.exit(0)
 
 
-	if cfg.receipt:
-		sys.exit(await tx.status.display(print_receipt=True))
-
 	if cfg.status:
 	if cfg.status:
 		if tx.coin_txid:
 		if tx.coin_txid:
 			cfg._util.qmsg(f'{tx.proto.coin} txid: {tx.coin_txid.hl()}')
 			cfg._util.qmsg(f'{tx.proto.coin} txid: {tx.coin_txid.hl()}')
@@ -162,35 +152,12 @@ async def main():
 	if tx.is_swap and not tx.check_swap_expiry():
 	if tx.is_swap and not tx.check_swap_expiry():
 		die(1, 'Swap quote has expired. Please re-create the transaction')
 		die(1, 'Swap quote has expired. Please re-create the transaction')
 
 
-	if not cfg.yes:
+	if not (cfg.yes or cfg.receipt):
 		tx.info.view_with_prompt('View transaction details?')
 		tx.info.view_with_prompt('View transaction details?')
 		if tx.add_comment(): # edits an existing comment, returns true if changed
 		if tx.add_comment(): # edits an existing comment, returns true if changed
 			if not cfg.autosign:
 			if not cfg.autosign:
 				tx.file.write(ask_write_default_yes=True)
 				tx.file.write(ask_write_default_yes=True)
 
 
-	if cfg.dump_hex:
-		from .fileutil import write_data_to_file
-		write_data_to_file(
-				cfg,
-				cfg.dump_hex,
-				tx.serialized + '\n',
-				desc = 'serialized transaction hex data',
-				ask_overwrite = False,
-				ask_tty = False)
-		if cfg.autosign:
-			if keypress_confirm(cfg, 'Mark transaction as sent on removable device?'):
-				await post_send(tx)
-		else:
-			await post_send(tx)
-	elif cfg.tx_proxy:
-		from .tx.tx_proxy import send_tx
-		if send_tx(cfg, tx):
-			if (not cfg.autosign or
-				keypress_confirm(cfg, 'Mark transaction as sent on removable device?')):
-				await post_send(tx)
-	elif cfg.test:
-		await tx.test_sendable()
-	elif await tx.send():
-		await post_send(tx)
+	await tx.send(cfg, asi)
 
 
 async_run(main())
 async_run(main())

+ 22 - 39
mmgen/proto/btc/tx/online.py

@@ -37,11 +37,11 @@ class OnlineSigned(Signed, TxBase.OnlineSigned):
 
 
 		await self.status.display()
 		await self.status.display()
 
 
-	async def test_sendable(self):
+	async def test_sendable(self, txhex):
 
 
 		await self.send_checks()
 		await self.send_checks()
 
 
-		res = await self.rpc.call('testmempoolaccept', (self.serialized,))
+		res = await self.rpc.call('testmempoolaccept', (txhex,))
 		ret = res[0]
 		ret = res[0]
 
 
 		if ret['allowed']:
 		if ret['allowed']:
@@ -54,44 +54,27 @@ class OnlineSigned(Signed, TxBase.OnlineSigned):
 			msg(ret['reject-reason'])
 			msg(ret['reject-reason'])
 			return False
 			return False
 
 
-	async def send(self, *, prompt_user=True):
-
-		await self.send_checks()
-
-		if prompt_user:
-			self.confirm_send()
-
-		if self.cfg.bogus_send:
-			m = 'BOGUS transaction NOT sent: {}'
-		else:
-			m = 'Transaction sent: {}'
-			try:
-				ret = await self.rpc.call('sendrawtransaction', self.serialized)
-			except Exception as e:
-				errmsg = str(e)
-				nl = '\n'
-				if errmsg.count('Signature must use SIGHASH_FORKID'):
-					m = (
-						'The Aug. 1 2017 UAHF has activated on this chain.\n'
-						'Re-run the script with the --coin=bch option.')
-				elif errmsg.count('Illegal use of SIGHASH_FORKID'):
-					m  = (
-						'The Aug. 1 2017 UAHF is not yet active on this chain.\n'
-						'Re-run the script without the --coin=bch option.')
-				elif errmsg.count('non-final'):
-					m = "Transaction with nLockTime {!r} can’t be included in this block!".format(
-						self.info.strfmt_locktime(self.get_serialized_locktime()))
-				else:
-					m, nl = ('', '')
-				msg(orange('\n'+errmsg))
-				die(2, f'{m}{nl}Send of MMGen transaction {self.txid} failed')
+	async def send_with_node(self, txhex):
+		try:
+			return await self.rpc.call('sendrawtransaction', txhex)
+		except Exception as e:
+			errmsg = str(e)
+			nl = '\n'
+			if errmsg.count('Signature must use SIGHASH_FORKID'):
+				m = (
+					'The Aug. 1 2017 UAHF has activated on this chain.\n'
+					'Re-run the script with the --coin=bch option.')
+			elif errmsg.count('Illegal use of SIGHASH_FORKID'):
+				m  = (
+					'The Aug. 1 2017 UAHF is not yet active on this chain.\n'
+					'Re-run the script without the --coin=bch option.')
+			elif errmsg.count('non-final'):
+				m = "Transaction with nLockTime {!r} can’t be included in this block!".format(
+					self.info.strfmt_locktime(self.get_serialized_locktime()))
 			else:
 			else:
-				assert ret == self.coin_txid, 'txid mismatch (after sending)'
-
-		msg(m.format(self.coin_txid.hl()))
-		self.add_sent_timestamp()
-		self.add_blockcount()
-		return True
+				m, nl = ('', '')
+			msg(orange('\n'+errmsg))
+			die(2, f'{m}{nl}Send of MMGen transaction {self.txid} failed')
 
 
 	def post_write(self):
 	def post_write(self):
 		pass
 		pass

+ 10 - 25
mmgen/proto/eth/tx/online.py

@@ -20,42 +20,27 @@ from .signed import Signed, TokenSigned
 
 
 class OnlineSigned(Signed, TxBase.OnlineSigned):
 class OnlineSigned(Signed, TxBase.OnlineSigned):
 
 
-	async def test_sendable(self):
+	async def test_sendable(self, txhex):
 		raise NotImplementedError('transaction testing not implemented for Ethereum')
 		raise NotImplementedError('transaction testing not implemented for Ethereum')
 
 
-	async def send(self, *, prompt_user=True):
-
+	async def send_checks(self):
 		self.check_correct_chain()
 		self.check_correct_chain()
-
 		if not self.disable_fee_check and (self.fee > self.proto.max_tx_fee):
 		if not self.disable_fee_check and (self.fee > self.proto.max_tx_fee):
 			die(2, 'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
 			die(2, 'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
 				self.fee,
 				self.fee,
 				self.proto.name,
 				self.proto.name,
 				self.proto.max_tx_fee,
 				self.proto.max_tx_fee,
 				self.proto.coin))
 				self.proto.coin))
-
 		await self.status.display()
 		await self.status.display()
 
 
-		if prompt_user:
-			self.confirm_send()
-
-		if self.cfg.bogus_send:
-			m = 'BOGUS transaction NOT sent: {}'
-		else:
-			try:
-				ret = await self.rpc.call('eth_sendRawTransaction', '0x'+self.serialized)
-			except Exception as e:
-				msg(orange('\n'+str(e)))
-				die(2, f'Send of MMGen transaction {self.txid} failed')
-			m = 'Transaction sent: {}'
-			assert ret == '0x'+self.coin_txid, 'txid mismatch (after sending)'
-			await erigon_sleep(self)
-
-		msg(m.format(self.coin_txid.hl()))
-		self.add_sent_timestamp()
-		self.add_blockcount()
-
-		return True
+	async def send_with_node(self, txhex):
+		try:
+			ret = await self.rpc.call('eth_sendRawTransaction', '0x' + txhex)
+		except Exception as e:
+			msg(orange('\n'+str(e)))
+			die(2, f'Send of MMGen transaction {self.txid} failed')
+		await erigon_sleep(self)
+		return ret.removeprefix('0x')
 
 
 	def post_write(self):
 	def post_write(self):
 		if 'token_addr' in self.txobj and not self.txobj['to']:
 		if 'token_addr' in self.txobj and not self.txobj['to']:

+ 4 - 5
mmgen/proto/eth/tx/status.py

@@ -17,7 +17,7 @@ from ....util import msg, Msg, die, suf, capfirst
 
 
 class Status(TxBase.Status):
 class Status(TxBase.Status):
 
 
-	async def display(self, *, usr_req=False, return_exit_val=False, print_receipt=False):
+	async def display(self, *, usr_req=False, return_exit_val=False, print_receipt=False, idx=''):
 
 
 		def do_exit(retval, message):
 		def do_exit(retval, message):
 			if return_exit_val:
 			if return_exit_val:
@@ -27,6 +27,7 @@ class Status(TxBase.Status):
 				die(retval, message)
 				die(retval, message)
 
 
 		tx = self.tx
 		tx = self.tx
+		coin_txid = '0x' + getattr(tx, f'coin_txid{idx}')
 
 
 		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:
@@ -36,12 +37,10 @@ class Status(TxBase.Status):
 			elif tx.rpc.daemon.id in ('geth', 'reth', 'erigon'):
 			elif tx.rpc.daemon.id in ('geth', 'reth', 'erigon'):
 				res = await tx.rpc.call('txpool_content')
 				res = await tx.rpc.call('txpool_content')
 				pool = list(res['pending']) + list(res['queued'])
 				pool = list(res['pending']) + list(res['queued'])
-			return '0x'+tx.coin_txid in pool
+			return coin_txid in pool
 
 
 		async def is_in_wallet():
 		async def is_in_wallet():
-			d = await tx.rpc.call(
-				'eth_getTransactionReceipt',
-				'0x' + tx.coin_txid)
+			d = await tx.rpc.call('eth_getTransactionReceipt', coin_txid)
 			if d and 'blockNumber' in d and d['blockNumber'] is not None:
 			if d and 'blockNumber' in d and d['blockNumber'] is not None:
 				from collections import namedtuple
 				from collections import namedtuple
 				receipt_info = namedtuple('receipt_info', ['confs', 'exec_status', 'rx'])
 				receipt_info = namedtuple('receipt_info', ['confs', 'exec_status', 'rx'])

+ 57 - 0
mmgen/tx/online.py

@@ -46,6 +46,63 @@ class OnlineSigned(Signed):
 			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')
 
 
+	async def post_send(self, asi):
+		from . import SentTX
+		tx2 = await SentTX(cfg=self.cfg, data=self.__dict__, automount=bool(asi))
+		tx2.add_sent_timestamp()
+		tx2.add_blockcount()
+		tx2.file.write(
+			outdir        = asi.txauto_dir if asi else None,
+			ask_overwrite = False,
+			ask_write     = False)
+		tx2.post_write()
+
+	async def send(self, cfg, asi):
+
+		if not (cfg.receipt or cfg.dump_hex or cfg.test):
+			self.confirm_send()
+
+		do_post_send = False
+
+		for idx in ('', '2'):
+			if coin_txid := getattr(self, f'coin_txid{idx}', None):
+				txhex = getattr(self, f'serialized{idx}')
+				if cfg.receipt:
+					import sys
+					sys.exit(await self.status.display(print_receipt=True, idx=idx))
+				elif cfg.dump_hex:
+					from ..fileutil import write_data_to_file
+					write_data_to_file(
+							cfg,
+							cfg.dump_hex + idx,
+							txhex + '\n',
+							desc = 'serialized transaction hex data',
+							ask_overwrite = False,
+							ask_tty = False)
+				elif cfg.tx_proxy:
+					from .tx_proxy import send_tx
+					if ret := send_tx(cfg, txhex):
+						if ret != coin_txid:
+							from ..util import ymsg
+							ymsg(f'Warning: txid mismatch (after sending) ({ret} != {coin_txid})')
+						do_post_send = 'confirm'
+				elif cfg.test:
+					await self.test_sendable(txhex)
+				else: # node send
+					if not cfg.bogus_send:
+						ret = await self.send_with_node(txhex)
+						assert ret == coin_txid, f'txid mismatch (after sending) ({ret} != {coin_txid})'
+					desc = 'BOGUS transaction NOT' if cfg.bogus_send else 'Transaction'
+					from ..util import msg
+					msg(desc + ' sent: ' + coin_txid.hl())
+					do_post_send = 'no_confirm'
+
+		if do_post_send:
+			from ..ui import keypress_confirm
+			if do_post_send == 'no_confirm' or not asi or keypress_confirm(
+					cfg, 'Mark transaction as sent on removable device?'):
+				await self.post_send(asi)
+
 class AutomountOnlineSigned(AutomountSigned, OnlineSigned):
 class AutomountOnlineSigned(AutomountSigned, OnlineSigned):
 	pass
 	pass
 
 

+ 3 - 6
mmgen/tx/tx_proxy.py

@@ -170,14 +170,11 @@ class EtherscanTxProxyClient(TxProxyClient):
 		else:
 		else:
 			return False
 			return False
 
 
-def send_tx(cfg, tx):
+def send_tx(cfg, txhex):
 
 
 	c = get_client(cfg)
 	c = get_client(cfg)
 	msg(f'Using {pink(cfg.tx_proxy.upper())} tx proxy')
 	msg(f'Using {pink(cfg.tx_proxy.upper())} tx proxy')
 
 
-	if not cfg.test:
-		tx.confirm_send()
-
 	msg_r(f'Retrieving form from {orange(c.host)}...')
 	msg_r(f'Retrieving form from {orange(c.host)}...')
 	form_text = c.get_form(timeout=180)
 	form_text = c.get_form(timeout=180)
 	msg('done')
 	msg('done')
@@ -186,7 +183,7 @@ def send_tx(cfg, tx):
 	post_data = c.create_post_data(
 	post_data = c.create_post_data(
 		form_text = form_text,
 		form_text = form_text,
 		coin      = cfg.coin,
 		coin      = cfg.coin,
-		tx_hex    = tx.serialized)
+		tx_hex    = txhex)
 	msg('done')
 	msg('done')
 
 
 	if cfg.test:
 	if cfg.test:
@@ -205,7 +202,7 @@ def send_tx(cfg, tx):
 	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')
 
 
-	return bool(txid)
+	return txid
 
 
 tx_proxies = {
 tx_proxies = {
 	'blockchair': BlockchairTxProxyClient,
 	'blockchair': BlockchairTxProxyClient,

+ 0 - 1
test/cmdtest_d/autosign.py

@@ -524,7 +524,6 @@ class CmdTestAutosignThreaded(CmdTestAutosignBase):
 			t.do_comment(comment)
 			t.do_comment(comment)
 			if dump_hex:
 			if dump_hex:
 				t.written_to_file('Serialized transaction hex data')
 				t.written_to_file('Serialized transaction hex data')
-				t.expect('(y/N): ', 'n') # mark as sent?
 			else:
 			else:
 				self._do_confirm_send(t, quiet=True)
 				self._do_confirm_send(t, quiet=True)
 				t.written_to_file('Sent automount transaction')
 				t.written_to_file('Sent automount transaction')

+ 1 - 1
test/cmdtest_d/ethbump.py

@@ -308,7 +308,7 @@ class CmdTestEthBump(CmdTestEthBumpMethods, CmdTestEthSwapMethods, CmdTestSwapMe
 			add_opts = ['--token=MM1'],
 			add_opts = ['--token=MM1'],
 			add_args = [dfl_words_file])
 			add_args = [dfl_words_file])
 		t.expect('to confirm: ', 'YES\n')
 		t.expect('to confirm: ', 'YES\n')
-		t.written_to_file('Signed transaction')
+		t.written_to_file('Sent transaction')
 		return t
 		return t
 
 
 	def token_bal2(self):
 	def token_bal2(self):

+ 3 - 2
test/cmdtest_d/regtest.py

@@ -1207,7 +1207,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 		if signed_tx:
 		if signed_tx:
 			t.passphrase(dfl_wcls.desc, rt_pw)
 			t.passphrase(dfl_wcls.desc, rt_pw)
 			t.written_to_file('Signed transaction')
 			t.written_to_file('Signed transaction')
-			self.txsend_ui_common(t, caller='txdo', bogus_send=False, file_desc='Signed transaction')
+			self.txsend_ui_common(t, caller='txdo', bogus_send=False)
 		else:
 		else:
 			t.expect('Save fee-bumped transaction? (y/N): ', 'y')
 			t.expect('Save fee-bumped transaction? (y/N): ', 'y')
 			t.written_to_file('Fee-bumped transaction')
 			t.written_to_file('Fee-bumped transaction')
@@ -2230,7 +2230,8 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared):
 			['-d', self.dump_hex_subdir, f'--dump-hex={file}', '--bob', txfile], no_passthru_opts=['coin'])
 			['-d', self.dump_hex_subdir, f'--dump-hex={file}', '--bob', txfile], no_passthru_opts=['coin'])
 		t.expect('view: ', '\n')
 		t.expect('view: ', '\n')
 		t.expect('(y/N): ', '\n') # add comment?
 		t.expect('(y/N): ', '\n') # add comment?
-		t.written_to_file('Sent transaction')
+		if file != '-':
+			t.written_to_file('Serialized transaction hex data')
 		return t
 		return t
 
 
 	def bob_dump_hex_dump(self):
 	def bob_dump_hex_dump(self):