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:
parent
04814eb21f
commit
f17416cad6
24 changed files with 293 additions and 176 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
16.3.0dev19
|
||||
16.3.0dev20
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
23
mmgen/proto/xmr/tx/signed.py
Normal file
23
mmgen/proto/xmr/tx/signed.py
Normal 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'
|
||||
35
mmgen/proto/xmr/tx/status.py
Normal file
35
mmgen/proto/xmr/tx/status.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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']:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue