From 69a39fad5e2b4763af1e644eb4e0f85b82aefb7e Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 26 Apr 2025 10:38:55 +0000 Subject: [PATCH] txsend: add `--wait` and `--txhex-idx` options; related cleanups --- mmgen/main_txbump.py | 3 ++ mmgen/main_txdo.py | 3 ++ mmgen/main_txsend.py | 28 +++++++-------- mmgen/proto/btc/tx/online.py | 4 +-- mmgen/proto/btc/tx/status.py | 45 +++++++++++------------ mmgen/proto/eth/tx/base.py | 26 +++++++++++--- mmgen/proto/eth/tx/online.py | 24 ++++++++++--- mmgen/proto/eth/tx/status.py | 52 ++++++++++++--------------- mmgen/tx/online.py | 68 ++++++++++++++++++++++++++--------- mmgen/tx/tx_proxy.py | 10 +++--- pyproject.toml | 1 + test/cmdtest_d/ethbump.py | 9 ++--- test/cmdtest_d/ethdev.py | 70 +++++++++++++++--------------------- test/cmdtest_d/ethswap.py | 9 +++-- 14 files changed, 199 insertions(+), 153 deletions(-) diff --git a/mmgen/main_txbump.py b/mmgen/main_txbump.py index 3b39a9aa..7e0e8544 100755 --- a/mmgen/main_txbump.py +++ b/mmgen/main_txbump.py @@ -78,7 +78,10 @@ opts_data = { -- -q, --quiet Suppress warnings; overwrite files without prompting -- -s, --send Sign and send the transaction (the default if seed + 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 + e- -w, --wait Wait for transaction confirmation -- -W, --allow-non-wallet-swap Allow signing of swap transactions that send funds + to non-wallet addresses -- -x, --proxy=P Fetch the swap quote via SOCKS5 proxy ‘P’ (host:port) diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index 85557203..5319569f 100755 --- a/mmgen/main_txdo.py +++ b/mmgen/main_txdo.py @@ -90,11 +90,14 @@ opts_data = { + according to BIP 125) -s -s, --swap-proto Swap protocol to use (Default: {x_dfl}, + 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}, + maximum: {ss_max}). Only the default or first supplied + wallet is scanned for subseeds. -- -v, --verbose Produce more verbose output 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) e- -X, --cached-balances Use cached balances -- -y, --yes Answer 'yes' to prompts, suppress non-essential output diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 25cf512e..1b99f732 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -51,13 +51,16 @@ opts_data = { action has been successfully sent out-of-band. -n, --tx-proxy=P Send transaction via public TX proxy ‘P’ (supported proxies: {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 -r, --receipt Print the receipt of the sent transaction (Ethereum only) -s, --status Get status of a sent transaction (or current transaction, whether sent or unsent, when used with --autosign) -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 +-w, --wait Wait for transaction confirmation (Ethereum only) -x, --proxy=P Connect to TX proxy via SOCKS5 proxy ‘P’ (host:port) -y, --yes Answer 'yes' to prompts, suppress non-essential output """ @@ -141,22 +144,15 @@ async def main(): await tx.post_send(asi) 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 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 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.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) diff --git a/mmgen/proto/btc/tx/online.py b/mmgen/proto/btc/tx/online.py index b28ef37b..1c86a25e 100755 --- a/mmgen/proto/btc/tx/online.py +++ b/mmgen/proto/btc/tx/online.py @@ -76,8 +76,8 @@ class OnlineSigned(Signed, TxBase.OnlineSigned): msg(orange('\n'+errmsg)) 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): pass diff --git a/mmgen/proto/btc/tx/status.py b/mmgen/proto/btc/tx/status.py index 09888ad7..6cbde8a0 100755 --- a/mmgen/proto/btc/tx/status.py +++ b/mmgen/proto/btc/tx/status.py @@ -15,20 +15,19 @@ proto.btc.tx.status: Bitcoin transaction status class import time from ....tx import status as TxBase -from ....util import msg, suf, die +from ....util import msg, suf from ....util2 import format_elapsed_hr 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) - return retval - else: - die(retval, message) + return exitval + assert idx == '', f'multiple txhex not supported for {self.tx.proto}' tx = self.tx class r: @@ -82,25 +81,23 @@ class Status(TxBase.Status): return False 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: - 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(): - 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(): - 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(): msg('Transaction has been replaced') msg('Replacement transaction ' + ( @@ -117,4 +114,4 @@ class Status(TxBase.Status): d.append({}) for txid, mp_entry in zip(r.replacing_txs, d): msg(f' {txid}' + (' in mempool' if 'height' in mp_entry else '')) - return do_exit(0, '') + return do_return(0, '') diff --git a/mmgen/proto/eth/tx/base.py b/mmgen/proto/eth/tx/base.py index 0cf1ab35..ff4a8ba4 100755 --- a/mmgen/proto/eth/tx/base.py +++ b/mmgen/proto/eth/tx/base.py @@ -54,11 +54,29 @@ class Base(TxBase): def is_replaceable(self): 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 + tx = await self.rpc.call('eth_getTransactionByHash', '0x'+txid) return namedtuple('exec_status', ['status', 'gas_sent', 'gas_used', 'gas_price', 'contract_addr', 'tx', 'rx'])( diff --git a/mmgen/proto/eth/tx/online.py b/mmgen/proto/eth/tx/online.py index 312da93d..46910397 100755 --- a/mmgen/proto/eth/tx/online.py +++ b/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 ....color import orange +from ....color import yellow, green, orange from ....tx import online as TxBase from .. import erigon_sleep from .signed import Signed, TokenSigned @@ -42,9 +42,25 @@ class OnlineSigned(Signed, TxBase.OnlineSigned): await erigon_sleep(self) 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): diff --git a/mmgen/proto/eth/tx/status.py b/mmgen/proto/eth/tx/status.py index e3d98d96..225a81f2 100755 --- a/mmgen/proto/eth/tx/status.py +++ b/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 ....util import msg, Msg, die, suf, capfirst +from ....util import msg, suf, capfirst 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) - return retval - else: - die(retval, message) + return exitval tx = self.tx coin_txid = '0x' + getattr(tx, f'coin_txid{idx}') + tx_desc = 'transaction' + (f' {idx}' if idx else '') async def is_in_mempool(): if not 'full_node' in tx.rpc.caps: return False 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'): + def gen(key): + for e in res[key].values(): + for v in e.values(): + yield v['hash'] 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(): d = await tx.rpc.call('eth_getTransactionReceipt', coin_txid) @@ -50,26 +52,18 @@ class Status(TxBase.Status): rx = d) 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): pass diff --git a/mmgen/tx/online.py b/mmgen/tx/online.py index 14fb3875..6fb024c0 100755 --- a/mmgen/tx/online.py +++ b/mmgen/tx/online.py @@ -12,6 +12,11 @@ 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 class OnlineSigned(Signed): @@ -22,10 +27,7 @@ class OnlineSigned(Signed): return _base_proto_subclass('Status', 'status', self.proto)(self) def check_swap_expiry(self): - import time - from ..util import msg, make_timestr from ..util2 import format_elapsed_hr - from ..color import pink, yellow expiry = self.swap_quote_expiry now = int(time.time()) t_rem = expiry - now @@ -36,8 +38,7 @@ class OnlineSigned(Signed): c = make_timestr(expiry))) return t_rem >= 0 - def confirm_send(self): - from ..util import msg + def confirm_send(self, idxs): from ..ui import confirm_or_raise confirm_or_raise( 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', expect = 'YES' if self.cfg.quiet or self.cfg.yes else 'YES, I REALLY WANT TO DO THIS') 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): from . import SentTX @@ -55,21 +58,40 @@ class OnlineSigned(Signed): 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() - + status_exitval = None sent_status = None + all_ok = True + idxs = ['', '2'] - for idx in ('', '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') + + 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): 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: from ..fileutil import write_data_to_file write_data_to_file( @@ -80,29 +102,43 @@ class OnlineSigned(Signed): 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(f'Sending TX: {coin_txid.hl()}') if ret := send_tx(cfg, txhex): if ret != coin_txid: - from ..util import ymsg ymsg(f'Warning: txid mismatch (after sending) ({ret} != {coin_txid})') sent_status = 'confirm_post_send' elif cfg.test: await self.test_sendable(txhex) else: # node send + msg(f'Sending TX: {coin_txid.hl()}') if not cfg.bogus_send: + if idx != '': + await asyncio.sleep(1) 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()) 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 if sent_status == 'no_confirm_post_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: + self.info.view_with_prompt('View transaction details?', pause=False) + sys.exit(status_exitval) + class AutomountOnlineSigned(AutomountSigned, OnlineSigned): pass diff --git a/mmgen/tx/tx_proxy.py b/mmgen/tx/tx_proxy.py index d8b69c7e..fe900d44 100755 --- a/mmgen/tx/tx_proxy.py +++ b/mmgen/tx/tx_proxy.py @@ -71,14 +71,14 @@ class TxProxyClient: assert len(res) == 1, 'more than one matching form!' 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 write_data_to_file( self.cfg, - self.cache_fn(desc), + self.cache_fn(desc, extra_desc=extra_desc), data, desc = f'{desc} page from {orange(self.host)}') @@ -200,7 +200,7 @@ def send_tx(cfg, txhex): msg('done') 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 diff --git a/pyproject.toml b/pyproject.toml index e61d0e00..ae97a4d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ ignore = [ "test/*.py" = [ "F401" ] # imported but unused "test/colortest.py" = [ "F403", "F405" ] # `import *` used "test/tooltest2.py" = [ "F403", "F405" ] # `import *` used +"test/overlay/tree/*" = [ "ALL" ] [tool.pylint.format] indent-string = "\t" diff --git a/test/cmdtest_d/ethbump.py b/test/cmdtest_d/ethbump.py index a674aacb..7d1d9640 100755 --- a/test/cmdtest_d/ethbump.py +++ b/test/cmdtest_d/ethbump.py @@ -208,9 +208,7 @@ class CmdTestEthBump(CmdTestEthBumpMethods, CmdTestEthSwapMethods, CmdTestSwapMe ('token_deploy_a', 'deploying ERC20 token MM1 (SafeMath)'), ('token_deploy_b', 'deploying ERC20 token MM1 (Owned)'), ('token_deploy_c', 'deploying ERC20 token MM1 (Token)'), - ('wait_reth2', 'waiting for block'), ('token_fund_user', 'transferring token funds from dev to user'), - ('wait6', 'waiting for block'), ('token_addrgen', 'generating token addresses'), ('token_addrimport', 'importing token addresses using token address (MM1)'), ('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)', ('token_txdo1', 'creating, signing and sending a token transaction'), ('token_txbump1', 'bumping the token transaction (fee-bump)'), - ('wait7', 'waiting for block'), + ('wait6', 'waiting for block'), ('token_bal2', 'the token balance'), ), 'token_new_outputs': ( @@ -228,7 +226,7 @@ class CmdTestEthBump(CmdTestEthBumpMethods, CmdTestEthSwapMethods, CmdTestSwapMe ('token_txbump2', 'creating a replacement token transaction (new outputs)'), ('token_txbump2sign', 'signing the replacement transaction'), ('token_txbump2send', 'sending the replacement transaction'), - ('wait8', 'waiting for block'), + ('wait7', 'waiting for block'), ('token_bal3', 'the token balance'), ) } @@ -338,8 +336,7 @@ class CmdTestEthBump(CmdTestEthBumpMethods, CmdTestEthSwapMethods, CmdTestSwapMe def wait_reth1(self): 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 txsend1 = txsend2 = txbump1send = txbump2send = CmdTestEthBumpMethods._txsend diff --git a/test/cmdtest_d/ethdev.py b/test/cmdtest_d/ethdev.py index 3db6b061..d83ec155 100755 --- a/test/cmdtest_d/ethdev.py +++ b/test/cmdtest_d/ethdev.py @@ -240,8 +240,8 @@ class CmdTestEthdevMethods: gas, mmgen_cmd = 'txdo', gas_price = '8G', - num = None, - get_receipt = True): + num = None): + keyfile = joinpath(self.tmpdir, dfl_devkey_fn) fn = joinpath(self.tmpdir, 'mm'+str(num), key+'.bin') args = [ @@ -250,8 +250,8 @@ class CmdTestEthdevMethods: f'--gas={gas}', f'--contract-data={fn}', f'--inputs={dfl_devaddr}', - '--yes', - ] + '--yes' + ] + (['--wait'] if mmgen_cmd == 'txdo' else []) contract_addr = self._get_contract_address(dfl_devaddr) if key == 'Token': @@ -271,36 +271,30 @@ class CmdTestEthdevMethods: txfile = txfile.replace('.rawtx', '.sigtx') 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, quiet = mmgen_cmd == 'txdo' or not self.cfg.debug, + contract_addr = contract_addr, 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': imsg(f'\nToken MM{num} deployed!') 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( - 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( - 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( - 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): return self._bal_check(pat=pat, add_opts=['--token=MM1']) @@ -315,7 +309,7 @@ class CmdTestEthdevMethods: caller = cmd, 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) from mmgen.tool.wallet import tool_cmd usr_mmaddrs = [f'{sid}:E:{i}' for i in mm_idxs] @@ -338,9 +332,14 @@ class CmdTestEthdevMethods: key = dfl_devkey, gas = self.proto.coin_amt(120000, from_unit='wei'), 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): for i in range(len(usr_mmaddrs)): @@ -1114,13 +1113,13 @@ class CmdTestEthdev(CmdTestEthdevMethods, CmdTestBase, CmdTestShared): def bal3(self): 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() ext = ext.format('-α' if self.cfg.debug_utf8 else '') txfile = self.get_file_with_ext(ext, no_dot=True) t = self.spawn( 'mmgen-txsend', - self.eth_opts + ['--status', txfile], + self.eth_opts + add_opts + ['--status', txfile], no_passthru_opts = ['coin'], exit_val = exit_val) t.expect(expect_str) @@ -1129,7 +1128,10 @@ class CmdTestEthdev(CmdTestEthdevMethods, CmdTestBase, CmdTestShared): return t 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): 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} 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): return await self._token_deploy_math(num=1) diff --git a/test/cmdtest_d/ethswap.py b/test/cmdtest_d/ethswap.py index d411c1af..ac472ffb 100755 --- a/test/cmdtest_d/ethswap.py +++ b/test/cmdtest_d/ethswap.py @@ -34,20 +34,19 @@ def {name}(self): class CmdTestEthSwapMethods: 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): - 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): - return await self._token_deploy_token(num=1, get_receipt=False) + return await self._token_deploy_token(num=1) def token_fund_user(self): return self._token_transfer_ops( op = 'fund_user', mm_idxs = [1], - amt = self.token_fund_amt, - get_receipt = False) + amt = self.token_fund_amt) def token_addrgen(self): return self._token_addrgen(mm_idxs=[1], naddrs=5)