xmrwallet: get_info(), chksum cleanups

This commit is contained in:
The MMGen Project 2023-04-30 09:36:00 +00:00
commit 1a0b674585
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
4 changed files with 97 additions and 71 deletions

View file

@ -47,8 +47,7 @@ sweep - sweep funds in specified wallet:account to new address in same
relay - relay a transaction from a transaction file created using sweep
or transfer with the --no-relay option
submit - submit an autosigned transaction to a wallet and the network
txview - view a transaction file or files created using sweep or
transfer with the --no-relay option
txview - display detailed information about a transaction file or files
dump - produce JSON dumps of wallet metadata (accounts, addresses and
labels) for a list or range of wallets
restore - same as create, but additionally restore wallet metadata from
@ -135,6 +134,12 @@ wallets, creating the dumps is as easy as executing ‘mmgen-xmrwallet
JSON and thus suitable for efficient incremental backup using git.
TXVIEW OPERATION NOTES
Transactions are displayed in chronological order based on submit time or
creation time.
SECURITY WARNING
If you have an existing MMGen Monero hot wallet setup, youre strongly

View file

@ -61,9 +61,9 @@ opts_data = {
export-outputs operation
-a, --autosign Use appropriate outdir and other params for
autosigning operations (implies --watch-only).
When this option is in effect, the viewkey-
address file is located automatically, so the
xmr_keyaddrfile argument must be omitted.
When this option is in effect, filename argu-
ments must be omitted, as files are located
automatically.
-m, --autosign-mountpoint=P Specify the autosign mountpoint (defaults to
/mnt/mmgen_autosign, implies --autosign)
-b, --rescan-blockchain Rescan the blockchain if wallet fails to sync
@ -111,7 +111,10 @@ cfg = Config(opts_data=opts_data)
cmd_args = cfg._args
if cmd_args and cfg.autosign and (
cmd_args[0] in (MoneroWalletOps.kafile_arg_ops + ('export-outputs','import-key-images'))
cmd_args[0] in (
MoneroWalletOps.kafile_arg_ops
+ ('export-outputs','import-key-images')
)
or len(cmd_args) == 1 and cmd_args[0] == 'submit'
):
cmd_args.insert(1,None)

View file

@ -43,6 +43,7 @@ from .util import (
make_timestr,
make_chksum_N,
capfirst,
list_gen,
)
from .fileutil import get_data_from_file
from .seed import SeedID
@ -119,6 +120,8 @@ def is_xmr_tx_file(cfg,fn):
class MoneroMMGenFile:
silent_load = False
def make_chksum(self,keys=None):
res = json.dumps(
dict( (k,v) for k,v in self.data._asdict().items() if (not keys or k in keys) ),
@ -157,15 +160,36 @@ class MoneroMMGenFile:
)
def extract_data_from_file(self,cfg,fn):
return json.loads( get_data_from_file( cfg, str(fn), self.desc ))[self.data_label]
return json.loads(
get_data_from_file( cfg, str(fn), self.desc, silent=self.silent_load )
)[self.data_label]
class MoneroMMGenTX:
class Base(MoneroMMGenFile):
data_label = 'MoneroMMGenTX'
base_chksum_fields = ('op','create_time','network','seed_id','source','dest','amount')
full_chksum_fields = ('op','create_time','network','seed_id','source','dest','amount','fee','blob')
# both base_chksum and full_chksum are used to make the filename stem, so we must not include
# fields that change when TX is signed and submitted (e.g. ‘sign_time’)
base_chksum_fields = {
'op',
'create_time',
'network',
'seed_id',
'source',
'dest',
'amount' }
full_chksum_fields = {
'op',
'create_time',
'network',
'seed_id',
'source',
'dest',
'amount',
'fee',
'blob' }
chksum_nchars = 6
xmrwallet_tx_data = namedtuple('xmrwallet_tx_data',[
'op',
@ -195,66 +219,58 @@ class MoneroMMGenTX:
def get_info(self,indent=''):
d = self.data
if d.dest:
to_entry = f'\n{indent} To: ' + (
'Wallet {}, account {}, address {}'.format(
d.dest.wallet.hl(),
red(f'#{d.dest.account}'),
red(f'#{d.dest.account_address}')
)
)
fs = """
Info for transaction {a} [Seed ID: {b}. Network: {c}]:
TxID: {d}
Created: {e:19} [{f}]
Signed: {g:19} [{h}]
Type: {i}
From: Wallet {j}, account {k}{l}
Amount: {m} XMR
Fee: {n} XMR
Dest: {o}
"""
pmid = d.dest_address.parsed.payment_id
if pmid:
fs += ' Payment ID: {pmid}'
coldsign_status = (
pink(' [cold signed{}]'.format(', submitted' if d.complete else ''))
if d.signed_txset else '' )
pmt_id = d.dest_address.parsed.payment_id
fs = '\n'.join(list_gen(
['Info for transaction {a} [Seed ID: {b}. Network: {c}]:'],
[' TxID: {d}'],
[' Created: {e:19} [{f}]'],
[' Signed: {g:19} [{h}]', d.sign_time],
[' Type: {i}{S}'],
[' From: Wallet {j}, account {k}'],
[' To: Wallet {x}, account {y}, address {z}', d.dest],
[' Amount: {m} XMR'],
[' Fee: {n} XMR'],
[' Dest: {o}'],
[' Payment ID: {P}', pmt_id],
))
from .util2 import format_elapsed_hr
return fmt(fs,strip_char='\t',indent=indent).format(
a = orange(self.base_chksum.upper()),
a = orange(self.file_id),
b = d.seed_id.hl(),
c = yellow(d.network.upper()),
d = d.txid.hl(),
e = make_timestr(d.create_time),
f = format_elapsed_hr(d.create_time),
g = make_timestr(d.sign_time) if d.sign_time else '-',
h = format_elapsed_hr(d.sign_time) if d.sign_time else '-',
i = blue(capfirst(d.op)) + coldsign_status,
g = make_timestr(d.sign_time) if d.sign_time else None,
h = format_elapsed_hr(d.sign_time) if d.sign_time else None,
i = blue(capfirst(d.op)),
j = d.source.wallet.hl(),
k = red(f'#{d.source.account}'),
l = to_entry if d.dest else '',
m = d.amount.hl(),
n = d.fee.hl(),
o = d.dest_address.hl(),
pmid = pink(pmid.hex()) if pmid else None
P = pink(pmt_id.hex()) if pmt_id else None,
S = pink(f" [cold signed{', submitted' if d.complete else ''}]") if d.signed_txset else '',
x = d.dest.wallet.hl() if d.dest else None,
y = red(f'#{d.dest.account}') if d.dest else None,
z = red(f'#{d.dest.account_address}') if d.dest else None,
)
@property
def file_id(self):
return (self.base_chksum + ('-' + self.full_chksum if self.full_chksum else '')).upper()
def write(self,delete_metadata=False,ask_write=True,ask_overwrite=True):
dict_data = self.data._asdict()
if delete_metadata:
dict_data['metadata'] = None
fn = '{a}{b}-XMR[{c!s}]{d}.{e}'.format(
a = self.base_chksum.upper(),
b = (lambda s: f'-{s.upper()}' if s else '')(self.full_chksum),
c = self.data.amount,
d = (lambda s: '' if s == 'mainnet' else f'.{s}')(self.data.network),
e = self.ext
fn = '{a}-XMR[{b!s}]{c}.{d}'.format(
a = self.file_id,
b = self.data.amount,
c = (lambda s: '' if s == 'mainnet' else f'.{s}')(self.data.network),
d = self.ext
)
if self.cfg.autosign:
@ -294,8 +310,8 @@ class MoneroMMGenTX:
self.data = self.xmrwallet_tx_data(
op = d.op,
create_time = getattr(d,'create_time',now),
sign_time = (getattr(d,'sign_time',None) or now) if self.signed else None,
create_time = now if self.name in ('NewSigned','NewUnsigned') else getattr(d,'create_time',None),
sign_time = now if self.name in ('NewSigned','NewColdSigned') else getattr(d,'sign_time',None),
network = d.network,
seed_id = SeedID(sid=d.seed_id),
source = XMRWalletAddrSpec(d.source),
@ -308,7 +324,7 @@ class MoneroMMGenTX:
metadata = d.metadata,
unsigned_txset = d.unsigned_txset,
signed_txset = getattr(d,'signed_txset',None),
complete = True if self.name == 'NewSigned' else getattr(d,'complete',False),
complete = self.name in ('NewSigned','NewSubmitted'),
)
class NewUnsigned(New):
@ -337,17 +353,17 @@ class MoneroMMGenTX:
super().__init__()
self.cfg = cfg
self.fn = fn
self.fn = Path(fn)
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 not 'unsigned_txset' in d_wrap['data']: # backwards compat: use old checksum fields
self.full_chksum_fields = (
set(self.xmrwallet_tx_data._fields) -
{'metadata','unsigned_txset','signed_txset','complete'} )
if 'unsigned_txset' in d_wrap['data']: # post-autosign
self.full_chksum_fields &= set(d_wrap['data']) # allow for added chksum fields in future
else:
self.full_chksum_fields = set(d_wrap['data']) - {'metadata'}
for key in self.xmrwallet_tx_data._fields: # backwards compat: fill in missing fields
if not key in d_wrap['data']:
@ -355,7 +371,7 @@ class MoneroMMGenTX:
d = self.xmrwallet_tx_data(**d_wrap['data'])
if self.name != 'Completed':
if self.name not in ('View','Completed'):
assert fn.name.endswith('.'+self.ext), 'TX filename {fn} has incorrect extension (not {self.ext!r})'
assert getattr(d,self.req_field), f'{self.name} TX missing required field {self.req_field!r}'
assert bool(d.sign_time)==self.signed,'{} has {}sign time!'.format(self.desc,'no 'if self.signed else'')
@ -407,14 +423,17 @@ class MoneroMMGenTX:
desc = 'submitted transaction'
ext = 'subtx'
class View(Completed):
silent_load = True
class MoneroWalletOutputsFile:
class Base(MoneroMMGenFile):
desc = 'wallet outputs'
data_label = 'MoneroMMGenWalletOutputsFile'
base_chksum_fields = ('seed_id','wallet_index','outputs_data_hex',)
full_chksum_fields = ('seed_id','wallet_index','outputs_data_hex','signed_key_images')
base_chksum_fields = {'seed_id','wallet_index','outputs_data_hex',}
full_chksum_fields = {'seed_id','wallet_index','outputs_data_hex','signed_key_images'}
fn_fs = '{a}-outputs-{b}.{c}'
ext_offset = 25 # len('-outputs-') + len(chksum) ({b})
chksum_nchars = 16
@ -529,7 +548,7 @@ class MoneroWalletDumpFile:
class Base:
desc = 'Monero wallet dump'
data_label = 'MoneroMMGenWalletDumpFile'
base_chksum_fields = ('seed_id','wallet_index','wallet_metadata')
base_chksum_fields = {'seed_id','wallet_index','wallet_metadata'}
full_chksum_fields = None
ext = 'dump'
ext_offset = 0
@ -833,7 +852,7 @@ class MoneroWalletOps:
c = 'WatchOnly' if watch_only else '',
d = f'.{self.cfg.network}' if self.cfg.network != 'mainnet' else '')
)
@property
def add_wallet_desc(self):
return 'offline signing ' if self.offline else 'watch-only ' if self.cfg.watch_only else ''
@ -1644,7 +1663,6 @@ class MoneroWalletOps:
new_tx = MoneroMMGenTX.NewSubmitted(
cfg = self.cfg,
complete = True,
_in_tx = tx,
)
gmsg('\nOK')
@ -1796,10 +1814,10 @@ class MoneroWalletOps:
class txview(base):
async def main(self):
txs = sorted(
(MoneroMMGenTX.View( self.cfg, Path(fn) ) for fn in uarg.infile),
key = lambda x: x.data.create_time
)
self.cfg._util.stdout_or_pager(
'\n'.join(
tx.get_info() for tx in
sorted(
(MoneroMMGenTX.Completed( self.cfg, Path(fn) ) for fn in uarg.infile),
key = lambda x: x.data.sign_time or x.data.create_time )
))
'\n'.join(tx.get_info() for tx in txs)
)

View file

@ -51,8 +51,8 @@ class TestSuiteMisc(TestSuiteBase):
t = self.spawn(f'mmgen-xmrwallet',['txview','test/ref/monero/3EBD06-2D6E3B-XMR[0.74].testnet.sigtx'])
res = strip_ansi_escapes(t.read()).replace('\r','')
for s in (
'Amount: 0.74 XMR',
'Dest: 56VQ9M6k',
'Amount: 0.74 XMR',
'Dest: 56VQ9M6k',
):
assert s in res, f'{s} not in {res}'
return t