From f17416cad648435dfb8a964aa5977803fca08e49 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Fri, 23 Oct 2026 17:01:32 +0000 Subject: [PATCH] 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. --- mmgen/autosign/signable.py | 12 +++- mmgen/data/version | 2 +- mmgen/main_txcreate.py | 2 +- mmgen/main_txsend.py | 13 ++-- mmgen/proto/xmr/tx/base.py | 20 ++++++ mmgen/proto/xmr/tx/completed.py | 30 +++++---- mmgen/proto/xmr/tx/online.py | 48 ++++---------- mmgen/proto/xmr/tx/signed.py | 23 +++++++ mmgen/proto/xmr/tx/status.py | 35 ++++++++++ mmgen/tx/completed.py | 3 +- mmgen/tx/file.py | 61 ++++++++++------- mmgen/tx/new.py | 65 +++++++++---------- mmgen/tx/online.py | 4 +- mmgen/xmrwallet/file/__init__.py | 2 +- mmgen/xmrwallet/file/tx.py | 21 ++++-- mmgen/xmrwallet/ops/sign.py | 2 + mmgen/xmrwallet/ops/submit.py | 15 +++-- mmgen/xmrwallet/ops/sweep.py | 8 ++- test/cmdtest_d/autosign.py | 10 ++- test/cmdtest_d/misc.py | 10 +++ test/cmdtest_d/xmr_autosign.py | 17 +++-- test/cmdtest_d/xmrswap.py | 28 ++++---- .../3EBD06-2D6E3B-XMR[0.74].testnet.sigtx | 37 ++++++----- .../CD6521-XMR[0.234499978787].regtest.arawtx | 1 + 24 files changed, 293 insertions(+), 176 deletions(-) create mode 100644 mmgen/proto/xmr/tx/signed.py create mode 100644 mmgen/proto/xmr/tx/status.py create mode 100644 test/ref/monero/compat/CD6521-XMR[0.234499978787].regtest.arawtx diff --git a/mmgen/autosign/signable.py b/mmgen/autosign/signable.py index 16d0ba4b..c76a15c1 100644 --- a/mmgen/autosign/signable.py +++ b/mmgen/autosign/signable.py @@ -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' diff --git a/mmgen/data/version b/mmgen/data/version index 7142f78e..77730025 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -16.3.0dev19 +16.3.0dev20 diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index 9d945eb7..22919e8d 100644 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -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) diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 2295067a..13189d9f 100644 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -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) diff --git a/mmgen/proto/xmr/tx/base.py b/mmgen/proto/xmr/tx/base.py index 0ade71f8..9c68f869 100644 --- a/mmgen/proto/xmr/tx/base.py +++ b/mmgen/proto/xmr/tx/base.py @@ -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 diff --git a/mmgen/proto/xmr/tx/completed.py b/mmgen/proto/xmr/tx/completed.py index b971d933..a5c0b1e1 100644 --- a/mmgen/proto/xmr/tx/completed.py +++ b/mmgen/proto/xmr/tx/completed.py @@ -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) diff --git a/mmgen/proto/xmr/tx/online.py b/mmgen/proto/xmr/tx/online.py index 6aef9e15..d3c92fcb 100644 --- a/mmgen/proto/xmr/tx/online.py +++ b/mmgen/proto/xmr/tx/online.py @@ -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' diff --git a/mmgen/proto/xmr/tx/signed.py b/mmgen/proto/xmr/tx/signed.py new file mode 100644 index 00000000..fe161fe0 --- /dev/null +++ b/mmgen/proto/xmr/tx/signed.py @@ -0,0 +1,23 @@ +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2026 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.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' diff --git a/mmgen/proto/xmr/tx/status.py b/mmgen/proto/xmr/tx/status.py new file mode 100644 index 00000000..5baa2606 --- /dev/null +++ b/mmgen/proto/xmr/tx/status.py @@ -0,0 +1,35 @@ +# MMGen Wallet, a terminal-based cryptocurrency wallet +# Copyright (C)2013-2026 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.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 diff --git a/mmgen/tx/completed.py b/mmgen/tx/completed.py index 9c6cce1b..95995c36 100644 --- a/mmgen/tx/completed.py +++ b/mmgen/tx/completed.py @@ -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 diff --git a/mmgen/tx/file.py b/mmgen/tx/file.py index d400474c..fc34f0f9 100644 --- a/mmgen/tx/file.py +++ b/mmgen/tx/file.py @@ -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], diff --git a/mmgen/tx/new.py b/mmgen/tx/new.py index dd1170ae..922996de 100644 --- a/mmgen/tx/new.py +++ b/mmgen/tx/new.py @@ -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') diff --git a/mmgen/tx/online.py b/mmgen/tx/online.py index 2e20fb64..7cd7dbed 100644 --- a/mmgen/tx/online.py +++ b/mmgen/tx/online.py @@ -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']: diff --git a/mmgen/xmrwallet/file/__init__.py b/mmgen/xmrwallet/file/__init__.py index ab044e2d..b49516cc 100644 --- a/mmgen/xmrwallet/file/__init__.py +++ b/mmgen/xmrwallet/file/__init__.py @@ -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] diff --git a/mmgen/xmrwallet/file/tx.py b/mmgen/xmrwallet/file/tx.py index 94cc4dc8..a7c5fff4 100644 --- a/mmgen/xmrwallet/file/tx.py +++ b/mmgen/xmrwallet/file/tx.py @@ -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' diff --git a/mmgen/xmrwallet/ops/sign.py b/mmgen/xmrwallet/ops/sign.py index 09a9e31f..7b7e9145 100644 --- a/mmgen/xmrwallet/ops/sign.py +++ b/mmgen/xmrwallet/ops/sign.py @@ -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 diff --git a/mmgen/xmrwallet/ops/submit.py b/mmgen/xmrwallet/ops/submit.py index ffbae2f6..586779d3 100644 --- a/mmgen/xmrwallet/ops/submit.py +++ b/mmgen/xmrwallet/ops/submit.py @@ -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) diff --git a/mmgen/xmrwallet/ops/sweep.py b/mmgen/xmrwallet/ops/sweep.py index 03628453..beb6a6bf 100644 --- a/mmgen/xmrwallet/ops/sweep.py +++ b/mmgen/xmrwallet/ops/sweep.py @@ -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: diff --git a/test/cmdtest_d/autosign.py b/test/cmdtest_d/autosign.py index d2c1410c..a6ba4541 100644 --- a/test/cmdtest_d/autosign.py +++ b/test/cmdtest_d/autosign.py @@ -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: diff --git a/test/cmdtest_d/misc.py b/test/cmdtest_d/misc.py index be4cd13c..be666b5f 100644 --- a/test/cmdtest_d/misc.py +++ b/test/cmdtest_d/misc.py @@ -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' diff --git a/test/cmdtest_d/xmr_autosign.py b/test/cmdtest_d/xmr_autosign.py index 0ac47444..ca82819b 100644 --- a/test/cmdtest_d/xmr_autosign.py +++ b/test/cmdtest_d/xmr_autosign.py @@ -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) diff --git a/test/cmdtest_d/xmrswap.py b/test/cmdtest_d/xmrswap.py index 89b3a456..efaf2b45 100644 --- a/test/cmdtest_d/xmrswap.py +++ b/test/cmdtest_d/xmrswap.py @@ -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' diff --git a/test/ref/monero/3EBD06-2D6E3B-XMR[0.74].testnet.sigtx b/test/ref/monero/3EBD06-2D6E3B-XMR[0.74].testnet.sigtx index 4f4971cf..e74d1ed0 100644 --- a/test/ref/monero/3EBD06-2D6E3B-XMR[0.74].testnet.sigtx +++ b/test/ref/monero/3EBD06-2D6E3B-XMR[0.74].testnet.sigtx @@ -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 } + } } diff --git a/test/ref/monero/compat/CD6521-XMR[0.234499978787].regtest.arawtx b/test/ref/monero/compat/CD6521-XMR[0.234499978787].regtest.arawtx new file mode 100644 index 00000000..46f8c4c8 --- /dev/null +++ b/test/ref/monero/compat/CD6521-XMR[0.234499978787].regtest.arawtx @@ -0,0 +1 @@ +{"MMGenTransaction":{"coin_id":"XMR","version":4,"MoneroMMGenTX":{"base_chksum":"cd6521","full_chksum":"08feeb","data":{"op":"transfer","create_time":1792645385,"sign_time":null,"submit_time":null,"network":"regtest","seed_id":"98831F3A","source":"1:0:None","dest":null,"dest_address":"477PUpbuezBNteVhPkBMJdMN4HM7vJQCwgJ7AmmXMrMqhNkPdhtmJnKEJbY3PRCY9CfhyKH3XVAZDcqfCpUT3X5g5QYV2y8","txid":"6f731b37e47982bd1e17cb4b5208ea647b12847e05ae94e7a491ef9d653d1e4c","amount":"0.234499978787","priority":3,"fee":"0.028633","blob":"020001020010120e1f70330e015d030229160205283d0100000000000000000000000000000000000000000000000000000000000000020003392933a3006114962f87c35ae8099705dc2b0eda19faea10fe77691830c2566e3900038cfbf2cc4f49174effc333fa85712fdce7fa8482e72cf8d7ff71aea45f75a5e4432c018c94083f2ac67fd4b7156a04807aa73f1a691f150653bd17737135d9a390dcd80209014c5df2449cd7b02106c0d0a3d56ac99f53024beb4bcd8c56fc1fb6d1523f5508e858a415f62884f801d7c133847fd1888479d5632529e988fad2e52bd31da202bc2e928238840cb42251c9209b4af809f467406f3c21cc658eed3c6c6b0a010a2383aa83590430665bdaa48315e96814207bf6bc6aa8bae14f3ac65890688cad4af205a36489ec5ef1d1b6f576fc2185e97008ec489797e1c138040d22be958b3c6654597cd0d4147023e3d33d9df7a86cd385cfed74fac39aef50e9db2d868398321f1219909ee6b2956e62e77964772e60defda50cdca85b44a0d6487808e5f2f15df718eb66ff34fb29f65456dd4ce2eeb89a6252eb98d0564b2ed6440637f6dfacbad6d212119d7826fd6f5d5163bb9b65d5808660d1138a5782e3f1050771fb31d88e0eb6c1919044da6b3d9f6e283c3f436e6933c80c3173fa858ff13ecb6c55a88337f66e25303c951745e80e1c5f0e3fdb08afb724584dc075418817aa314157f71129c4b29ae6002b193e32d9c67887600bb60008309a81f52ef964307c4be3f7108106e5052ce616976bcaea7ab9b8364bf0cae542c0ea2b919d6d07abc727be6c8cd31ffad3915ee298ca8fd811ae2e6e6e681596ae167f0a6817bf32df0d2ca3faf7c25c7f49655adecbdfb1af5951f7d75566372ac586f0ad70900a0f6b9b49fb6e3e530dbb16f92ed53e611197f456272a38811e81e02fddad079defe08f5ba4590d4ecbd129efa93d66e7387ddd78be724908d02a7895cdc08296080e1ebee20daa6750786699fe0c86c710f41136391c6d5c185f3745361dc06632b7ecf67e0c92cd61a19ad0466deea5828be8e1534470e03a39fa611e8e4c9b33a4c7c846c49e38b2d5f4cb2723368abd1642f375dac515863285fa77c88a57a9d7b7c3ff4375d31bdb6ed050a726d86dabcf8f5db65c6e4386b873f73a43446ce5c166d8c454414aba03039c9ab7402e37f26030598d08a088886e46af0ec5431cc142704df480962fb6b547f859d9a1db39664a7795187194987d94a02bc32d463f2d8fa50c55153e96fe6cd5cfc26c7c14bdb209e964366f0457f20a06a122be7a24526583c513da34cf612632347c57598c21a30a4fd388d57c09e404d8aefd0480385bc6683cdac16c777ca96f9ec6522db2b9b677dd1e2e1e50fc0f0bba30813738f06cd6452ad218ce0c5a751b4bda73ced7e4c924f9acd15e4b00dd535225b976e19bebf16cde34f53a8b8afe0388f09be9d5fac2fedc0886ab07dffdf68e8b3bdb809669287fb7902e686434e97878865cf28030d2fd19baae0890a1394afdbd104c46d4273f01cbbd9aff07ea03270ca4c8b8f5cb20b3f8c90a39a55e48b54bd45e0f14064261c190df34f0ad05d90a0632dee71f9dcb613501acdaacea80853525393f4980e5b6678d848677471d00238471ebdf7a7a207b0c66a5d3bf3daef74330521e5ed3699c59c1ba8c3293b05ab0ff7809892c53820a373c631f407f796e2f93950f75da5568a1354b29ca51f5382726940752095d00dfe511bdcf1c4b4cc994b3d012957ace8d7a2fc77400acbc36dd573668847f0ae8621bb6469e73a24702316b3c9f6557d8b6a72d008b80b7372890d5106d9a064c7128124fa18781c5e0cb7c39bc023945a49d629b2a13b39eda3c062f62d9091627a2ff0607082cfe5a7a75b5a674123031259ecfbda61227c97d860ac383073976da68edf817414d7e02a6336f2ace340e8359bba333a64e36ce5ac156fb0c87bd5a1ae0d1cae9ca8705f689e8cf2c6e947567e0959c3108f4cf9b7ea6dd035ceae143040ffd7928e09e076d2a841023c839fdb0d8bd087e3b736e2a0a5335035b17c835a97abeedcb443646f83124cb73d9644d17fe400805824b43ab5147","metadata":null,"unsigned_txset":"4d6f6e65726f20756e7369676e656420747820736574056304ce91b3350e0e2b8ce5b204fe5e4b2874754ae8d297d929a94b8497432c63473aed90b39a9602671bab00577b997c56b3a03b190f69b79db723420d4857e176697d25cb6ab44ee95f3c13bbedc21644b0b7eac865fe488e7a37398030fa7701527493ed8c19575efe5d9087ac2a50b61bc1abe8a43b708908b9481d2d089c1138c0093e1618360234bc1e6a9296ec980a779c6d0b4aa4295317dd5995351d3e8a24f650d566fec2bc541e54b577a8f0bda988bf3a1d1f1590bdd9793fdf7b21fbab14bfa2c19c3a0278ede9a44046e89dd169690b88a94a9f551375ca88c9c72166564940339a3e8c13656769452647dde71536279876fa740b80d3b5359d521f9bd0a4b239b0fb63368d046dd9a5ef00db42a1a12f9a81563e610e9755b7044e071d300f126338ea56c74b0b3a49183a772a7f4b8fc0c11984e651e20139014ed757c7279880db4a42365df8bfb6b346aeeb7572e6decd99094e6fc9a77c2c1c9549be6a6e36296f7e203a95ad92ffbb9762434d51ab35bfbc01ed05ef5cb7bb3ca785d5c1123b20fc2fdcc52ad865e484eef8abad044773cc08cb40e23d84528f1de8f4439d55355501a6327733df381cfec3907801d2e013050c7994d7ed4249f63513dab01888b856a81b996dd88d18d4692fee180c8ace87c24caed5022f734b34b306b50f9c9b34e6445b66e2275cffc39b14c125e7d7438a94790618ed0024c79e44d6a483fd70f4da18e21c280463c263e13496fb18910399e90d40c308fffd68d2a9868d8b9e7ee7e1c8a34c04405905a02e37982e7299dceab4a9efa29e9a4db07e5836b5ef7655d880ca17693ab41db97a15fa13d0d70c9a3fc5e2758c468b23f68176330baa7089b7497049c8a36ffa4e9ddddd5fbda34beddbc93340d45921a2fb48c5cc9b258024190fa1fa2919fe6470d5e89e5af95831d07653bccf1876a3429671ea05c09f2db27d11b5763ac64b0dd26c5fb10e0cc361df3c43dcf9a8d6ff5ad93b5c0cce7b65cdd4bc2a8bc3d7bfb17051e91bc55f473dcc41e2c76cf6e26d37ece963d84563ed23d2586fa342eb2fca357cf776fa775595486e870df3afb1d91893ef585ddd139619da20a51b1391c361b7177cf35fbf516381e421885bf2340d346264bf988cdea234d32c516b1a71ebd63db659f5dbf3599c6c192e4b8a2956758f71f178cf283f6dacc76740083fcdcac1dfbe29d3b090c85e94608d4a96fd58a131a3958dd5c8376e12612c90e499da6a69ac54250c42dc136b4b9df202050e8d48d4fe55339711795e3282101e3980946a983da2fc3cab14ed71ec2bfd91f2b68f5b44366515847f36cc833056bb618f8deadb752d8f31d14b9be4a4cdf7996ccf86c1d0c2da535574870668c5fcc884e8d68a9e1f09210bd6d18f6137359e254febe108f5adea423749b2fb73ff631c8d6bd840e3cd9c5dc14195b6c114a49b08e4b5161e7475be0be7f146fc622cc326ce9ecf82b572be158484086f97cf08e001c65df8b96e33e0bfc98202ee3d8e6edcf9bcc3f8e7abd9a5ac8871a4da6a351a42eb2da40c90d9d8e5295d656aeed84f9c3d0ada332085da123b630e0e3d98468e1c2dce22e87a788b01a211e15c823b1bb02da61f4df5c2f46ff9d072893758cdc9e4606ee0585fa1b38c96d9dee32a4240a83a4e3307285ef204dc0359e257640de65258fad628a4d5f52de01b11158bda6a024ee259dcfde72b1547e1d9637b3f5c405796468cc6dc1ac933fd6576475f1468ecda9d0a8438abb5f93ec1fc2cdef204d39c9f583287c9450644f90fee4208f27c9bb105823af1b3fbb1a9d4f4531d303e0f85d4ec69838465c358109233f2ca277f5bc17b188eedd7aea19b5770750b5ed4cbfba6515f84cecdabca60ae3868d53fc8aa23efaec4463f1d3d7d1031852762382e21260a127e44bb1fbdb141ecaf871fac5fc225aba3b0f35d81c21542597a2ea799f263358969899b4099298b5fa1b841b864588a6be61f418b3de7b5e1b0cccc62a89416cc9631b79ae98c79a12e778e437865b439263de9565fde3a2d53836149fdf1381f6e55db3707dfbbeba8198d167b0d9960836a980af363aff5036f31146ed4f3c4ed2431c9358be9681829e165325b0d23a656a854ae23469fb75722ba432f9eae921677f2d6abc9eb297ac5407425cc6ac8aaf46562a87ab0d9cb6193fc979afa50d2590dbe6ab5bfd05fab46b66bd59ccc5a9903c025fc0cfd510cd2ee0780fd13ab0d27bdffecea58c6c06042014a4ea506f607d89c069eb2e25c3eb0dccdb8c89778a2ca54a3940658068d8b53a2d6967408e46dd82ca30e91043f92a385de7e5c9a24bfd4e8f42a37ae171d105e49e4cad657721c362ca5b076878bb4b51b26ed3fac921fc14041dbb373bd2f2256e6d8b90391e8cfe02e7e1222545a073bfb8a46292cdaf468c46bb3e31ade4fb2fb88a73611b3b4f47aa4cb0df086b2c55c748bdc3bfe30131242de816551e4fc81571c0704f5b2d5e7698c1bf383948234cdf6418752021ac6f26bd1e89332212f8c1efeb2dbc1244fbda749bc88ef56f9079c135b763c8efd18c731d4f380662d0b47ea0ec4c2048c5bccec0b9bb4480b65532a9d6daa696a52b90b021def1eb3649abad939a568a326ce46b3eef0a025ffe2f55047ef810aa0f22e542a5eb33959cec9b29f8d35d7e4f8d40682e50721b5739e5025600ba34416b02b4f4cbf0224d17c9d7b87a992a10e28b9f89024b93aae4ff19136c09f9970440cc6270e5a96ee44dd6dc624064593b1f1f1989d3864fd48d4550f23ed798a8f0f","signed_txset":null,"complete":false}},"is_swap":true,"swap_proto":"thorchain","swap_quote_expiry":1792645982,"swap_recv_addr_mmid":"98831F3A:B:3","swap_recv_asset_spec":"LTC","is_memoless":true,"refnum":78787},"chksum":"6cdc28"} \ No newline at end of file