From 5e326df10f9a90182255c64e1636a56fe7c7c962 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Tue, 27 Oct 2026 13:30:14 +0000 Subject: [PATCH] txsend: improve options handling; tx.online: reimplement `send()` --- mmgen/data/version | 2 +- mmgen/exception.py | 1 - mmgen/main_txbump.py | 2 +- mmgen/main_txdo.py | 2 +- mmgen/main_txsend.py | 43 ++++++--- mmgen/proto/btc/tx/status.py | 4 +- mmgen/proto/eth/tx/status.py | 7 +- mmgen/proto/rune/tx/status.py | 2 +- mmgen/proto/xmr/tx/status.py | 2 +- mmgen/tx/online.py | 170 +++++++++++++++++++--------------- 10 files changed, 137 insertions(+), 98 deletions(-) diff --git a/mmgen/data/version b/mmgen/data/version index e3ef4331..44d72f3b 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -16.3.0dev22 +16.3.0dev23 diff --git a/mmgen/exception.py b/mmgen/exception.py index bec8c01e..3a941d2b 100644 --- a/mmgen/exception.py +++ b/mmgen/exception.py @@ -75,7 +75,6 @@ class SwapCfgValueError(Exception): mmcode = 2 # 3: yellow hl, 'MMGen Error' + exception + message class RPCFailure(Exception): mmcode = 3 class RPCChainMismatch(Exception): mmcode = 3 -class TxIDMismatch(Exception): mmcode = 3 class BadTxSizeEstimate(Exception): mmcode = 3 class MaxInputSizeExceeded(Exception): mmcode = 3 class MaxFileSizeExceeded(Exception): mmcode = 3 diff --git a/mmgen/main_txbump.py b/mmgen/main_txbump.py index 76f8b326..2b2ce177 100644 --- a/mmgen/main_txbump.py +++ b/mmgen/main_txbump.py @@ -211,7 +211,7 @@ async def main(): TxKeys(cfg, tx2, seedfiles=seedfiles, keylist=kl, keyaddrlist=kal).keys): tx4 = await OnlineSignedTX(cfg=cfg, data=tx3.__dict__) tx4.file.write(ask_write=False) - sys.exit(await tx4.send(cfg, asi if cfg.autosign else None)) + sys.exit(await tx4.send(cfg, 'rpc_send', None, 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 1c5dc366..75a48951 100644 --- a/mmgen/main_txdo.py +++ b/mmgen/main_txdo.py @@ -184,7 +184,7 @@ async def main(): if tx3: tx3.file.write(ask_write=False) tx4 = await SentTX(cfg=cfg, data=tx3.__dict__) - return await tx4.send(cfg, asi=None) + return await tx4.send(cfg, 'rpc_send', None, asi=None) else: die(2, 'Transaction could not be signed') diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 9aaa1a0f..b3faaa95 100644 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -20,7 +20,7 @@ mmgen-txsend: Broadcast a transaction signed by 'mmgen-txsign' to the network import sys -from .cfg import gc, Config +from .cfg import gc, Config, fmt_opt from .util import msg, async_run, die opts_data = { @@ -97,14 +97,30 @@ if cfg.autosign and cfg.outdir: if cfg.mark_sent and not cfg.autosign: die(1, '--mark-sent is used only in combination with --autosign') -if cfg.test and cfg.dump_hex: - die(1, '--test cannot be used in combination with --dump-hex') +opt_ops = ( + 'test', + 'dump_hex', + 'status', + 'receipt', + 'mark_sent', + 'abort') -if cfg.dump_hex and cfg.dump_hex != '-': +match [op for op in opt_ops if getattr(cfg, op) is not None]: + case []: + op = 'proxy_send' if cfg.tx_proxy else 'rpc_send' + op_val = None + case [usr_op]: + op = ('proxy_test' if cfg.tx_proxy else 'rpc_test') if usr_op == 'test' else usr_op + op_val = getattr(cfg, usr_op) + case usr_ops: + die(1, 'Only one of the following options may be specified: {}'.format( + ', '.join(fmt_opt(op) for op in usr_ops))) + +if op == 'dump_hex' and op_val != '-': from .fileutil import check_outfile_dir - check_outfile_dir(cfg.dump_hex) + check_outfile_dir(op_val) -post_send_op = cfg.status or cfg.receipt +post_send_op = op in ('status', 'receipt') asi, tx_range = (None, None) def init_autosign(arg): @@ -114,7 +130,7 @@ def init_autosign(arg): from .tx.online import SentTXRange asi = mount_removable_device(cfg) si = Signable.automount_transaction(asi) - if cfg.abort: + if op == 'abort': si.shred_abortable() # prompts user, then raises exception or exits elif post_send_op and (si.unsent or si.unsigned): die(1, 'Transaction is {}'.format('unsent' if si.unsent else 'unsigned')) @@ -139,7 +155,7 @@ match cfg._args: case _: cfg._usage() -if not cfg.status: +if not post_send_op: from .ui import do_license_msg do_license_msg(cfg) @@ -170,7 +186,7 @@ async def process_tx(tx): txcfg = Config({'_clone': cfg, 'proto': tx.proto, 'coin': tx.proto.coin}) - if (not post_send_op) and cfg.tx_proxy: + if op == 'proxy_send': from .tx.tx_proxy import check_client check_client(txcfg) @@ -178,10 +194,11 @@ async def process_tx(tx): from .rpc import rpc_init tx.rpc = await rpc_init(txcfg) + if op == 'mark_sent': + await tx.post_send(asi) + return 0 + if not post_send_op: - if cfg.mark_sent: - await tx.post_send(asi) - return 0 if tx.is_swap or tx.is_refreq: tx.check_swap_network() @@ -198,7 +215,7 @@ async def process_tx(tx): if tx.has_comment and tx.add_comment() and not cfg.autosign: tx.file.write(ask_write_default_yes=True) - return await tx.send(txcfg, asi, batch=batch) + return await tx.send(txcfg, op, op_val, asi, batch=batch) async def main(): if cfg.autosign and post_send_op: diff --git a/mmgen/proto/btc/tx/status.py b/mmgen/proto/btc/tx/status.py index d503171f..812054c3 100644 --- a/mmgen/proto/btc/tx/status.py +++ b/mmgen/proto/btc/tx/status.py @@ -18,7 +18,7 @@ from ....util2 import format_elapsed_hr class Status(TxBase.Status): - async def display(self, *, idx=''): + async def display(self, *, idx=0): def do_return(exitval, message): if message: @@ -112,3 +112,5 @@ class Status(TxBase.Status): for txid, mp_entry in zip(r.replacing_txs, d): msg(f' {txid}' + (' in mempool' if 'height' in mp_entry else '')) return do_return(0, '') + else: + return do_return(3, 'Transaction is in an unknown state') diff --git a/mmgen/proto/eth/tx/status.py b/mmgen/proto/eth/tx/status.py index 0aa3f3de..09138943 100644 --- a/mmgen/proto/eth/tx/status.py +++ b/mmgen/proto/eth/tx/status.py @@ -15,7 +15,7 @@ from ....util import msg, suf, capfirst class Status(TxBase.Status): - async def display(self, *, idx=''): + async def display(self, *, idx=0): def do_return(exitval, message): if message: @@ -23,8 +23,9 @@ class Status(TxBase.Status): return exitval tx = self.tx - coin_txid = '0x' + getattr(tx, f'coin_txid{idx}') - tx_desc = 'transaction' + (f' {idx}' if idx else '') + sfx = idx + 1 if idx else '' + coin_txid = '0x' + getattr(tx, f'coin_txid{sfx}') + tx_desc = 'transaction' + (f' {sfx}' if sfx else '') async def is_in_mempool(): if not 'full_node' in tx.rpc.caps: diff --git a/mmgen/proto/rune/tx/status.py b/mmgen/proto/rune/tx/status.py index 046d7550..604bf5f6 100644 --- a/mmgen/proto/rune/tx/status.py +++ b/mmgen/proto/rune/tx/status.py @@ -15,7 +15,7 @@ from ....tx import status as TxBase class Status(TxBase.Status): - async def display(self, *, idx=''): + async def display(self, *, idx=0): try: self.tx.rpc.tx_info(self.tx.coin_txid) diff --git a/mmgen/proto/xmr/tx/status.py b/mmgen/proto/xmr/tx/status.py index 5baa2606..9e9b76b2 100644 --- a/mmgen/proto/xmr/tx/status.py +++ b/mmgen/proto/xmr/tx/status.py @@ -16,7 +16,7 @@ from ....xmrwallet import op as xmrwallet_op class Status(TxBase.Status): - async def display(self, *, idx=''): + async def display(self, *, idx=0): tx = self.tx if tx.cfg.verbose: msg(f'{tx.proto.coin} TxID: {tx.coin_txid.hl()}') diff --git a/mmgen/tx/online.py b/mmgen/tx/online.py index 68915462..b98e58df 100644 --- a/mmgen/tx/online.py +++ b/mmgen/tx/online.py @@ -18,6 +18,13 @@ from ..color import pink, yellow from .signed import Signed, AutomountSigned +def test_txid(txid, txid_chk): + if txid == txid_chk: + return 0 + else: + ymsg(f'Warning: txid mismatch (after sending) ({txid} != {txid_chk})') + return 1 + class SentTXRange(MMGenRange): min_idx = 0 max_idx = 1_000_000 @@ -26,6 +33,9 @@ class CreatedTXRange(SentTXRange): pass class OnlineSigned(Signed): + """ + send_op_*() methods return an integer exit value to the system + """ @property def status(self): @@ -78,94 +88,104 @@ class OnlineSigned(Signed): ask_overwrite = False, ask_write = False) - async def send(self, cfg, asi, batch=False): + async def send_op_status(self, cfg, idx, txid, txhex, op_val): + if cfg.verbose: + await self.post_network_send(txid) + else: + cfg._util.qmsg(f'{self.proto.coin} TxID: {txid.hl()}') + return await self.status.display(idx=idx) + + async def send_op_receipt(self, cfg, idx, txid, txhex, op_val): + if res := await self.get_receipt(txid, receipt_only=True): + import json + Msg(json.dumps(res, indent=4)) + return 0 + else: + msg(f'Unable to get receipt for TX {txid.hl()}') + return 1 + + async def send_op_dump_hex(self, cfg, idx, txid, txhex, op_val): + from ..fileutil import write_data_to_file + sfx = idx + 1 if idx else '' + write_data_to_file( + cfg, + ('-' if op_val == '-' else f'{op_val}{sfx}'), + txhex + '\n', + desc = 'serialized transaction hex data', + ask_overwrite = False, + ask_tty = False) + return 0 + + async def send_op_rpc_send(self, cfg, idx, txid, txhex, op_val): + msg(f'Sending TX: {txid.hl()}') + if cfg.test_suite_bogus_send: + msg(f'BOGUS transaction NOT sent: {txid.hl()}') + return 0 + else: + if idx: + await asyncio.sleep(1) + ret = await self.rpc_send(txhex) + msg(f'Transaction sent: {txid.hl()}') + return test_txid(ret, txid) + + async def send_op_rpc_test(self, cfg, idx, txid, txhex, op_val): + if await self.test_sendable(txhex): + gmsg('Transaction can be sent') + return 0 + else: + ymsg('Transaction cannot be sent') + return 1 + + async def send_op_proxy_send(self, cfg, idx, txid, txhex, op_val, *, test=False): + if idx and not cfg.test_suite: + await asyncio.sleep(2) + from .tx_proxy import send_tx + msg('{} TX: {}'.format('Testing' if test else 'Sending', txid.hl())) + ret = send_tx(cfg, txhex, test=test) + return 0 if test else test_txid(ret, txid) + + async def send_op_proxy_test(self, cfg, idx, txid, txhex, op_val): + return await self.send_op_proxy_send(cfg, idx, txid, txhex, op_val, test=True) + + async def send(self, cfg, op, op_val, asi, batch=False): """ returns an integer exit value to the system """ - status_exitval = None - sent_status = None - all_ok = True - idxs = ['', '2'] + sending = op in ('rpc_send', 'proxy_send') + idxs = ( + [int(cfg.txhex_idx) - 1] if cfg.txhex_idx else + [0, 1] if getattr(self, 'coin_txid2', None) else + [0]) + ret = [] + post_network_ret = [] - if cfg.txhex_idx and getattr(self, 'coin_txid2', None): - idxs = ['' if cfg.txhex_idx == '1' else cfg.txhex_idx] - - if not (cfg.status or cfg.receipt or cfg.dump_hex or cfg.test): + if sending: self.confirm_send(idxs) for idx in idxs: - if coin_txid := getattr(self, f'coin_txid{idx}', None): - txhex = getattr(self, f'serialized{idx}') - if cfg.status: - if cfg.verbose: - await self.post_network_send(coin_txid) - else: - cfg._util.qmsg(f'{self.proto.coin} TxID: {coin_txid.hl()}') - 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: - 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: - if idx != '' and not cfg.test_suite: - await asyncio.sleep(2) - from .tx_proxy import send_tx - msg('{} TX: {}'.format('Testing' if cfg.test else 'Sending', coin_txid.hl())) - if ret := send_tx(cfg, txhex, test=cfg.test): - if ret != coin_txid: - ymsg(f'Warning: txid mismatch (after sending) ({ret} != {coin_txid})') - sent_status = 'confirm_post_send' - if cfg.test: - break - elif cfg.test: - if await self.test_sendable(txhex): - gmsg('Transaction can be sent') - else: - ymsg('Transaction cannot be sent') - else: # node send - msg(f'Sending TX: {coin_txid.hl()}') - if cfg.test_suite_bogus_send: - msg(f'BOGUS transaction NOT sent: {coin_txid.hl()}') - else: - if idx != '': - await asyncio.sleep(1) - ret = await self.rpc_send(txhex) - msg(f'Transaction sent: {coin_txid.hl()}') - if ret != coin_txid: - die('TxIDMismatch', f'txid mismatch (after sending) ({ret} != {coin_txid})') - sent_status = 'no_confirm_post_send' + sfx = idx + 1 if idx else '' + txid = getattr(self, f'coin_txid{sfx}') + txhex = getattr(self, f'serialized{sfx}') + ret.append(await getattr(self, f'send_op_{op}')(cfg, idx, txid, txhex, op_val)) + if op == 'proxy_send' and cfg.test: + continue + if sending and cfg.wait: + post_network_ret.append(await self.post_network_send(txid)) - 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: + if sending and all(post_network_ret) and not cfg.txhex_idx: from ..ui import keypress_confirm - if sent_status == 'no_confirm_post_send' or not asi or keypress_confirm( + if op == 'rpc_send' or not asi or keypress_confirm( cfg, 'Mark transaction as sent on removable device?'): await self.post_send(asi) - if status_exitval is not None: - if cfg.verbose: - if batch: - self.info.view(pause=False, terse=True) - else: - self.info.view_with_prompt('View transaction details?', pause=False) - return status_exitval - return 0 + if op == 'status' and cfg.verbose: + if batch: + self.info.view(pause=False, terse=True) + else: + self.info.view_with_prompt('View transaction details?', pause=False) + + return max(ret) class AutomountOnlineSigned(AutomountSigned, OnlineSigned): pass