txsend: improve options handling; tx.online: reimplement send()

This commit is contained in:
The MMGen Project 2026-10-27 13:30:14 +00:00
commit 5e326df10f
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
10 changed files with 139 additions and 100 deletions

View file

@ -1 +1 @@
16.3.0dev22
16.3.0dev23

View file

@ -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

View file

@ -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:

View file

@ -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')

View file

@ -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:

View file

@ -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')

View file

@ -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:

View file

@ -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)

View file

@ -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()}')

View file

@ -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