From 48660c3189071138a8612651d3fa5b463261688e Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Mon, 21 Apr 2025 14:01:16 +0000 Subject: [PATCH] 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 --- mmgen/data/version | 2 +- mmgen/main_txbump.py | 3 +- mmgen/main_txdo.py | 4 +-- mmgen/main_txsend.py | 45 ++++---------------------- mmgen/proto/btc/tx/online.py | 61 +++++++++++++----------------------- mmgen/proto/eth/tx/online.py | 35 ++++++--------------- mmgen/proto/eth/tx/status.py | 9 +++--- mmgen/tx/online.py | 57 +++++++++++++++++++++++++++++++++ mmgen/tx/tx_proxy.py | 9 ++---- test/cmdtest_d/autosign.py | 1 - test/cmdtest_d/ethbump.py | 2 +- test/cmdtest_d/regtest.py | 5 +-- 12 files changed, 109 insertions(+), 124 deletions(-) diff --git a/mmgen/data/version b/mmgen/data/version index 384c74d0..033560e1 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -15.1.dev27 +15.1.dev28 diff --git a/mmgen/main_txbump.py b/mmgen/main_txbump.py index 4ac15afd..3b39a9aa 100755 --- a/mmgen/main_txbump.py +++ b/mmgen/main_txbump.py @@ -205,8 +205,7 @@ async def main(): if tx3: tx4 = await OnlineSignedTX(cfg=cfg, data=tx3.__dict__) 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: die(2, 'Transaction could not be signed') else: diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index 8dd045fe..85557203 100755 --- a/mmgen/main_txdo.py +++ b/mmgen/main_txdo.py @@ -176,9 +176,7 @@ async def main(): if tx3: tx3.file.write(ask_write=False) 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: die(2, 'Transaction could not be signed') diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 597720b0..25cf512e 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -83,6 +83,8 @@ if cfg.dump_hex and cfg.dump_hex != '-': from .fileutil import check_outfile_dir check_outfile_dir(cfg.dump_hex) +asi = None + if len(cfg._args) == 1: infile = cfg._args[0] from .fileutil import check_infile @@ -109,16 +111,7 @@ if not cfg.status: from .ui import do_license_msg 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(): @@ -145,12 +138,9 @@ async def main(): cfg._util.vmsg(f'Getting {tx.desc} ‘{tx.infile}’') if cfg.mark_sent: - await post_send(tx) + await tx.post_send(asi) sys.exit(0) - if cfg.receipt: - sys.exit(await tx.status.display(print_receipt=True)) - if cfg.status: if tx.coin_txid: 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(): 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?') 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 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()) diff --git a/mmgen/proto/btc/tx/online.py b/mmgen/proto/btc/tx/online.py index 5c5596b6..b28ef37b 100755 --- a/mmgen/proto/btc/tx/online.py +++ b/mmgen/proto/btc/tx/online.py @@ -37,11 +37,11 @@ class OnlineSigned(Signed, TxBase.OnlineSigned): await self.status.display() - async def test_sendable(self): + async def test_sendable(self, txhex): await self.send_checks() - res = await self.rpc.call('testmempoolaccept', (self.serialized,)) + res = await self.rpc.call('testmempoolaccept', (txhex,)) ret = res[0] if ret['allowed']: @@ -54,44 +54,27 @@ class OnlineSigned(Signed, TxBase.OnlineSigned): msg(ret['reject-reason']) 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: - 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): pass diff --git a/mmgen/proto/eth/tx/online.py b/mmgen/proto/eth/tx/online.py index ed4a2f26..511a58b7 100755 --- a/mmgen/proto/eth/tx/online.py +++ b/mmgen/proto/eth/tx/online.py @@ -20,42 +20,27 @@ from .signed import Signed, TokenSigned class OnlineSigned(Signed, TxBase.OnlineSigned): - async def test_sendable(self): + async def test_sendable(self, txhex): raise NotImplementedError('transaction testing not implemented for Ethereum') - async def send(self, *, prompt_user=True): - + async def send_checks(self): self.check_correct_chain() - if not self.disable_fee_check and (self.fee > self.proto.max_tx_fee): die(2, 'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format( self.fee, self.proto.name, self.proto.max_tx_fee, self.proto.coin)) - 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): if 'token_addr' in self.txobj and not self.txobj['to']: diff --git a/mmgen/proto/eth/tx/status.py b/mmgen/proto/eth/tx/status.py index a50ba0a3..e3d98d96 100755 --- a/mmgen/proto/eth/tx/status.py +++ b/mmgen/proto/eth/tx/status.py @@ -17,7 +17,7 @@ from ....util import msg, Msg, die, suf, capfirst 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): if return_exit_val: @@ -27,6 +27,7 @@ class Status(TxBase.Status): die(retval, message) tx = self.tx + coin_txid = '0x' + getattr(tx, f'coin_txid{idx}') async def is_in_mempool(): 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'): res = await tx.rpc.call('txpool_content') pool = list(res['pending']) + list(res['queued']) - return '0x'+tx.coin_txid in pool + return coin_txid in pool 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: from collections import namedtuple receipt_info = namedtuple('receipt_info', ['confs', 'exec_status', 'rx']) diff --git a/mmgen/tx/online.py b/mmgen/tx/online.py index 3b182af1..ee2f2fbc 100755 --- a/mmgen/tx/online.py +++ b/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') 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): pass diff --git a/mmgen/tx/tx_proxy.py b/mmgen/tx/tx_proxy.py index 48510685..d8b69c7e 100755 --- a/mmgen/tx/tx_proxy.py +++ b/mmgen/tx/tx_proxy.py @@ -170,14 +170,11 @@ class EtherscanTxProxyClient(TxProxyClient): else: return False -def send_tx(cfg, tx): +def send_tx(cfg, txhex): c = get_client(cfg) 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)}...') form_text = c.get_form(timeout=180) msg('done') @@ -186,7 +183,7 @@ def send_tx(cfg, tx): post_data = c.create_post_data( form_text = form_text, coin = cfg.coin, - tx_hex = tx.serialized) + tx_hex = txhex) msg('done') if cfg.test: @@ -205,7 +202,7 @@ def send_tx(cfg, tx): msg('Transaction ' + (f'sent: {txid.hl()}' if txid else 'send failed')) c.save_response(result_text, 'result') - return bool(txid) + return txid tx_proxies = { 'blockchair': BlockchairTxProxyClient, diff --git a/test/cmdtest_d/autosign.py b/test/cmdtest_d/autosign.py index 66f8385b..3d165b4e 100755 --- a/test/cmdtest_d/autosign.py +++ b/test/cmdtest_d/autosign.py @@ -524,7 +524,6 @@ class CmdTestAutosignThreaded(CmdTestAutosignBase): t.do_comment(comment) if dump_hex: t.written_to_file('Serialized transaction hex data') - t.expect('(y/N): ', 'n') # mark as sent? else: self._do_confirm_send(t, quiet=True) t.written_to_file('Sent automount transaction') diff --git a/test/cmdtest_d/ethbump.py b/test/cmdtest_d/ethbump.py index bd27bb4a..f454f2e5 100755 --- a/test/cmdtest_d/ethbump.py +++ b/test/cmdtest_d/ethbump.py @@ -308,7 +308,7 @@ class CmdTestEthBump(CmdTestEthBumpMethods, CmdTestEthSwapMethods, CmdTestSwapMe add_opts = ['--token=MM1'], add_args = [dfl_words_file]) t.expect('to confirm: ', 'YES\n') - t.written_to_file('Signed transaction') + t.written_to_file('Sent transaction') return t def token_bal2(self): diff --git a/test/cmdtest_d/regtest.py b/test/cmdtest_d/regtest.py index fe1a7a63..72a87d53 100755 --- a/test/cmdtest_d/regtest.py +++ b/test/cmdtest_d/regtest.py @@ -1207,7 +1207,7 @@ class CmdTestRegtest(CmdTestBase, CmdTestShared): if signed_tx: t.passphrase(dfl_wcls.desc, rt_pw) 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: t.expect('Save fee-bumped transaction? (y/N): ', 'y') 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']) t.expect('view: ', '\n') 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 def bob_dump_hex_dump(self):