XMR compat: encapsulate legacy transaction data

With transaction operations, the compatibility layer formerly handed control
to the legacy xmrwallet code, which handled the writing and parsing of
transaction files.

Now control is returned to the caller, which takes care of all transaction file
operations.  This allows for the inclusion of metadata required to properly
display transaction details and results in more integrated program flow and a
more consistent user experience.

The legacy code transparently handles the new transaction file format, so that
backwards compatibility is maintained.

The transaction version has been bumped. Offline installations must be upgraded.
This commit is contained in:
The MMGen Project 2026-10-23 17:01:32 +00:00
commit f17416cad6
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
24 changed files with 293 additions and 176 deletions

View file

@ -12,6 +12,7 @@ autosign.signable: Signable class for MMGen Wallet autosigning
import sys
from ..tx import UnsignedTX, SignedTX
from ..util import msg, msg_r, gmsg, bmsg, die, suf, fmt_list
from ..color import yellow, red, orange
@ -244,7 +245,9 @@ class Signable:
def print_summary(self, signables):
bmsg('\nAutosign summary:')
msg('\n'.join(s.get_info(indent=' ') for s in signables) + self.summary_footer)
msg('\n'.join(
s.get_info(indent=' ') if hasattr(s, 'get_info') else s.info.format()
for s in signables) + self.summary_footer)
class xmr_transaction(xmr_signable, automount_transaction):
desc = 'Monero non-compat transaction'
@ -277,6 +280,13 @@ class Signable:
sigext = 'asigtx'
subext = 'asubtx'
async def sign(self, f, compat_call=False):
tx1 = UnsignedTX(cfg=self.parent.xmrwallet_cfg, filename=f)
tx2 = SignedTX(cfg=self.parent.xmrwallet_cfg, data=tx1.__dict__, automount=self.automount)
tx2.compat_data = await self.do_sign(f, compat_call=compat_call)
tx2.file.write(ask_write=False, outdir=self.dir)
return tx2
class xmr_wallet_outputs_file(xmr_signable, base):
desc = 'Monero wallet outputs file'
dir_name = 'xmr_outputs_dir'

View file

@ -1 +1 @@
16.3.0dev19
16.3.0dev20

View file

@ -187,7 +187,7 @@ async def main():
tx2 = NewTX(cfg=rune_cfg, proto=rune_cfg._proto, target='tx', refreq_src=tx1)
tx3 = await tx2.create(args=('deposit,0', f'data:{tx1.reference_memo}'))
write_to_file(tx3, asi)
elif not tx1.is_compat:
else:
write_to_file(tx2, asi)
async_run(cfg, main)

View file

@ -156,10 +156,8 @@ async def process_tx(tx):
do_sep()
cfg._util.vmsg(f'Getting {tx.desc} ‘{tx.infile}’')
if tx.is_compat:
if not cfg.autosign:
die(1, 'Sending XMR transaction via compatibility layer requires --autosign')
return await tx.compat_send()
if tx.is_compat and not cfg.autosign:
die(1, 'Sending XMR transaction via compatibility layer requires --autosign')
txcfg = Config({'_clone': cfg, 'proto': tx.proto, 'coin': tx.proto.coin})
@ -167,8 +165,9 @@ async def process_tx(tx):
from .tx.tx_proxy import check_client
check_client(txcfg)
from .rpc import rpc_init
tx.rpc = await rpc_init(txcfg)
if 'rpc_init' in txcfg.proto.mmcaps:
from .rpc import rpc_init
tx.rpc = await rpc_init(txcfg)
if not post_send_op:
if cfg.mark_sent:
@ -187,7 +186,7 @@ async def process_tx(tx):
if not cfg.yes:
tx.info.view_with_prompt('View transaction details?')
# edits existing comment, returns true if changed:
if tx.add_comment() and not cfg.autosign:
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)

View file

@ -13,7 +13,27 @@ proto.xmr.tx.base: Monero base transaction class
class Base:
is_compat = True
has_comment = False
compat_data_label = 'MoneroMMGenTX'
raw_compat_data = None
@property
def nondata_outputs(self):
return ()
def is_replaceable(self):
return False
def get_serialized_locktime(self):
return None
def check_serialized_integrity(self):
pass
def check_sigs(self):
return bool(getattr(self.compat_tx.data, 'signed_txset', None))
def add_sent_timestamp(self):
pass
def add_blockcount(self):
pass

View file

@ -10,27 +10,24 @@
proto.xmr.tx.completed: Monero completed transaction class
"""
from ....cfg import Config
from ....util import cached_property, make_timestamp
from ....color import cyan
from ....tx.completed import Completed as TxCompleted
from .base import Base
class Completed(Base):
def __init__(self, cfg, *args, proto, filename, **kwargs):
self.cfg = Config({
'_clone': cfg,
'coin': 'XMR',
'network': proto.network})
self.proto = proto
self.filename = filename
self.infile = filename
class Completed(Base, TxCompleted):
@cached_property
def compat_tx(self):
from ....xmrwallet.file.tx import MoneroMMGenTX
from pathlib import Path
return MoneroMMGenTX.View(self.cfg, Path(self.filename)) # View = Completed with silent open
if self.compat_data:
return MoneroMMGenTX.ViewData(self.cfg, self.compat_data)
elif self.raw_compat_data:
return MoneroMMGenTX.Completed(self.cfg, None, compat_data=self.raw_compat_data)
else:
from pathlib import Path
return MoneroMMGenTX.View(self.cfg, Path(self.infile))
@cached_property
def timestamp(self):
@ -45,5 +42,10 @@ class Completed(Base):
from ....tx.info import TxInfo
class info_cls(TxInfo):
def format(info_self, *args, **kwargs):
return self.compat_tx.get_info(full_address=True, indent=' ', is_swap=self.is_swap)
body = self.compat_tx.get_info(full_address=True, indent=' ', is_swap=self.is_swap)
if self.compat_data or self.raw_compat_data:
swap_subhdr = ''.join(info_self.gen_swap_subhdr()) + '\n' if self.is_swap else ''
return cyan(self.data_desc.upper()) + '\n' + swap_subhdr + body
else:
return body
return info_cls(self.cfg, self)

View file

@ -10,43 +10,23 @@
proto.xmr.tx.online: Monero online signed transaction class
"""
from ....xmrwallet import op as xmrwallet_op
from ....tx.online import OnlineSigned as TxOnlineSigned
from .completed import Completed
class OnlineSigned(Completed):
desc = 'signed transaction'
ext = 'asigtx'
class OnlineSigned(Completed, TxOnlineSigned):
async def compat_send(self):
"""
returns integer exit val to system
"""
from ....xmrwallet import op as xmrwallet_op
op_name = 'daemon' if self.cfg.status else 'submit'
op = xmrwallet_op(op_name, self.cfg, self.filename, None, compat_call=True)
if self.cfg.status:
from ....util import msg, msg_r, ymsg, suf
ret = 0
txid = self.compat_tx.data.txid
if not (self.cfg.verbose or self.cfg.quiet):
from ....obj import CoinTxID
msg('{} TxID: {}'.format(self.cfg.coin, CoinTxID(txid).hl()))
res = op.dc.call_raw('get_transactions', txs_hashes=[txid])
if res['status'] == 'OK':
tx = res['txs'][0]
if tx['in_pool']:
msg('Transaction is in mempool')
else:
confs = tx['confirmations']
msg('Transaction has {} confirmation{}'.format(confs, suf(confs)))
else:
ymsg('An RPC error occurred while fetching transaction data')
ret = 1
if self.cfg.verbose:
msg_r('\n' + self.info.format())
return ret
else:
await op.restart_wallet_daemon()
return int(not await op.main())
async def post_network_send(self, coin_txid):
return True
async def check_memoless_presend(self):
return await super().check_memoless_presend(amt=self.send_amt)
async def send_with_node(self, txhex):
op = xmrwallet_op('submit', self.cfg, self.infile, None, compat_call=True)
await op.restart_wallet_daemon()
self.compat_data = await op.main()
return self.compat_data['data']['txid']
class AutomountOnlineSigned(OnlineSigned):
desc = 'signed automount transaction'

View file

@ -0,0 +1,23 @@
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2026 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
proto.xmr.tx.signed: Monero signed transaction class
"""
from ....tx import signed as TxBase
from .completed import Completed
class Signed(Completed, TxBase.Signed):
def parse_txfile_serialized_data(self):
pass
class AutomountSigned(Signed):
desc = 'signed automount transaction'
ext = 'asigtx'

View file

@ -0,0 +1,35 @@
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2026 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
proto.xmr.tx.status: Monero transaction status class
"""
from ....util import msg, ymsg, suf
from ....tx import status as TxBase
from ....xmrwallet import op as xmrwallet_op
class Status(TxBase.Status):
async def display(self, *, idx=''):
tx = self.tx
if tx.cfg.verbose:
msg(f'{tx.proto.coin} TxID: {tx.coin_txid.hl()}')
op = xmrwallet_op('daemon', tx.cfg, tx.infile, None, compat_call=True)
res = op.dc.call_raw('get_transactions', txs_hashes=[tx.coin_txid])
if res['status'] == 'OK':
data = res['txs'][0]
if data['in_pool']:
msg('Transaction is in mempool')
else:
confs = data['confirmations']
msg(f'Transaction has {confs} confirmation{suf(confs)}')
else:
ymsg('An RPC error occurred while fetching transaction data')
return 1
return 0

View file

@ -18,7 +18,7 @@ class Completed(Base):
signed or unsigned transaction with associated file
"""
filename_api = True
txfile_version = 4
txfile_version = 5
def __init__(self, cfg, *args, filename=None, data=None, quiet_open=False, **kwargs):
@ -144,6 +144,7 @@ class Completed(Base):
class DummyCompleted: # required by MMGenTxFile.get_proto()
desc = 'dummy transaction'
is_compat = False
def __init__(self, cfg):
self.cfg = cfg

View file

@ -64,19 +64,20 @@ class MMGenTxFile(MMGenObject):
'timestamp': None,
'blockcount': None,
'serialized': None}
extra_attrs = {
'locktime': None,
'comment': MMGenTxComment,
'coin_txid': CoinTxID,
'sent_timestamp': None,
'is_deposit': None,
'is_swap': None,
'is_refreq': None}
def __init__(self, tx):
self.tx = tx
self.fmt_data = None
self.filename = None
self.extra_attrs = {
'locktime': None,
'comment': MMGenTxComment,
'is_deposit': None,
'is_swap': None,
'is_refreq': None} | (
{} if tx.is_compat else {
'coin_txid': CoinTxID,
'sent_timestamp': None})
def parse(self, infile, *, get_proto_only=False, quiet_open=False):
tx = self.tx
@ -85,9 +86,10 @@ class MMGenTxFile(MMGenObject):
if len(data) > tx.cfg.max_tx_file_size:
die('MaxFileSizeExceeded',
f'Transaction file size exceeds limit ({tx.cfg.max_tx_file_size} bytes)')
return (self.parse_data_json if data[0] == '{' else self.parse_data_legacy)(data, get_proto_only)
return (self.parse_data_json if data[0] == '{' else self.parse_data_legacy)(
data, infile, get_proto_only)
def parse_data_json(self, data, get_proto_only):
def parse_data_json(self, data, infile, get_proto_only):
tx = self.tx
tx.file_format = 'json'
@ -98,6 +100,7 @@ class MMGenTxFile(MMGenObject):
tx.cfg,
'XMR',
network = outer_data[self.xmr_data_label]['data']['network'])
tx.infile = infile
return None
data = outer_data[self.data_label]
@ -105,7 +108,12 @@ class MMGenTxFile(MMGenObject):
chk = make_chksum_6(txfile_json_dumps(data))
die(3, f'{self.data_label}: invalid checksum for TxID {data["txid"]} ({chk} != {outer_data["chksum"]})')
tx.proto = get_proto_from_coin_id(tx, data['coin_id'], data['chain'])
if self.xmr_data_label in data:
compat_data = data[self.xmr_data_label]
tx.proto = get_proto_from_coin_id(tx, data['coin_id'], compat_data['data']['network'])
else:
compat_data = None
tx.proto = get_proto_from_coin_id(tx, data['coin_id'], data['chain'])
if get_proto_only:
return
@ -113,9 +121,19 @@ class MMGenTxFile(MMGenObject):
if data.get('version', 0) != tx.txfile_version:
tx.txfile_version_chk(data.get('version', 0), tx.txfile_version)
for k, v in self.attrs.items():
if v != 'skip':
setattr(tx, k, v(data[k]) if v else data[k])
if compat_data:
cdata = compat_data['data']
tx.raw_compat_data = compat_data
tx.txid = MMGenTxID(compat_data['base_chksum'].upper())
tx.chain = cdata['network']
tx.coin_txid = CoinTxID(cdata['txid'])
tx.send_amt = tx.proto.coin_amt(cdata['amount'])
tx.serialized = cdata['unsigned_txset'] or cdata['signed_txset']
tx.blockcount = None
else:
for k, v in self.attrs.items():
if v != 'skip':
setattr(tx, k, v(data[k]) if v else data[k])
for k, v in self.extra_attrs.items():
if k in data:
@ -126,14 +144,13 @@ class MMGenTxFile(MMGenObject):
if k in data:
setattr(tx, k, v(data[k]) if v else data[k])
for k in ('inputs', 'outputs'):
setattr(tx, k, eval_io_data(tx, data[k], desc=k))
if not compat_data:
for k in ('inputs', 'outputs'):
setattr(tx, k, eval_io_data(tx, data[k], desc=k))
tx.check_txfile_hex_data()
tx.parse_txfile_serialized_data() # Ethereum RLP or JSON data
tx.check_txfile_hex_data()
tx.parse_txfile_serialized_data() # Ethereum RLP or JSON data
def parse_data_legacy(self, data, get_proto_only):
def parse_data_legacy(self, data, infile, get_proto_only):
tx = self.tx
tx.file_format = 'legacy'
@ -284,7 +301,7 @@ class MMGenTxFile(MMGenObject):
data = txfile_json_dumps({
'coin_id': coin_id,
'version': tx.txfile_version
} | (tx.compat_data or ({
} | ({self.xmr_data_label: tx.compat_data} if tx.compat_data else ({
k: getattr(tx, k) for k in self.attrs
} | {
'inputs': [e._asdict() for e in tx.inputs],

View file

@ -513,11 +513,6 @@ class New(Base):
if not (self.is_bump or self.cfg.inputs):
await self.twuo.view_filter_and_sort()
if self.is_sweep:
del self.twctl
del self.twuo.twctl
return await self.compat_create()
if not self.is_bump:
self.twuo.display_total()
@ -527,27 +522,26 @@ class New(Base):
import sys
sys.exit(0)
outputs_sum = self.sum_outputs()
msg('Total amount to spend: {}'.format(
f'{outputs_sum.hl()} {self.dcoin}' if outputs_sum else 'Unknown'))
while True:
if not await self.get_inputs(outputs_sum):
continue
if self.is_swap:
fee_hint = await self.update_vault_output(
self.vault_output.amt or self.sum_inputs(),
deduct_est_fee = self.vault_output == self.chg_output)
else:
await self.set_gas()
fee_hint = None
desc = 'User-selected' if self.cfg.fee else 'Recommended' if fee_hint else None
if (funds_left := await self.get_funds_remaining(
self.cfg.fee or fee_hint,
outputs_sum,
desc)) is not None:
break
if not self.is_sweep:
outputs_sum = self.sum_outputs()
msg('Total amount to spend: {}'.format(
f'{outputs_sum.hl()} {self.dcoin}' if outputs_sum else 'Unknown'))
while True:
if not await self.get_inputs(outputs_sum):
continue
if self.is_swap:
fee_hint = await self.update_vault_output(
self.vault_output.amt or self.sum_inputs(),
deduct_est_fee = self.vault_output == self.chg_output)
else:
await self.set_gas()
fee_hint = None
desc = 'User-selected' if self.cfg.fee else 'Recommended' if fee_hint else None
if (funds_left := await self.get_funds_remaining(
self.cfg.fee or fee_hint,
outputs_sum,
desc)) is not None:
break
if not self.is_compat:
self.check_non_mmgen_inputs(caller=caller)
@ -566,16 +560,15 @@ class New(Base):
await self.update_vault_output(self.vault_output.amt)
if self.is_compat:
del self.twctl
del self.twuo.twctl
return await self.compat_create()
await self.create_serialized(locktime=locktime) # creates self.txid too
self.add_timestamp()
self.add_blockcount()
self.chain = self.proto.chain_name
self.check_fee()
self.compat_data = await self.compat_create()
self.send_amt = self.compat_data['data']['amount']
self.txid = self.compat_data['base_chksum'].upper()
else:
await self.create_serialized(locktime=locktime) # creates self.txid too
self.add_timestamp()
self.add_blockcount()
self.chain = self.proto.chain_name
self.check_fee()
self.cfg._util.qmsg('Transaction successfully created')

View file

@ -32,8 +32,8 @@ class OnlineSigned(Signed):
from . import _base_proto_subclass
return _base_proto_subclass('Status', 'status', proto=self.proto)(self)
async def check_memoless_presend(self):
data = self.swap_proto_rpc.do_validate_amt(self.send_asset, self.vault_output.amt)
async def check_memoless_presend(self, *, amt=None):
data = self.swap_proto_rpc.do_validate_amt(self.send_asset, amt or self.vault_output.amt)
if data['reference'] != self.refnum:
die(3, f'registered reference number ({data["reference"]}) does not match ours ({self.refnum})')
if not data['available']:

View file

@ -57,4 +57,4 @@ class MoneroMMGenFile:
def extract_data_from_file(self, cfg, fn):
data = json.loads(get_data_from_file(cfg, str(fn), desc=self.desc, silent=self.silent_load))
return data[self.data_label]
return data.get(self.data_label) or data['MMGenTransaction'][self.data_label]

View file

@ -255,17 +255,20 @@ class MoneroMMGenTX:
desc = 'transaction'
forbidden_fields = ()
def __init__(self, cfg, fn):
def __init__(self, cfg, fn, *, compat_data=None):
super().__init__()
self.cfg = cfg
try:
d_wrap = self.extract_data_from_file(cfg, fn)
except Exception as e:
die('MoneroMMGenTXFileParseError',
f'{type(e).__name__}: {e}\nCould not load transaction file')
if compat_data:
d_wrap = compat_data
else:
try:
d_wrap = self.extract_data_from_file(cfg, fn)
except Exception as e:
die('MoneroMMGenTXFileParseError',
f'{type(e).__name__}: {e}\nCould not load transaction file')
if 'unsigned_txset' in d_wrap['data']: # post-autosign
self.full_chksum_fields &= set(d_wrap['data']) # allow for added chksum fields in future
@ -339,6 +342,12 @@ class MoneroMMGenTX:
class View(Completed):
silent_load = True
class ViewData(Completed):
def __init__(self, cfg, data):
MoneroMMGenTX.Base.__init__(self)
self.cfg = cfg
self.data = self.xmrwallet_tx_data(**data['data'])
class UnsignedCompat(Unsigned):
ext = 'arawtx'

View file

@ -37,4 +37,6 @@ class OpSign(OpWallet):
unsigned_txset = None,
signed_txset = res['signed_txset'],
_in_tx = tx)
if self.compat_call:
return new_tx.make_outer_data()
return new_tx

View file

@ -86,12 +86,11 @@ class OpSubmit(OpWallet):
h = MoneroWalletRPC(self, self.kal.entry(tx.src_wallet_idx))
h.open_wallet('TX-relay-configured watch-only', refresh=False)
msg('\n' + tx.get_info(indent=' '))
if self.cfg.tx_relay_daemon:
self.display_tx_relay_info(indent=' ')
keypress_confirm(self.cfg, f'{self.name.capitalize()} transaction?', do_exit=True)
if not self.compat_call:
msg('\n' + tx.get_info(indent=' '))
if self.cfg.tx_relay_daemon:
self.display_tx_relay_info(indent=' ')
keypress_confirm(self.cfg, f'{self.name.capitalize()} transaction?', do_exit=True)
if self.cfg.tx_relay_daemon:
msg_r('Relaying transaction to remote daemon, please be patient...')
@ -107,6 +106,10 @@ class OpSubmit(OpWallet):
new_tx = self.get_tx_cls('NewSubmitted')(cfg=self.cfg, _in_tx=tx)
gmsg('\nOK')
if self.compat_call:
return new_tx.make_outer_data()
new_tx.write(
ask_write = not self.cfg.autosign,
ask_overwrite = not self.cfg.autosign)

View file

@ -196,14 +196,16 @@ class OpSweep(OpMixinSpec, OpWallet):
h, new_tx = self.create_tx(h, wallet_data)
msg('\n' + new_tx.get_info(indent=' '))
if not self.compat_call:
msg('\n' + new_tx.get_info(indent=' '))
if self.cfg.tx_relay_daemon:
self.display_tx_relay_info(indent=' ')
if not self.compat_call:
msg('Saving TX data to file')
if self.compat_call:
return new_tx.make_outer_data()
msg('Saving TX data to file')
new_tx.write(delete_metadata=True)
if self.cfg.no_relay or self.cfg.autosign:

View file

@ -601,18 +601,16 @@ class CmdTestAutosignThreaded(CmdTestAutosignBase):
[f'--{user}', '--quiet', '--autosign'] + extra_opt + add_opts, no_passthru_opts=['coin'])
if '--status' in add_opts:
if '--verbose' in add_opts:
t.expect('view: ', 't')
if expect_pat:
t.expect(expect_pat, regex=True)
elif mark_sent:
t.written_to_file('Sent automount transaction')
elif is_xmr:
if expect_pat:
t.expect(expect_pat, regex=True)
t.expect('Submit transaction? (y/N): ', 'y')
t.written_to_file('Submitted transaction')
else:
t.view_tx('t', expect_pat=expect_pat)
t.do_comment(comment)
if not is_xmr:
t.do_comment(comment)
if dump_hex:
t.written_to_file('Serialized transaction hex data')
else:

View file

@ -69,7 +69,9 @@ class CmdTestMisc(CmdTestBase):
('bch_txview_cashaddr1', '‘mmgen-tool --coin=bch --cashaddr=1 txview terse=0’'),
('bch_txview_cashaddr2', '‘mmgen-tool --coin=bch --cashaddr=1 txview terse=1’'),
('xmr_txview', '‘mmgen-tool txview’ with XMR transactions'),
('xmr_txview_compat', '‘mmgen-tool txview’ with XMR compat transactions'),
('xmrwallet_txview', '‘mmgen-xmrwallet txview’'),
('xmrwallet_txview_compat', '‘mmgen-xmrwallet txview’ with XMR compat transactions'),
('xmrwallet_txlist', '‘mmgen-xmrwallet txlist’'),
('coin_daemon_info', '‘examples/coin-daemon-info.py’'),
('examples_bip_hd', '‘examples/bip_hd.py’'),
@ -105,6 +107,9 @@ class CmdTestMisc(CmdTestBase):
def bch_txview_cashaddr2(self):
return self._bch_txview(1, 1, '[1HpynST7vkLn8yNtdrqPfeghexZk4sdB3W]')
def xmr_txview_compat(self):
return self._xmr_txview(tdir='test/ref/monero/compat', ext='tx')
def xmr_txview(self):
return self._xmr_txview(tdir='test/ref/monero', ext='tx')
@ -122,6 +127,11 @@ class CmdTestMisc(CmdTestBase):
def xmrwallet_txview(self):
return self._xmrwallet_txview(search_list=('Amount: 0.74 XMR', 'Dest: 56VQ9M6k'))
def xmrwallet_txview_compat(self):
return self._xmrwallet_txview(
tdir = 'test/ref/monero/compat',
search_list = ('Amount: 0.234499978787 XMR', 'Dest: 477PUpbuezBNte'))
def _xmrwallet_txview(self, tdir='test/ref/monero', op='txview', search_list=None, search_pat=None):
if self.cfg.no_altcoin:
return 'skip'

View file

@ -24,6 +24,7 @@ from ..include.common import (
read_from_file)
from .include.common import get_file_with_ext, cleanup_env
from .shared import CmdTestShared
from .xmrwallet import CmdTestXMRWallet
from .autosign import CmdTestAutosignThreaded
@ -35,7 +36,7 @@ def make_burn_addr(cfg):
proto = cfg._proto,
mmtype = 'monero').privhex2addr('beadcafe'*8)
class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded, CmdTestShared):
"""
Monero autosigning operations (xmrwallet compat mode)
"""
@ -119,6 +120,7 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
self.autosign_opts = ['--autosign'] # mmgen-xmrwallet opts
self.autosign_setup_opts = ['--regtest=1', f'--xmrwallets={self.users[self.user].kal_range}']
self.spawn_env['MMGEN_TEST_SUITE_BOGUS_SEND'] = ''
def create_tmp_wallets(self):
self.spawn(msg_only=True)
@ -849,13 +851,13 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
user_txsend1 = user_txsend2 = user_txsend3 = user_txsend4 = _user_txsend
def user_txstatus1(self):
return self._user_txstatus(expect_str='TxID: .* in mempool')
return self._user_txstatus(expect_str='in mempool')
def user_txstatus2(self):
return self._user_txstatus(['--quiet'], expect_str='confirmations')
def user_txstatus3(self):
return self._user_txstatus(['--verbose'], expect_str='confirmations.*Info for transaction')
return self._user_txstatus(['--verbose'], expect_str='TRANSACTION DATA')
def _user_txstatus(self, add_opts=[], expect_str=None):
return self._user_txops(
@ -900,6 +902,9 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
pass
elif '--abort' in opts:
t.expect('(y/N): ', 'y')
elif '--status' in opts:
if '--verbose' in add_opts:
t.expect('view: ', 't')
elif op == 'txcreate':
if sweep_type:
t.expect(self.menu_prompt, menu)
@ -911,9 +916,11 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
for ch in menu + 'q':
t.expect(self.menu_prompt, ch)
t.expect('to spend from: ', f'{acct_num}\n')
t.view_tx('y')
t.expect('(y/N): ', 'y') # save?
elif op == 'txsend' and not '--status' in opts:
t.expect('(y/N): ', 'y') # view?
elif op == 'txsend':
t.view_tx('y')
self._do_confirm_send(t)
if expect_str:
t.expect(expect_str, regex=True)

View file

@ -44,15 +44,11 @@ class CmdTestXMRSwapMethods:
t.expect(prompt, 'q')
t.expect('to spend: ', f'{inputs}\n')
t.expect('continue: ', '\n')
if tx_type == 'xmr_swap':
for pat in expect_pats:
t.expect(pat, regex=True)
t.expect('(y/N): ', 'y') # save?
t.written_to_file('Unsigned transaction')
return t
t.expect('(Y/n): ', 'y') # fee OK?
t.expect('(Y/n): ', 'y') # change OK?
t.expect('(y/N): ', 'n') # add comment?
if tx_type != 'xmr_swap':
t.expect('(Y/n): ', 'y') # fee OK?
t.expect('(Y/n): ', 'y') # change OK?
if tx_type != 'xmr_swap':
t.expect('(y/N): ', 'n') # add comment?
t.expect('view: ', 't')
for pat in expect_pats:
t.expect(pat, regex=True)
@ -217,8 +213,10 @@ class CmdTestXMRSwap(CmdTestXMRSwapMethods, CmdTestSwap):
('swaptxcreate_refnum7_bad', 'creating an XMR->LTC swap transaction (bad, no amount)'),
('swaptxcreate_refnum7', 'creating an XMR->LTC swap transaction (auto chg, auto dest)'),
('swaptxsend_refnum7', 'sending the transaction'),
('swaptxstatus_refnum7a', 'getting the transaction status (mempool)'),
('xmr_mine_blocks_10', ''),
('swaptxstatus_refnum7', 'getting the transaction status'))}
('swaptxstatus_refnum7b', 'getting the transaction status (confirmations)'),
('swaptxstatus_refnum7c', 'getting the transaction status (--verbose)'))}
exec(create_cross_methods(cross_coin, cross_group, cmd_group_in, cmd_subgroups))
@ -349,8 +347,14 @@ class CmdTestXMRSwap(CmdTestXMRSwapMethods, CmdTestSwap):
def swaptxsend_refnum7(self):
return self._swaptxsend_refnum(r'Amount:.*0.234499978787', is_xmr=True)
def swaptxstatus_refnum7(self):
return self._swaptxsend_refnum(r'Amount:.*0.234499978787', add_opts=['--status', '--verbose'])
def swaptxstatus_refnum7a(self):
return self._swaptxsend_refnum('in mempool', add_opts=['--status'], no_wait=True)
def swaptxstatus_refnum7b(self):
return self._swaptxsend_refnum('confirmations', add_opts=['--status'], no_wait=True)
def swaptxstatus_refnum7c(self):
return self._swaptxsend_refnum(None, add_opts=['--status', '--verbose'], no_wait=True)
class CmdTestXMRSwapXMR(CmdTestXMRSwapMethods, CmdTestSwapMethods, CmdTestXMRCompat):
'Monero swap operations - Monero wallet'

View file

@ -1,21 +1,22 @@
{
"MoneroMMGenTX": {
"base_chksum": "3ebd06",
"full_chksum": "2d6e3b",
"data": {
"op": "sweep",
"create_time": 1665169030,
"sign_time": 1665169030,
"network": "testnet",
"seed_id": "FE3C6545",
"source": "2:1:None",
"dest": "3:0:0",
"dest_address": "56VQ9M6k3C2H7awPLUcUKA86fkh4NqqJ3SQhVnFw7odfRVj3Xkpb3rv4J18o7c5KUBa36eCNpGDo16nKPvJgYHorEuu9WZj",
"txid": "63ca48f93968788d7756328e55947f50c939ef764d5049ad7d43c03c7202f779",
"amount": "0.74",
"fee": "0.002",
"blob": "deadbeef",
"metadata": null
}
"MoneroMMGenTX": {
"base_chksum": "3ebd06",
"full_chksum": "d2a255",
"data": {
"op": "sweep",
"create_time": 1665169030,
"sign_time": 1665169030,
"network": "testnet",
"seed_id": "FE3C6545",
"source": "2:1:None",
"dest": "3:0:0",
"dest_address": "56VQ9M6k3C2H7awPLUcUKA86fkh4NqqJ3SQhVnFw7odfRVj3Xkpb3rv4J18o7c5KUBa36eCNpGDo16nKPvJgYHorEuu9WZj",
"txid": "63ca48f93968788d7756328e55947f50c939ef764d5049ad7d43c03c7202f779",
"amount": "0.74",
"fee": "0.002",
"blob": "deadbeef",
"signed_txset": "beadcafe",
"metadata": null
}
}
}

File diff suppressed because one or more lines are too long