diff --git a/mmgen/autosign.py b/mmgen/autosign.py index b0aacfc4..d0ac4b7b 100755 --- a/mmgen/autosign.py +++ b/mmgen/autosign.py @@ -189,6 +189,11 @@ class Signable: cfg = self.cfg, filename = f, automount = self.automount) + if tx1.proto.coin == 'XMR': + ctx = Signable.xmr_compat_transaction(self.parent) + for k in ('desc', 'print_summary', 'print_bad_list'): + setattr(self, k, getattr(ctx, k)) + return await ctx.sign(f, compat_call=True) if tx1.proto.sign_mode == 'daemon': from .rpc import rpc_init tx1.rpc = await rpc_init(self.cfg, tx1.proto, ignore_wallet=True) @@ -357,7 +362,7 @@ class Signable: sigext = 'sigtx' subext = 'subtx' - async def sign(self, f): + async def sign(self, f, compat_call=False): from . import xmrwallet from .xmrwallet.file.tx import MoneroMMGenTX tx1 = MoneroMMGenTX.Completed(self.parent.xmrwallet_cfg, f) @@ -365,11 +370,19 @@ class Signable: 'sign', self.parent.xmrwallet_cfg, infile = str(self.parent.wallet_files[0]), # MMGen wallet file - wallets = str(tx1.src_wallet_idx)) + wallets = str(tx1.src_wallet_idx), + compat_call = compat_call) tx2 = await m.main(f, restart_daemon=self.need_daemon_restart(m, tx1.src_wallet_idx)) tx2.write(ask_write=False) return tx2 + class xmr_compat_transaction(xmr_transaction): + desc = 'Monero compat transaction' + dir_name = 'txauto_dir' + rawext = 'arawtx' + sigext = 'asigtx' + subext = 'asubtx' + class xmr_wallet_outputs_file(xmr_signable, base): desc = 'Monero wallet outputs file' dir_name = 'xmr_outputs_dir' @@ -553,8 +566,10 @@ class Autosign: self.signables += Signable.non_xmr_signables if self.have_xmr: - self.dirs |= self.xmr_dirs - self.signables += Signable.xmr_signables + self.dirs |= self.xmr_dirs | ( + {'txauto_dir': 'txauto'} if cfg.xmrwallet_compat and self.xmr_only else {}) + self.signables += Signable.xmr_signables + ( + ('automount_transaction',) if cfg.xmrwallet_compat and self.xmr_only else ()) for name, path in self.dirs.items(): setattr(self, name, self.mountpoint / path) diff --git a/mmgen/data/mmgen.cfg b/mmgen/data/mmgen.cfg index d79faf25..51fe71b7 100644 --- a/mmgen/data/mmgen.cfg +++ b/mmgen/data/mmgen.cfg @@ -153,7 +153,8 @@ # monero_wallet_rpc_password passw0rd # Configure mmgen-xmrwallet for compatibility with mmgen-tx{create,sign,send} -# family of commands (equivalent to mmgen-xmrwallet --compat option) +# family of commands (equivalent to mmgen-xmrwallet --compat option). This +# option also enables signing of XMR compat transactions by `mmgen-autosign`. # xmrwallet_compat true ####################################################################### diff --git a/mmgen/data/release_date b/mmgen/data/release_date index 7a8c0a2a..c6750571 100644 --- a/mmgen/data/release_date +++ b/mmgen/data/release_date @@ -1 +1 @@ -November 2025 +December 2025 diff --git a/mmgen/data/version b/mmgen/data/version index 6471f962..6da2940f 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -16.1.dev19 +16.1.dev20 diff --git a/mmgen/help/help_notes.py b/mmgen/help/help_notes.py index cd4b5e38..c97d1b29 100755 --- a/mmgen/help/help_notes.py +++ b/mmgen/help/help_notes.py @@ -22,6 +22,8 @@ class help_notes: match self.proto.base_proto: case 'Bitcoin': return '[ADDR,AMT ... | DATA_SPEC] ADDR [addr file ...]' + case 'Monero': + return 'ADDR,AMT' case _: return 'ADDR,AMT [addr file ...]' diff --git a/mmgen/help/txcreate_examples.py b/mmgen/help/txcreate_examples.py index ecd6b320..e1c1d15e 100755 --- a/mmgen/help/txcreate_examples.py +++ b/mmgen/help/txcreate_examples.py @@ -56,6 +56,15 @@ EXAMPLES: $ {gc.prog_name} {mmtype} """ + case 'Monero': + return f""" +EXAMPLES: + + Send 0.123 {proto.coin} to an external {proto.name} address: + + $ {gc.prog_name} {sample_addr},0.123 +""" + case _: return f""" EXAMPLES: diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index b55064e5..0c55e962 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -22,7 +22,8 @@ mmgen-txcreate: Create a cryptocoin transaction with MMGen- and/or non-MMGen """ from .cfg import gc, Config -from .util import Msg, fmt_list, async_run +from .util import Msg, fmt_list, fmt_dict, async_run +from .xmrwallet import tx_priorities target = gc.prog_name.split('-')[1].removesuffix('create') @@ -72,6 +73,10 @@ opts_data = { b- -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses -- -m, --minconf= n Minimum number of confirmations required to spend + outputs (default: 1) + m- -p, --priority=N Specify an integer priority ‘N’ for inclusion of trans- + + action in blockchain (higher number means higher fee). + + Valid parameters: {tp}. + + If option is omitted, the default priority will be used -- -q, --quiet Suppress warnings; overwrite files without prompting -s -r, --stream-interval=N Set block interval for streaming swap (default: {si}) bt -R, --no-rbf Make transaction non-replaceable (non-replace-by-fee @@ -101,6 +106,7 @@ opts_data = { a_info = help_notes('account_info_desc'), fu = help_notes('rel_fee_desc'), fl = help_notes('fee_spec_letters', use_quotes=True), + tp = fmt_dict(tx_priorities, fmt='equal_compact'), si = help_notes('stream_interval'), fe_all = fmt_list(cfg._autoset_opts['fee_estimate_mode'].choices, fmt='no_spc'), fe_dfl = cfg._autoset_opts['fee_estimate_mode'].choices[0], @@ -132,7 +138,7 @@ async def main(): if cfg.autosign: from .tx.util import mount_removable_device from .autosign import Signable - asi = mount_removable_device(cfg) + asi = mount_removable_device(cfg, add_cfg={'xmrwallet_compat': True}) Signable.automount_transaction(asi).check_create_ok() if target == 'swaptx': @@ -149,10 +155,11 @@ async def main(): locktime = int(cfg.locktime or 0), do_info = cfg.info) - tx2.file.write( - outdir = asi.txauto_dir if cfg.autosign else None, - ask_write = not cfg.yes, - ask_overwrite = not cfg.yes, - ask_write_default_yes = False) + if not tx1.is_compat: + tx2.file.write( + outdir = asi.txauto_dir if cfg.autosign else None, + ask_write = not cfg.yes, + ask_overwrite = not cfg.yes, + ask_write_default_yes = False) async_run(cfg, main) diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 12aae915..5a07b714 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -131,6 +131,9 @@ async def main(): automount = cfg.autosign, quiet_open = True) + if tx.is_compat: + return await tx.compat_send() + cfg = Config({'_clone': cfg, 'proto': tx.proto, 'coin': tx.proto.coin}) if cfg.tx_proxy: diff --git a/mmgen/opts.py b/mmgen/opts.py index 9ca0e238..d46322dd 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -308,9 +308,10 @@ class UserOpts(Opts): br --rpc-user=USER Authenticate to coin daemon using username USER br --rpc-password=PASS Authenticate to coin daemon using password PASS Rr --rpc-backend=backend Use backend 'backend' for JSON-RPC communications - mr --monero-wallet-rpc-user=USER Monero wallet RPC username - mr --monero-wallet-rpc-password=USER Monero wallet RPC password - mr --monero-daemon=HOST:PORT Connect to the monerod at HOST:PORT + -r --monero-wallet-rpc-user=USER Monero wallet RPC username + -r --monero-wallet-rpc-password=USER Monero wallet RPC password + -r --monero-daemon=HOST:PORT Connect to the monerod at HOST:PORT + -r --xmrwallet-compat Enable XMR compatibility mode Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp -p --regtest=0|1 Disable or enable regtest mode -- --testnet=0|1 Disable or enable testnet diff --git a/mmgen/proto/xmr/params.py b/mmgen/proto/xmr/params.py index 8c9d1002..ddaaad66 100755 --- a/mmgen/proto/xmr/params.py +++ b/mmgen/proto/xmr/params.py @@ -37,9 +37,10 @@ class mainnet(CoinProtocol.RPC, CoinProtocol.DummyWIF, CoinProtocol.Base): pubkey_type = 'monero' # required by DummyWIF avg_bdi = 120 privkey_len = 32 - mmcaps = ('rpc',) + mmcaps = ('rpc', 'tw') coin_amt = 'XMRAmt' sign_mode = 'standalone' + has_usr_fee = False coin_cfg_opts = ( 'ignore_daemon_version', diff --git a/mmgen/proto/xmr/tw/view.py b/mmgen/proto/xmr/tw/view.py index 2a8cb7b6..991c4d23 100755 --- a/mmgen/proto/xmr/tw/view.py +++ b/mmgen/proto/xmr/tw/view.py @@ -25,6 +25,7 @@ from ....tw.unspent import TwUnspentOutputs class MoneroTwView: + is_account_based = True item_desc = 'account' nice_addr_w = {'addr': 20} total = None diff --git a/mmgen/proto/xmr/tx/base.py b/mmgen/proto/xmr/tx/base.py new file mode 100755 index 00000000..3b39ef1b --- /dev/null +++ b/mmgen/proto/xmr/tx/base.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# 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.base: Monero base transaction class +""" + +class Base: + is_compat = True + has_comment = False diff --git a/mmgen/proto/xmr/tx/completed.py b/mmgen/proto/xmr/tx/completed.py new file mode 100755 index 00000000..c6343f40 --- /dev/null +++ b/mmgen/proto/xmr/tx/completed.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# 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.completed: Monero completed transaction class +""" + +from ....cfg import Config + +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 diff --git a/mmgen/proto/xmr/tx/new.py b/mmgen/proto/xmr/tx/new.py new file mode 100755 index 00000000..0dfed000 --- /dev/null +++ b/mmgen/proto/xmr/tx/new.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# 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.new: Monero new transaction class +""" + +from ....tx.new import New as TxNew + +from .base import Base + +class New(Base, TxNew): + + async def get_input_addrs_from_inputs_opt(self): + return [] # TODO + + async def set_gas(self): + pass + + async def get_fee(self, fee, outputs_sum, start_fee_desc): + return True + + def copy_inputs_from_tw(self, tw_unspent_data): + self.inputs = tw_unspent_data + + def get_unspent_nums_from_user(self, accts_data): + from ....util import msg, is_int + from ....ui import line_input + prompt = 'Enter an account number to spend from: ' + while True: + if reply := line_input(self.cfg, prompt).strip(): + if is_int(reply) and 1 <= int(reply) <= len(accts_data): + return [int(reply)] + msg(f'Account number must be an integer between 1 and {len(accts_data)} inclusive') + + async def compat_create(self): + from ....xmrwallet import op as xmrwallet_op + i = self.inputs[0] + o = self.outputs[0] + op = xmrwallet_op( + 'transfer', + self.cfg, + None, + None, + spec = f'{i.idx}:{i.acct_idx}:{o.addr},{o.amt}', + compat_call = True) + await op.restart_wallet_daemon() + return await op.main() diff --git a/mmgen/proto/xmr/tx/online.py b/mmgen/proto/xmr/tx/online.py new file mode 100755 index 00000000..2f1b1d44 --- /dev/null +++ b/mmgen/proto/xmr/tx/online.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# 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.online: Monero online signed transaction class +""" + +from .completed import Completed + +class OnlineSigned(Completed): + + async def compat_send(self): + from ....xmrwallet import op as xmrwallet_op + op = xmrwallet_op('submit', self.cfg, self.filename, None, compat_call=True) + await op.restart_wallet_daemon() + return await op.main() + +class Sent(OnlineSigned): + pass + +class AutomountOnlineSigned(OnlineSigned): + pass + +class AutomountSent(AutomountOnlineSigned): + pass diff --git a/mmgen/proto/xmr/tx/unsigned.py b/mmgen/proto/xmr/tx/unsigned.py new file mode 100755 index 00000000..288820dd --- /dev/null +++ b/mmgen/proto/xmr/tx/unsigned.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2025 The MMGen Project +# 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.unsigned: Monero unsigned transaction class +""" + +from .completed import Completed + +class Unsigned(Completed): + pass + +class AutomountUnsigned(Unsigned): + pass diff --git a/mmgen/tw/view.py b/mmgen/tw/view.py index b0af8b9a..8378f0b5 100755 --- a/mmgen/tw/view.py +++ b/mmgen/tw/view.py @@ -80,6 +80,7 @@ class TwView(MMGenObject, metaclass=AsyncInit): def do(method, data, cw, fs, color, fmt_method): return [l.rstrip() for l in method(data, cw, fs, color, fmt_method)] + is_account_based = False has_age = False has_used = False has_wallet = True diff --git a/mmgen/tx/base.py b/mmgen/tx/base.py index 43288adc..30653e4a 100755 --- a/mmgen/tx/base.py +++ b/mmgen/tx/base.py @@ -81,6 +81,8 @@ class Base(MMGenObject): signed = False is_bump = False is_swap = False + is_compat = False + has_comment = True swap_attrs = { 'swap_proto': None, 'swap_quote_expiry': None, diff --git a/mmgen/tx/file.py b/mmgen/tx/file.py index 48853c1a..b5c33526 100755 --- a/mmgen/tx/file.py +++ b/mmgen/tx/file.py @@ -25,6 +25,10 @@ import os, json from ..util import ymsg, make_chksum_6, die from ..obj import MMGenObject, HexStr, MMGenTxID, CoinTxID, MMGenTxComment +def get_monero_proto(tx, data): + from ..protocol import init_proto + return init_proto(tx.cfg, 'XMR', network=data['MoneroMMGenTX']['data']['network']) + class txdata_json_encoder(json.JSONEncoder): def default(self, o): if type(o).__name__.endswith('Amt'): @@ -90,6 +94,9 @@ class MMGenTxFile(MMGenObject): tx = self.tx tx.file_format = 'json' outer_data = json.loads(data) + if 'MoneroMMGenTX' in outer_data: + tx.proto = get_monero_proto(tx, outer_data) + return None data = outer_data[self.data_label] if outer_data['chksum'] != make_chksum_6(json_dumps(data)): chk = make_chksum_6(json_dumps(data)) diff --git a/mmgen/tx/new.py b/mmgen/tx/new.py index 57eedad3..d6613d89 100755 --- a/mmgen/tx/new.py +++ b/mmgen/tx/new.py @@ -245,6 +245,9 @@ class New(Base): is_chg = not a.amt, is_vault = a.is_vault) + if self.is_compat: + return + if self.chg_idx is None: die(2, fmt(self.msg_no_change_output.format(self.dcoin)).strip() @@ -386,7 +389,7 @@ class New(Base): async def get_inputs(self, outputs_sum): - data = self.twuo.data + data = self.twuo.accts_data if self.twuo.is_account_based else self.twuo.data sel_nums = ( self.get_unspent_nums_from_inputs_opt if self.cfg.inputs else @@ -399,10 +402,10 @@ class New(Base): ' '.join(str(n) for n in sel_nums))) sel_unspent = MMGenList(data[i-1] for i in sel_nums) - if not await self.precheck_sufficient_funds( + if not (self.is_compat or await self.precheck_sufficient_funds( sum(s.amt for s in sel_unspent), sel_unspent, - outputs_sum): + outputs_sum)): return False self.copy_inputs_from_tw(sel_unspent) # makes self.inputs @@ -456,13 +459,16 @@ class New(Base): cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args) if self.is_swap: cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args) - from ..rpc import rpc_init - self.rpc = await rpc_init(self.cfg, self.proto) - from ..addrdata import TwAddrData - await self.process_cmdline_args( - cmd_args, - self.get_addrdata_from_files(self.proto, addrfile_args), - await TwAddrData(self.cfg, self.proto, twctl=self.twctl)) + if self.is_compat: + await self.process_cmdline_args(cmd_args, None, None) + else: + from ..rpc import rpc_init + self.rpc = await rpc_init(self.cfg, self.proto) + from ..addrdata import TwAddrData + await self.process_cmdline_args( + cmd_args, + self.get_addrdata_from_files(self.proto, addrfile_args), + await TwAddrData(self.cfg, self.proto, twctl=self.twctl)) if not self.is_bump: self.twuo = await TwUnspentOutputs( @@ -510,13 +516,12 @@ class New(Base): desc)) is not None: break - self.check_non_mmgen_inputs(caller=caller) + if not self.is_compat: + self.check_non_mmgen_inputs(caller=caller) + self.update_change_output(funds_left) + self.check_chg_addr_is_wallet_addr() - self.update_change_output(funds_left) - - self.check_chg_addr_is_wallet_addr() - - if not self.cfg.yes: + if self.has_comment and not self.cfg.yes: self.add_comment() # edits an existing comment if self.is_swap: @@ -524,6 +529,9 @@ class New(Base): if time.time() > self.swap_quote_refresh_time + self.swap_quote_refresh_timeout: await self.update_vault_output(self.vault_output.amt) + if self.is_compat: + return await self.compat_create() + await self.create_serialized(locktime=locktime) # creates self.txid too self.add_timestamp() diff --git a/mmgen/tx/util.py b/mmgen/tx/util.py index 149e19ab..d3886ddb 100755 --- a/mmgen/tx/util.py +++ b/mmgen/tx/util.py @@ -12,7 +12,7 @@ tx.util: transaction utilities """ -def get_autosign_obj(cfg): +def get_autosign_obj(cfg, add_cfg={}): from ..cfg import Config from ..autosign import Autosign return Autosign( @@ -21,10 +21,10 @@ def get_autosign_obj(cfg): 'mountpoint': cfg.autosign_mountpoint, 'coins': cfg.coin, # used only in online environment (xmrwallet, txcreate, txsend, txbump): - 'online': not cfg.offline})) + 'online': not cfg.offline} | add_cfg)) -def mount_removable_device(cfg): - asi = get_autosign_obj(cfg) +def mount_removable_device(cfg, add_cfg={}): + asi = get_autosign_obj(cfg, add_cfg=add_cfg) if not asi.device_inserted: from ..util import die die(1, 'Removable device not present!') diff --git a/mmgen/xmrwallet/__init__.py b/mmgen/xmrwallet/__init__.py index 6221d197..b1fb2d1d 100755 --- a/mmgen/xmrwallet/__init__.py +++ b/mmgen/xmrwallet/__init__.py @@ -116,16 +116,17 @@ def op_cls(op_name): def op(op, cfg, infile, wallets, *, spec=None, compat_call=False): if compat_call or (cfg.compat if cfg.compat is not None else cfg.xmrwallet_compat): - if cfg.wallet_dir: - die(1, '--wallet-dir can not be specified in xmrwallet compatibility mode') + if cfg.wallet_dir and not cfg.offline: + die(1, '--wallet-dir cannot be specified in xmrwallet compatibility mode') from ..tw.ctl import TwCtl from ..cfg import Config twctl_cls = cfg._proto.base_proto_subclass(TwCtl, 'tw.ctl') cfg = Config({ '_clone': cfg, 'compat': True, - 'no_start_wallet_daemon': cfg.no_start_wallet_daemon or compat_call, - 'daemon': cfg.daemon or cfg.monero_daemon, - 'watch_only': cfg.watch_only or cfg.autosign or bool(cfg.autosign_mountpoint), - 'wallet_dir': twctl_cls.get_tw_dir(cfg, cfg._proto)}) + 'xmrwallet_compat': True} | ({} if cfg.offline else { + 'no_start_wallet_daemon': cfg.no_start_wallet_daemon or compat_call, + 'daemon': cfg.daemon or cfg.monero_daemon, + 'watch_only': cfg.watch_only or cfg.autosign or bool(cfg.autosign_mountpoint), + 'wallet_dir': twctl_cls.get_tw_dir(cfg, cfg._proto)})) return op_cls(op)(cfg, uargs(infile, wallets, spec, compat_call)) diff --git a/mmgen/xmrwallet/file/tx.py b/mmgen/xmrwallet/file/tx.py index bdbbd896..e3680b86 100755 --- a/mmgen/xmrwallet/file/tx.py +++ b/mmgen/xmrwallet/file/tx.py @@ -164,7 +164,7 @@ class MoneroMMGenTX: d = self.ext) if self.cfg.autosign: - fn = get_autosign_obj(self.cfg).xmr_tx_dir / fn + fn = getattr(get_autosign_obj(self.cfg), self.tx_dir) / fn from ...fileutil import write_data_to_file write_data_to_file( @@ -183,6 +183,7 @@ class MoneroMMGenTX: is_submitting = False is_complete = False signed = False + tx_dir = 'xmr_tx_dir' def __init__(self, *args, **kwargs): @@ -245,6 +246,18 @@ class MoneroMMGenTX: is_submitting = True is_complete = True + class NewUnsignedCompat(NewUnsigned): + tx_dir = 'txauto_dir' + ext = 'arawtx' + + class NewColdSignedCompat(NewColdSigned): + tx_dir = 'txauto_dir' + ext = 'asigtx' + + class NewSubmittedCompat(NewSubmitted): + tx_dir = 'txauto_dir' + ext = 'asubtx' + class Completed(Base): desc = 'transaction' forbidden_fields = () @@ -333,3 +346,12 @@ class MoneroMMGenTX: class View(Completed): silent_load = True + + class UnsignedCompat(Unsigned): + ext = 'arawtx' + + class ColdSignedCompat(ColdSigned): + ext = 'asigtx' + + class SubmittedCompat(Submitted): + ext = 'asubtx' diff --git a/mmgen/xmrwallet/ops/__init__.py b/mmgen/xmrwallet/ops/__init__.py index 2563775e..54aa4bec 100755 --- a/mmgen/xmrwallet/ops/__init__.py +++ b/mmgen/xmrwallet/ops/__init__.py @@ -43,6 +43,7 @@ class OpBase: self.cfg = cfg self.uargs = uarg_tuple self.compat_call = self.uargs.compat_call + self.tx_dir = 'txauto_dir' if self.compat_call else 'xmr_tx_dir' classes = tuple(gen_classes()) self.opts = tuple(set(opt for cls in classes for opt in xmrwallet.opts)) @@ -102,6 +103,10 @@ class OpBase: self.cfg.tx_relay_daemon, re.ASCII) + def get_tx_cls(self, clsname): + from ..file.tx import MoneroMMGenTX + return getattr(MoneroMMGenTX, clsname + ('Compat' if self.compat_call else '')) + def display_tx_relay_info(self, *, indent=''): m = self.parse_tx_relay_opt() msg(fmt(f""" diff --git a/mmgen/xmrwallet/ops/sign.py b/mmgen/xmrwallet/ops/sign.py index 9a64b828..953848de 100755 --- a/mmgen/xmrwallet/ops/sign.py +++ b/mmgen/xmrwallet/ops/sign.py @@ -12,7 +12,6 @@ xmrwallet.ops.sign: Monero wallet ops for the MMGen Suite """ -from ..file.tx import MoneroMMGenTX from ..rpc import MoneroWalletRPC from .wallet import OpWallet @@ -24,7 +23,7 @@ class OpSign(OpWallet): async def main(self, fn, *, restart_daemon=True): if restart_daemon: await self.restart_wallet_daemon() - tx = MoneroMMGenTX.Unsigned(self.cfg, fn) + tx = self.get_tx_cls('Unsigned')(self.cfg, fn) h = MoneroWalletRPC(self, self.addr_data[0]) self.head_msg(tx.src_wallet_idx, h.fn) if restart_daemon: @@ -34,7 +33,7 @@ class OpSign(OpWallet): unsigned_txset = tx.data.unsigned_txset, export_raw = True, get_tx_keys = True) - new_tx = MoneroMMGenTX.NewColdSigned( + new_tx = self.get_tx_cls('NewColdSigned')( cfg = self.cfg, txid = res['tx_hash_list'][0], unsigned_txset = None, diff --git a/mmgen/xmrwallet/ops/submit.py b/mmgen/xmrwallet/ops/submit.py index 25c732ec..e4616955 100755 --- a/mmgen/xmrwallet/ops/submit.py +++ b/mmgen/xmrwallet/ops/submit.py @@ -20,7 +20,6 @@ from ...ui import keypress_confirm from ...proto.xmr.daemon import MoneroWalletDaemon from ...proto.xmr.rpc import MoneroWalletRPCClient -from ..file.tx import MoneroMMGenTX from ..rpc import MoneroWalletRPC from . import OpBase @@ -45,7 +44,7 @@ class OpSubmit(OpWallet): else: from ...autosign import Signable fn = Signable.xmr_transaction(self.asi).get_unsubmitted() - return MoneroMMGenTX.ColdSigned(cfg=self.cfg, fn=fn) + return self.get_tx_cls('ColdSigned')(cfg=self.cfg, fn=fn) def get_relay_rpc(self): @@ -100,7 +99,7 @@ class OpSubmit(OpWallet): from ...util2 import format_elapsed_hr msg(f'success\nRelay time: {format_elapsed_hr(t_start, rel_now=False, show_secs=True)}') - new_tx = MoneroMMGenTX.NewSubmitted(cfg=self.cfg, _in_tx=tx) + new_tx = self.get_tx_cls('NewSubmitted')(cfg=self.cfg, _in_tx=tx) gmsg('\nOK') new_tx.write( @@ -118,7 +117,7 @@ class OpResubmit(OpSubmit): def get_tx(self): from ...autosign import Signable fns = Signable.xmr_transaction(self.asi).get_submitted() - cls = MoneroMMGenTX.Submitted + cls = self.get_tx_cls('Submitted') return sorted((cls(self.cfg, Path(fn)) for fn in fns), key = lambda x: getattr(x.data, 'submit_time', None) or x.data.create_time)[-1] diff --git a/mmgen/xmrwallet/ops/txview.py b/mmgen/xmrwallet/ops/txview.py index 322f2513..6b8ff8af 100755 --- a/mmgen/xmrwallet/ops/txview.py +++ b/mmgen/xmrwallet/ops/txview.py @@ -33,7 +33,7 @@ class OpTxview(OpBase): self.mount_removable_device() if self.cfg.autosign: - files = [f for f in self.asi.xmr_tx_dir.iterdir() + files = [f for f in getattr(self.asi, self.tx_dir).iterdir() if f.name.endswith('.' + mtx.Submitted.ext)] else: files = self.uargs.infile diff --git a/mmgen/xmrwallet/rpc.py b/mmgen/xmrwallet/rpc.py index 7641fa49..12b2d7f2 100755 --- a/mmgen/xmrwallet/rpc.py +++ b/mmgen/xmrwallet/rpc.py @@ -32,6 +32,7 @@ class MoneroWalletRPC: self.d = d self.fn = parent.get_wallet_fn(d) self.new_tx_cls = ( + mtx.NewUnsignedCompat if self.parent.compat_call else mtx.NewUnsigned if self.cfg.watch_only else mtx.NewSigned) diff --git a/setup.cfg b/setup.cfg index 83564b39..e407aa05 100644 --- a/setup.cfg +++ b/setup.cfg @@ -105,6 +105,7 @@ packages = mmgen.proto.xchain mmgen.proto.xmr mmgen.proto.xmr.tw + mmgen.proto.xmr.tx mmgen.proto.zec mmgen.rpc mmgen.rpc.backends diff --git a/test/cmdtest_d/xmr_autosign.py b/test/cmdtest_d/xmr_autosign.py index bcf149f1..ffaffbd9 100755 --- a/test/cmdtest_d/xmr_autosign.py +++ b/test/cmdtest_d/xmr_autosign.py @@ -19,6 +19,7 @@ from mmgen.color import blue, cyan, brown from ..include.common import ( imsg, + oqmsg, silence, end_silence, strip_ansi_escapes, @@ -507,6 +508,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): Monero autosigning operations (compat mode) """ menu_prompt = 'efresh balances:\b' + extra_daemons = ['ltc'] cmd_group = ( ('autosign_setup', 'autosign setup with Alice’s seed'), @@ -535,6 +537,17 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): ('alice_twview2', 'viewing Alice’s tracking wallets (reload, sort options)'), ('alice_twview3', 'viewing Alice’s tracking wallets (check balances)'), ('alice_listaddresses2', 'listing Alice’s addresses (sort options)'), + ('wait_loop_start_compat', 'starting autosign wait loop in XMR compat mode [--coins=xmr]'), + ('alice_txcreate1', 'creating a transaction'), + ('alice_txabort1', 'aborting the transaction'), + ('alice_txcreate2', 'recreating the transaction'), + ('wait_signed1', 'autosigning the transaction'), + ('wait_loop_kill', 'stopping autosign wait loop'), + ('alice_txabort2', 'aborting the raw and signed transactions'), + ('alice_txcreate3', 'recreating the transaction'), + ('wait_loop_start_ltc', 'starting autosign wait loop in XMR compat mode [--coins=ltc,xmr]'), + ('alice_txsend1', 'sending the transaction'), + ('wait_loop_kill', 'stopping autosign wait loop'), ('stop_daemons', 'stopping all wallet and coin daemons'), ) @@ -546,11 +559,10 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): self.alice_dump_file = os.path.join( self.alice_tw_dir, '{}-2-MoneroWatchOnlyWallet.dump'.format(self.users['alice'].sid)) - self.alice_opts = [ - '--alice', - '--coin=xmr', - '--monero-wallet-rpc-password=passwOrd', - f'--monero-daemon=localhost:{self.users["alice"].md.rpc_port}'] + self.alice_daemon_opts = [ + f'--monero-daemon=localhost:{self.users["alice"].md.rpc_port}', + '--monero-wallet-rpc-password=passwOrd'] + self.alice_opts = ['--alice', '--coin=xmr'] + self.alice_daemon_opts def create_watchonly_wallets(self): return self._create_wallets() @@ -660,3 +672,61 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): assert s in text self.remove_device_online() return t + + def wait_loop_start_compat(self): + return self.wait_loop_start(opts=['--xmrwallet-compat', '--coins=xmr']) + + def wait_loop_start_ltc(self): + return self.wait_loop_start(opts=['--xmrwallet-compat', '--coins=ltc,xmr']) + + def alice_txcreate1(self): + return self._alice_txops('txcreate', [f'{self.burn_addr},0.012345'], acct_num=1) + + alice_txcreate3 = alice_txcreate2 = alice_txcreate1 + + def alice_txabort1(self): + return self._alice_txops('txsend', opts=['--alice', '--abort']) + + alice_txabort2 = alice_txabort1 + + def alice_txsend1(self): + return self._alice_txops( + 'txsend', + opts = ['--alice', '--quiet'], + add_opts = self.alice_daemon_opts, + acct_num = 1, + wait_signed = True) + + def wait_signed1(self): + self.spawn(msg_only=True) + oqmsg('') + self._wait_signed('transaction') + return 'silent' + + def _alice_txops( + self, + op, + args = [], + *, + opts = [], + add_opts = [], + menu = '', + acct_num = None, + wait_signed = False, + signable_desc = 'transaction'): + if wait_signed: + self._wait_signed(signable_desc) + self.insert_device_online() + t = self.spawn(f'mmgen-{op}', (opts or self.alice_opts) + self.autosign_opts + add_opts + args) + if '--abort' in opts: + t.expect('(y/N): ', 'y') + elif op == 'txcreate': + for ch in menu + 'q': + t.expect(self.menu_prompt, ch) + t.expect('to spend from: ', f'{acct_num}\n') + t.expect('(y/N): ', 'y') # save? + elif op == 'txsend': + t.expect('(y/N): ', 'y') # view? + t.read() # required! + self.remove_device_online() + return t