Monero offline transaction autosigning

Cold signing of Monero transactions with maximum convenience.

For general information about Monero offline signing, see:

    https://monerodocs.org/cold-storage/offline-transaction-signing/

The MMGen implementation automates the workflow described in that document,
with some minor changes.

A unique security feature is that signing wallets are created afresh in
volatile memory for each transacting session and thus disappear when the
signing machine is powered down.

Documentation:

    $ mmgen-xmrwallet --help # scroll to OFFLINE AUTOSIGNING

Testing:

    $ test/test.py --coin=xmr -e xmr_autosign
This commit is contained in:
The MMGen Project 2023-04-24 13:23:45 +00:00
commit de77f9c27d
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
14 changed files with 1437 additions and 96 deletions

View file

@ -9,7 +9,7 @@
# https://gitlab.com/mmgen/mmgen
"""
autosign: Auto-sign MMGen transactions and message files
autosign: Auto-sign MMGen transactions, message files and XMR wallet output files
"""
import sys,os,asyncio
@ -17,7 +17,7 @@ from subprocess import run,PIPE,DEVNULL
from collections import namedtuple
from .cfg import Config
from .util import msg,msg_r,ymsg,rmsg,gmsg,bmsg,die,suf,fmt,fmt_list
from .util import msg,msg_r,ymsg,rmsg,gmsg,bmsg,die,suf,fmt,fmt_list,async_run
from .color import yellow,red,orange
from .wallet import Wallet,get_wallet_cls
from .filename import find_file_in_dir
@ -39,6 +39,10 @@ class Signable:
def unsigned(self):
return self._unprocessed( '_unsigned', self.rawext, self.sigext )
@property
def unsubmitted(self):
return self._unprocessed( '_unsubmitted', self.sigext, self.subext )
def _unprocessed(self,attrname,rawext,sigext):
if not hasattr(self,attrname):
dirlist = tuple(os.scandir(self.dir))
@ -116,6 +120,58 @@ class Signable:
for f in bad_files:
yield red(f.path)
class xmr_transaction(transaction):
dir_name = 'xmr_tx_dir'
desc = 'Monero transaction'
subext = 'subtx'
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
if len(self.unsigned) > 1:
die('AutosignTXError', 'Only one unsigned XMR transaction allowed at a time!')
async def sign(self,f):
from .xmrwallet import MoneroMMGenTX,MoneroWalletOps,xmrwallet_uargs
tx1 = MoneroMMGenTX.Completed( self.parent.xmrwallet_cfg, f.path )
m = MoneroWalletOps.sign(
self.parent.xmrwallet_cfg,
xmrwallet_uargs(
infile = self.parent.wallet_files[0], # MMGen wallet file
wallets = str(tx1.src_wallet_idx),
spec = None ),
)
tx2 = await m.main(f.path) # TODO: stop wallet daemon?
tx2.write(ask_write=False)
return tx2
def print_summary(self,txs):
bmsg('\nAutosign summary:\n')
msg_r('\n'.join(tx.get_info() for tx in txs))
class xmr_wallet_outputs_file(transaction):
desc = 'Monero wallet outputs file'
rawext = 'raw'
sigext = 'sig'
dir_name = 'xmr_outputs_dir'
async def sign(self,f):
from .xmrwallet import MoneroWalletOps,xmrwallet_uargs
wallet_idx = MoneroWalletOps.wallet.get_idx_from_fn(f.name)
m = MoneroWalletOps.export_key_images(
self.parent.xmrwallet_cfg,
xmrwallet_uargs(
infile = self.parent.wallet_files[0], # MMGen wallet file
wallets = str(wallet_idx),
spec = None ),
)
obj = await m.main( f, wallet_idx )
obj.write()
return obj
def print_summary(self,txs):
bmsg('\nAutosign summary:')
msg(' ' + '\n '.join(tx.get_info() for tx in txs) + '\n')
class message(base):
desc = 'message file'
rawext = 'rawmsg.json'
@ -200,10 +256,18 @@ class Autosign:
self.coins = cfg.coins.upper().split(',') if cfg.coins else []
if cfg.xmrwallets and not 'XMR' in self.coins:
self.coins.append('XMR')
if not self.coins:
ymsg('Warning: no coins specified, defaulting to BTC')
self.coins = ['BTC']
if 'XMR' in self.coins:
self.xmr_dir = os.path.join( self.mountpoint, 'xmr' )
self.xmr_tx_dir = os.path.join( self.mountpoint, 'xmr', 'tx' )
self.xmr_outputs_dir = os.path.join( self.mountpoint, 'xmr', 'outputs' )
async def check_daemons_running(self):
from .protocol import init_proto
for coin in self.coins:
@ -239,7 +303,7 @@ class Autosign:
return self._wallet_files
def do_mount(self):
def do_mount(self,no_xmr_chk=False):
from stat import S_ISDIR,S_IWUSR,S_IRUSR
@ -272,6 +336,9 @@ class Autosign:
if self.have_msg_dir:
check_dir(self.msg_dir)
if 'XMR' in self.coins and not no_xmr_chk:
check_dir(self.xmr_tx_dir)
def do_umount(self):
if os.path.ismount(self.mountpoint):
run( ['sync'], check=True )
@ -330,7 +397,10 @@ class Autosign:
self.led.set('busy')
ret1 = await self.sign_all('transaction')
ret2 = await self.sign_all('message') if self.have_msg_dir else True
ret = ret1 and ret2
# import XMR wallet outputs BEFORE signing transactions:
ret3 = await self.sign_all('xmr_wallet_outputs_file') if 'XMR' in self.coins else True
ret4 = await self.sign_all('xmr_transaction') if 'XMR' in self.coins else True
ret = ret1 and ret2 and ret3 and ret4
self.do_umount()
self.led.set(('standby','off','error')[(not ret)*2 or bool(self.cfg.stealth_led)])
return ret
@ -365,7 +435,7 @@ class Autosign:
self.create_wallet_dir()
if not self.get_insert_status():
die(1,'Removable device not present!')
self.do_mount()
self.do_mount(no_xmr_chk=True)
self.wipe_existing_key()
self.create_key()
if not no_unmount:
@ -398,6 +468,53 @@ class Autosign:
ss_out = Wallet( self.cfg, ss=ss_in )
ss_out.write_to_file( desc='autosign wallet', outdir=self.wallet_dir )
@property
def xmrwallet_cfg(self):
if not hasattr(self,'_xmrwallet_cfg'):
from .cfg import Config
self._xmrwallet_cfg = Config({
'coin': 'xmr',
'wallet_rpc_user': 'autosigner',
'wallet_rpc_password': 'my very secret password',
'passwd_file': self.cfg.passwd_file,
'wallet_dir': self.wallet_dir,
'autosign': True,
'autosign_mountpoint': self.mountpoint,
'outdir': self.xmr_dir, # required by vkal.write()
})
return self._xmrwallet_cfg
def xmr_setup(self):
import shutil
try: shutil.rmtree(self.xmr_outputs_dir)
except: pass
os.makedirs(self.xmr_outputs_dir)
os.makedirs(self.xmr_tx_dir,exist_ok=True)
from .addrfile import ViewKeyAddrFile
from .fileutil import shred_file
for f in os.scandir(self.xmr_dir):
if f.name.endswith(ViewKeyAddrFile.ext):
msg(f'Shredding old viewkey-address file {f.name!r}')
shred_file(f.path)
if len(self.wallet_files) > 1:
ymsg(f'Warning: more that one wallet file, using the first ({self.wallet_files[0]}) for xmrwallet generation')
from .xmrwallet import MoneroWalletOps,xmrwallet_uargs
m = MoneroWalletOps.create_offline(
self.xmrwallet_cfg,
xmrwallet_uargs(
infile = self.wallet_files[0], # MMGen wallet file
wallets = self.cfg.xmrwallets, # XMR wallet idxs
spec = None ),
)
async_run(m.main())
async_run(m.stop_wallet_daemon())
def get_insert_status(self):
if self.cfg.no_insert_check:
return True

View file

@ -1 +1 @@
13.3.dev47
13.3.dev48

View file

@ -66,6 +66,7 @@ class ObjectInitError(Exception): mmcode = 2
class ClassFlagsError(Exception): mmcode = 2
class ExtensionModuleError(Exception): mmcode = 2
class MoneroMMGenTXFileParseError(Exception): mmcode = 2
class AutosignTXError(Exception): mmcode = 2
# 3: yellow hl, 'MMGen Error' + exception + message
class RPCFailure(Exception): mmcode = 3

View file

@ -46,8 +46,17 @@ sweep - sweep funds in specified wallet:account to new address in same
account or new account in another wallet
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
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
the corresponding JSON dump files created with dump
export-outputs - export outputs of watch-only wallets for later import
into their corresponding offline wallets
import-key-images - import key images signed by offline wallets into their
corresponding watch-only wallets
LABEL OPERATION NOTES
@ -103,24 +112,42 @@ Note that multiple sweep operations may be required to sweep all the funds
in an account.
RELAY OPERATION NOTES
SUBMIT AND RELAY OPERATION NOTES
By default, transactions are relayed to a monerod running on localhost at the
default RPC port. To relay transactions to a remote or non-default monerod
via optional SOCKS proxy, use the --tx-relay-daemon option described above.
When submit is used with --autosign, the transaction filename must be
omitted.
DUMP AND RESTORE OPERATION NOTES
These commands produce and read JSON wallet dump files with the same
filenames as their source wallets, plus a .dump extension.
Its highly advisable to make regular dumps of your Monero wallets and back
up the dump files, which can be used to easily regenerate the wallets using
the restore operation, should the need arise. For watch-only autosigning
wallets, creating the dumps is as easy as executing mmgen-xmrwallet
--autosign dump from your wallet directory. The dump files are formatted
JSON and thus suitable for efficient incremental backup using git.
SECURITY WARNING
To avoid exposing your private keys on a network-connected machine, youre
strongly advised to create all transactions offline using the --no-relay
option. For this, a monerod with a fully synced blockchain must be running
on the offline machine. The resulting transaction files are then sent using
the 'relay' operation.
If you have an existing MMGen Monero hot wallet setup, youre strongly
advised to migrate to offline autosigning to avoid further exposing your
private keys on your network-connected machine. See OFFLINE AUTOSIGNING
and Replacing Existing Hot Wallets with Watch-Only Wallets below.
EXAMPLES
Note that the transacting examples in this section apply for a hot wallet
setup, which is now deprecated. See OFFLINE AUTOSIGNING below.
Generate an XMR key-address file with 5 addresses from your default wallet:
$ mmgen-keygen --coin=xmr 1-5
@ -158,4 +185,235 @@ $ mmgen-xmrwallet new *.akeys.mmenc 2:1,"from ABC exchange"
View all the XMR transaction files in the current directory, sending output
to pager:
$ mmgen-xmrwallet --pager txview *XMR*.sigtx
OFFLINE AUTOSIGNING
Tutorial
Master the basic concepts of the MMGen wallet system and the processes of
wallet creation, conversion and backup described in the Getting Started
guide. Optionally create a default MMGen wallet on your offline machine
using mmgen-walletgen. If you choose not to do this, youll be prompted
for a seed phrase at the start of each signing session.
Familiarize yourself with the autosigning setup process as described in
mmgen-autosign --help. Prepare your removable device and set up the
mountpoints on your offline and online machines according to the instructions
therein. Install monero-wallet-rpc on your offline machine and the Monero
CLI wallet and daemon binaries on your online machine.
On the offline machine, insert the removable device and execute:
$ mmgen-autosign --xmrwallets=1-2,7 setup
This will create 3 Monero signing wallets with indexes 1, 2 and 7 and primary
addresses matching your seeds Monero addresses with the same indexes. (Note
that these particular indexes are arbitrary, for purposes of illustration
only. Feel free to choose your own list and/or range or perhaps just the
number 1 if one wallet is all you require).
These signing wallets are written to volatile memory and exist only for the
duration of the signing session, just like the temporary MMGen signing wallet
theyre generated from (see mmgen-autosign --help).
A viewkey-address file for the 3 addresses will also be written to the
removable device. The data in this file will be used to create and access
watch-only wallets on your online machine that match the signing wallets
youve just created.
When the setup operation completes, extract the removable device and restart
the autosign script in wait mode:
$ mmgen-autosign --coins=xmr --stealth-led wait
Your only further physical interaction with the offline signing machine now
(assuming everything goes as planned) will be inserting and extracting the
removable device on it. And this is the whole point of autosigning: to make
cold signing as convenient as possible, almost like transacting with a hot
wallet.
If your signing machine is an SoC with MMGen LED support (see mmgen-autosign
--help), a quickly flashing LED will indicate that signing is in progress, a
slowly flashing LED an error condition, and no LED that the program is idle
and waiting for device insertion.
On your online machine, start monerod, wait until its fully synced with the
network, insert the removable device and execute:
$ mmgen-xmrwallet --autosign --restore-height=current create
This will create 3 watch-only wallets matching your 3 offline signing wallets
and write them to the current directory (an alternate wallet directory may be
specified with the --wallet-dir option).
Note that --restore-height=current is required to prevent a time-consuming
full sync of the wallets from the Genesis block, a meaningless waste of time
in this case since the wallets contain no funds.
Also make note of the --autosign option, a requirement for ALL autosigning
operations with mmgen-xmrwallet.
Now list your newly created wallets:
$ mmgen-xmrwallet --autosign list
Note that you can also use the sync operation here, which produces more
abbreviated output than list.
Send some XMR (preferably a tiny amount) to the primary address of wallet #7.
Once the transaction has confirmed, invoke sync or list again to verify
the funds have arrived.
Since offline wallet #7 has no knowledge of the funds received by its online
counterpart, we need to update its state. Export the outputs of watch-only
wallet #7 as follows:
$ mmgen-xmrwallet --autosign export-outputs 7
The outputs are now saved to the removable device and will be imported into
offline wallet #7 when you sign your first transaction.
Now youre ready to begin transacting. Lets start by sweeping your funds in
wallet #7’s primary address (account 0) to a new address in the same account:
$ mmgen-xmrwallet --autosign sweep 7:0
This operation creates an unsigned sweep transaction and saves it to the
removable device.
Now extract the removable device and insert it on the offline machine. Wait
for the quick LED flashing to stop (or the blue safe to extract message, in
the absence of LED support), signalling that signing is complete.
Note that the offline wallet has performed two operations in one go here:
an import of wallet outputs from the previous step and the signing of your
just-created sweep transaction.
Extract the removable device, insert it on your online machine and submit the
signed sweep transaction to the watch-only wallet, which will broadcast it to
the network:
$ mmgen-xmrwallet --autosign submit
Note that you may also relay the transaction to a remote daemon, optionally
via a Tor proxy, using the --tx-relay-daemon option documented above.
Once your transaction has confirmed, invoke list or sync to view your
wallets balances.
Congratulations, youve performed your first autosigned Monero transaction!
For other examples, consult the EXAMPLES section above, noting the following
differences that apply to autosigning:
1) The --autosign option must always be included.
2) The key-address file argument must always be omitted.
3) The relay operation is replaced by submit, with TX filename omitted.
4) Always remember to sign your transactions after a sweep or transfer
operation.
5) Always remember to export a wallets outputs when it has received funds
from an outside source.
Exporting Outputs
Exporting outputs from a wallet is generally required only in two cases:
a) after the wallet has received funds from an outside source or another
wallet; and
c) at the start of each signing session (after mmgen-autosign setup).
However, sometimes things go wrong and an offline wallet may be unable to
sign a transaction due to missing outputs. In this case, you may export all
outputs from its corresponding watch-only wallet with the following command:
$ mmgen-xmrwallet --autosign --export-all export-outputs <wallet index>
Then reinsert the removable device on the offline machine to redo the
signing operation, which should now succeed.
At the start of each new signing session, you must export ALL outputs from
ALL wallets you intend to transact with:
$ mmgen-xmrwallet --autosign --export-all export-outputs [wallet indexes]
This is necessary because the signing wallets have just been created and
therefore know nothing about the state of their watch-only counterparts.
Replacing Existing Hot Wallets with Watch-Only Wallets
If you have an existing MMGen Monero hot wallet setup, you can migrate to
offline transaction signing by cloning your existing hot wallets as
watch-only ones via the dump and restore operations described below.
For additional security, its also wise to create new watch-only wallets that
have never had keys exposed on an online machine and gradually transfer all
funds from your cloned wallets to them. The creation of new wallets is
explained in the Tutorial above.
Start the cloning process by making dump files of your hot wallets metadata
(accounts, subaddresses and labels). cd to the wallet directory (or use
--wallet-dir) and execute:
$ mmgen-xmrwallet dump /path/to/key-address-file.akeys{.mmenc}
If youve been transacting with the wallets, you know where their key-address
file is along with its encryption password, if any. Supply an additional
index range and/or list at the end of the command line if the key-address
file contains more wallets than exist on disk or there are wallets you wish
to ignore.
Do a directory listing to verify that the dump files are present alongside
their source wallet files ending with MoneroWallet. Then execute:
$ mmgen-xmrwallet --watch-only restore /path/to/key-address-file.akeys{.mmenc}
This will create watch-only wallets that mirror the old hot wallets and
populate them with the metadata saved in the dump files.
Note that watch-only wallet filenames end with MoneroWatchOnlyWallet. Your
old hot wallets will be ignored from here on. Eventually, youll want to
destroy them.
Your new wallets must now be synced with the blockchain. Begin by starting
monerod and synchronizing with the network.
Mount /mnt/mmgen_autosign and locate the file in the xmr directory with
the .vkeys extension, which contains the passwords youll need to log into
the wallets. This is a plain text file viewable with cat, less or your
favorite text editor.
Then log into each watch-only wallet in turn as follows:
$ monero-wallet-cli --wallet <wallet filename>
Upon login, each wallet will begin syncing, a process which can take more
than an hour depending on your hardware. Note, however, that the process
is interruptible: you may exit monero-wallet-cli at any point, log back
in again and resume where you left off.
Once your watch-only wallets are synced, you need to export their outputs:
$ mmgen-xmrwallet --autosign export-outputs
Now insert the removable device on the offline machine and wait until the LED
stops flashing (or safe to extract). The wallet outputs are now imported
into the signing wallets and corresponding signed key images have been
written to the removable device.
Insert the removable device on your online machine and import the key images
into your watch-only wallets:
$ mmgen-xmrwallet --autosign import-key-images
Congratulations, your watch-only wallets are now complete and you may begin
transacting! First perform a sync or list to ensure that your balances
are correct. Then you might try sweeping some funds as described in the
Tutorial above.
Once youve gained proficiency with the autosigning process and feel ready
to delete your old hot wallets, make sure to do so securely using shred,
wipe or some other secure deletion utility.
""".strip()

View file

@ -17,20 +17,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
mmgen-autosign: Auto-sign MMGen transactions and message files
autosign: Auto-sign MMGen transactions, message files and XMR wallet output files
"""
import sys
from .cfg import Config
from .util import die,fmt_list,exit_if_mswin,async_run
from .util import msg,die,fmt_list,exit_if_mswin,async_run
exit_if_mswin('autosigning')
opts_data = {
'sets': [('stealth_led', True, 'led', True)],
'text': {
'desc': 'Auto-sign MMGen transactions and message files',
'desc': 'Auto-sign MMGen transactions, message files and XMR wallet output files',
'usage':'[opts] [operation]',
'options': """
-h, --help Print this help message
@ -52,6 +52,7 @@ opts_data = {
-v, --verbose Produce more verbose output
-w, --wallet-dir=D Specify an alternate wallet dir
(default: {asi.dfl_wallet_dir!r})
-x, --xmrwallets=L Range or list of wallets to be used for XMR autosigning
""",
'notes': """
@ -66,8 +67,9 @@ wait - start in loop mode: wait-mount-sign-unmount-wait
USAGE NOTES
If no operation is specified, this program mounts a removable device
(typically a USB flash drive) containing unsigned MMGen transactions and/or
message files, signs them, unmounts the removable device and exits.
(typically a USB flash drive) containing unsigned MMGen transactions, message
files, and/or XMR wallet output files, signs them, unmounts the removable
device and exits.
If invoked with wait, the program waits in a loop, mounting the removable
device, performing signing operations and unmounting the device every time it
@ -170,6 +172,10 @@ if len(cmd_args) == 1:
sys.exit(0)
elif cmd == 'setup':
asi.setup()
from .ui import keypress_confirm
if cfg.xmrwallets and keypress_confirm( cfg, '\nContinue with Monero setup?', default_yes=True ):
msg('')
asi.xmr_setup()
sys.exit(0)
elif cmd != 'wait':
die(1,f'{cmd!r}: unrecognized command')

View file

@ -30,25 +30,43 @@ from .xmrwallet import (
MoneroWalletOps,
xmrwallet_uarg_info,
xmrwallet_uargs,
get_autosign_obj,
)
opts_data = {
'sets': [
('autosign',True,'watch_only',True),
('autosign_mountpoint',bool,'autosign',True),
('autosign_mountpoint',bool,'watch_only',True),
],
'text': {
'desc': """Perform various Monero wallet and transacting operations for
addresses in an MMGen XMR key-address file""",
'usage2': [
'[opts] create | sync | list <xmr_keyaddrfile> [wallets]',
'[opts] label <xmr_keyaddrfile> LABEL_SPEC',
'[opts] new <xmr_keyaddrfile> NEW_ADDRESS_SPEC',
'[opts] transfer <xmr_keyaddrfile> TRANSFER_SPEC',
'[opts] sweep <xmr_keyaddrfile> SWEEP_SPEC',
'[opts] create | sync | list | dump | restore [xmr_keyaddrfile] [wallets]',
'[opts] label [xmr_keyaddrfile] LABEL_SPEC',
'[opts] new [xmr_keyaddrfile] NEW_ADDRESS_SPEC',
'[opts] transfer [xmr_keyaddrfile] TRANSFER_SPEC',
'[opts] sweep [xmr_keyaddrfile] SWEEP_SPEC',
'[opts] submit [TX_file]',
'[opts] relay <TX_file>',
'[opts] txview <TX_file> ...',
'[opts] export-outputs [wallets]',
'[opts] import-key-images [wallets]',
],
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common
options)
-A, --export-all Export all outputs when performing the
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.
-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
-d, --outdir=D Save transaction files to directory 'D'
instead of the working directory
@ -64,6 +82,7 @@ opts_data = {
-R, --no-relay Save transaction to file instead of relaying
-s, --no-start-wallet-daemon Dont start the wallet daemon at startup
-S, --no-stop-wallet-daemon Dont stop the wallet daemon at exit
-W, --watch-only Create or operate on watch-only wallets
-w, --wallet-dir=D Output or operate on wallets in directory 'D'
instead of the working directory
-H, --wallet-rpc-host=host Wallet RPC hostname (currently: {cfg.monero_wallet_rpc_host!r})
@ -92,6 +111,12 @@ 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'))
or len(cmd_args) == 1 and cmd_args[0] == 'submit'
):
cmd_args.insert(1,None)
if len(cmd_args) < 2:
cfg._opts.usage()
@ -99,25 +124,36 @@ op = cmd_args.pop(0)
infile = cmd_args.pop(0)
wallets = spec = None
if op not in MoneroWalletOps.ops:
if op.replace('-','_') not in MoneroWalletOps.ops:
die(1,f'{op!r}: unrecognized operation')
if op == 'relay':
if op in ('relay','submit'):
if len(cmd_args) != 0:
cfg._opts.usage()
elif op == 'txview':
infile = [infile] + cmd_args
elif op in ('create','sync','list'):
if len(cmd_args) not in (0,1):
elif op in ('create','sync','list','dump','restore'): # kafile_arg_ops
if len(cmd_args) > 1:
cfg._opts.usage()
if cmd_args:
wallets = cmd_args[0]
wallets = cmd_args.pop(0) if cmd_args else None
elif op in ('new','transfer','sweep','label'):
if len(cmd_args) != 1:
cfg._opts.usage()
spec = cmd_args[0]
elif op in ('export-outputs','import-key-images'):
if not cfg.autosign: # --autosign only for now - TODO
die(f'--autosign must be used with command {op!r}')
if len(cmd_args) > 1:
cfg._opts.usage()
wallets = cmd_args.pop(0) if cmd_args else None
m = getattr(MoneroWalletOps,op)(
if cfg.autosign and not cfg.test_suite:
asi = get_autosign_obj(cfg)
if not asi.get_insert_status():
die(1,'Removable device not present!')
asi.do_mount()
m = getattr(MoneroWalletOps,op.replace('-','_'))(
cfg,
xmrwallet_uargs(infile, wallets, spec))
@ -131,3 +167,6 @@ try:
async_run(m.stop_wallet_daemon())
except Exception as e:
ymsg(f'Unable to stop wallet daemon: {type(e).__name__}: {e}')
if cfg.autosign and not cfg.test_suite:
asi.do_umount()

View file

@ -101,7 +101,8 @@ class MoneroWalletDaemon(RPCDaemon):
daemon_addr = None,
proxy = None,
port_shift = None,
datadir = None ):
datadir = None,
trust_daemon = False ):
self.proto = proto
self.test_suite = test_suite
@ -143,7 +144,8 @@ class MoneroWalletDaemon(RPCDaemon):
"the MMGen config file." )
self.daemon_args = list_gen(
['--untrusted-daemon'],
['--trusted-daemon', trust_daemon],
['--untrusted-daemon', not trust_daemon],
[f'--rpc-bind-port={self.rpc_port}'],
['--wallet-dir='+self.wallet_dir],
['--log-file='+self.logfile],

View file

@ -39,6 +39,7 @@ class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Base):
mmcaps = ('key','addr')
ignore_daemon_version = False
coin_amt = 'XMRAmt'
sign_mode = 'standalone'
def get_addr_len(self,addr_fmt):
return (64,72)[addr_fmt == 'monero_integrated']

View file

@ -23,13 +23,15 @@ xmrwallet.py - MoneroWalletOps class
import os,re,time,json
from collections import namedtuple
from .objmethods import MMGenObject,Hilite,InitErrors
from .obj import CoinTxID
from .obj import CoinTxID,Int
from .color import red,yellow,green,blue,cyan,pink,orange
from .util import (
msg,
msg_r,
gmsg,
bmsg,
ymsg,
rmsg,
gmsg_r,
pp_msg,
die,
@ -45,7 +47,7 @@ from .seed import SeedID
from .protocol import init_proto
from .proto.btc.common import b58a
from .addr import CoinAddr,AddrIdx
from .addrlist import KeyAddrList,AddrIdxList
from .addrlist import KeyAddrList,ViewKeyAddrList,AddrIdxList
from .rpc import json_encoder
from .proto.xmr.rpc import MoneroRPCClient,MoneroWalletRPCClient
from .proto.xmr.daemon import MoneroWalletDaemon
@ -70,6 +72,16 @@ xmrwallet_uarg_info = (
r'(?:[^:]+):(?:\d+)'
)
def get_autosign_obj(cfg):
from .autosign import Autosign,AutosignConfig
return Autosign(
AutosignConfig({
'mountpoint': cfg.autosign_mountpoint,
'test_suite': cfg.test_suite,
'coins': 'XMR',
})
)
class XMRWalletAddrSpec(str,Hilite,InitErrors,MMGenObject):
color = 'cyan'
width = 0
@ -160,6 +172,7 @@ class MoneroMMGenTX:
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')
chksum_nchars = 6
xmrwallet_tx_data = namedtuple('xmrwallet_tx_data',[
'op',
@ -175,12 +188,18 @@ class MoneroMMGenTX:
'fee',
'blob',
'metadata',
'unsigned_txset',
'signed_txset',
'complete',
])
full_chksum_fields = set(xmrwallet_tx_data._fields) - {'metadata'}
def __init__(self):
self.name = type(self).__name__
@property
def src_wallet_idx(self):
return int(self.data.source.split(':')[0])
def get_info(self,indent=''):
d = self.data
if d.dest:
@ -208,6 +227,10 @@ class MoneroMMGenTX:
if pmid:
fs += ' Payment ID: {pmid}'
coldsign_status = (
pink(' [cold signed{}]'.format(', submitted' if d.complete else ''))
if d.signed_txset else '' )
from .util2 import format_elapsed_hr
return fmt(fs,strip_char='\t',indent=indent).format(
a = orange(self.base_chksum.upper()),
@ -216,9 +239,9 @@ class MoneroMMGenTX:
d = d.txid.hl(),
e = make_timestr(d.create_time),
f = format_elapsed_hr(d.create_time),
g = make_timestr(d.sign_time),
h = format_elapsed_hr(d.sign_time),
i = blue(capfirst(d.op)),
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,
j = d.source.wallet.hl(),
k = red(f'#{d.source.account}'),
l = to_entry if d.dest else '',
@ -241,6 +264,9 @@ class MoneroMMGenTX:
e = self.ext
)
if self.cfg.autosign:
fn = os.path.join( get_autosign_obj(self.cfg).xmr_tx_dir, fn )
from .fileutil import write_data_to_file
write_data_to_file(
cfg = self.cfg,
@ -249,7 +275,8 @@ class MoneroMMGenTX:
desc = self.desc,
ask_write = ask_write,
ask_write_default_yes = not ask_write,
ask_overwrite = ask_overwrite )
ask_overwrite = ask_overwrite,
ignore_opt_outdir = self.cfg.autosign )
class New(Base):
@ -259,7 +286,13 @@ class MoneroMMGenTX:
assert not args, 'Non-keyword args not permitted'
d = namedtuple('kwargs_tuple',kwargs)(**kwargs)
if '_in_tx' in kwargs:
in_data = kwargs.pop('_in_tx').data._asdict()
in_data.update(kwargs)
else:
in_data = kwargs
d = namedtuple('monero_tx_in_data_tuple',in_data)(**in_data)
self.cfg = d.cfg
proto = init_proto( self.cfg, 'xmr', network=d.network, need_amt=True )
@ -268,8 +301,8 @@ class MoneroMMGenTX:
self.data = self.xmrwallet_tx_data(
op = d.op,
create_time = now,
sign_time = now,
create_time = getattr(d,'create_time',now),
sign_time = (getattr(d,'sign_time',None) or now) if self.signed else None,
network = d.network,
seed_id = SeedID(sid=d.seed_id),
source = XMRWalletAddrSpec(d.source),
@ -280,15 +313,31 @@ class MoneroMMGenTX:
fee = proto.coin_amt(d.fee,from_unit='atomic'),
blob = d.blob,
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),
)
class NewUnsigned(New):
desc = 'unsigned transaction'
ext = 'rawtx'
signed = False
class NewSigned(New):
desc = 'signed transaction'
ext = 'sigtx'
signed = True
class NewColdSigned(NewSigned):
pass
class NewSubmitted(NewColdSigned):
desc = 'submitted transaction'
ext = 'subtx'
class Completed(Base):
desc = 'transaction'
forbidden_fields = ()
def __init__(self,cfg,fn):
@ -302,11 +351,23 @@ class MoneroMMGenTX:
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'} )
for key in self.xmrwallet_tx_data._fields: # backwards compat: fill in missing fields
if not key in d_wrap['data']:
d_wrap['data'][key] = None
d = self.xmrwallet_tx_data(**d_wrap['data'])
if self.name != 'Completed':
assert fn.endswith('.'+self.ext), 'TX filename {fn!r} 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'')
for f in self.forbidden_fields:
assert not getattr(d,f), f'{self.name} TX mismatch: contains forbidden field {f!r}'
proto = init_proto( cfg, 'xmr', network=d.network, need_amt=True )
@ -324,20 +385,182 @@ class MoneroMMGenTX:
fee = proto.coin_amt(d.fee),
blob = d.blob,
metadata = d.metadata,
unsigned_txset = d.unsigned_txset,
signed_txset = d.signed_txset,
complete = d.complete,
)
self.check_checksums(d_wrap)
class Unsigned(Completed):
desc = 'unsigned transaction'
ext = 'rawtx'
signed = False
req_field = 'unsigned_txset'
forbidden_fields = ('signed_txset',)
class Signed(Completed):
desc = 'signed transaction'
ext = 'sigtx'
signed = True
req_field = 'blob'
forbidden_fields = ('signed_txset','unsigned_txset')
class ColdSigned(Signed):
req_field = 'signed_txset'
forbidden_fields = ()
class Submitted(ColdSigned):
desc = 'submitted transaction'
ext = 'subtx'
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')
fn_fs = '{a}-outputs-{b}.{c}'
ext_offset = 25 # len('-outputs-') + len(chksum) ({b})
chksum_nchars = 16
data_tuple = namedtuple('wallet_outputs_data',[
'seed_id',
'wallet_index',
'outputs_data_hex',
'signed_key_images',
])
def __init__(self,cfg):
self.name = type(self).__name__
self.cfg = cfg
def write(self,add_suf=''):
from .fileutil import write_data_to_file
write_data_to_file(
cfg = self.cfg,
outfile = self.get_outfile( self.cfg, self.wallet_fn ) + add_suf,
data = self.make_wrapped_data(self.data._asdict()),
desc = self.desc,
ask_overwrite = False,
ignore_opt_outdir = True )
def get_outfile(self,cfg,wallet_fn):
fn = self.fn_fs.format(
a = wallet_fn,
b = self.base_chksum,
c = self.ext,
)
return os.path.join(
get_autosign_obj(cfg).xmr_outputs_dir,
os.path.basename(fn) ) if cfg.autosign else fn
def get_wallet_fn(self,fn):
assert fn.endswith(f'.{self.ext}'), (
f'{type(self).__name__}: filename does not end with {"."+self.ext!r}'
)
return fn[:-(len(self.ext)+self.ext_offset+1)]
def get_info(self,indent=''):
if self.data.signed_key_images is not None:
data = self.data.signed_key_images or []
return f'{self.wallet_fn}: {len(data)} signed key image{suf(data)}'
else:
return f'{self.wallet_fn}: no key images'
class New(Base):
ext = 'raw'
def __init__( self, parent, wallet_fn, data, wallet_idx=None ):
super().__init__(parent.cfg)
self.wallet_fn = wallet_fn
init_data = dict.fromkeys(self.data_tuple._fields)
init_data.update({
'seed_id': parent.kal.al_id.sid,
'wallet_index': wallet_idx or parent.get_idx_from_fn(os.path.basename(wallet_fn)),
})
init_data.update({k:v for k,v in data.items() if k in init_data})
self.data = self.data_tuple(**init_data)
class Completed(New):
def __init__( self, parent, fn=None, wallet_fn=None ):
def check_equal(desc,a,b):
assert a == b, f'{desc} mismatch: {a} (from file) != {b} (from filename)'
fn = fn or self.get_outfile( parent.cfg, wallet_fn )
wallet_fn = wallet_fn or self.get_wallet_fn(fn)
d_wrap = self.extract_data_from_file( parent.cfg, fn )
data = d_wrap['data']
check_equal( 'Seed ID', data['seed_id'], parent.kal.al_id.sid )
wallet_idx = parent.get_idx_from_fn(os.path.basename(wallet_fn))
check_equal( 'Wallet index', data['wallet_index'], wallet_idx )
super().__init__(
parent = parent,
wallet_fn = wallet_fn,
data = data,
wallet_idx = wallet_idx,
)
self.check_checksums(d_wrap)
@classmethod
def find_fn_from_wallet_fn(cls,cfg,wallet_fn,ret_on_no_match=False):
path = get_autosign_obj(cfg).xmr_outputs_dir or os.curdir
fn = os.path.basename(wallet_fn)
pat = cls.fn_fs.format(
a = fn,
b = f'[0-9a-f]{{{cls.chksum_nchars}}}\\',
c = cls.ext,
)
matches = [f for f in os.scandir(path) if re.match(pat,f.name)]
if not matches and ret_on_no_match:
return None
if not matches or len(matches) > 1:
die(2,'{a} matching pattern {b!r} found in {c}!'.format(
a = 'No files' if not matches else 'More than one file',
b = pat,
c = path
))
return matches[0].path
class Unsigned(Completed):
pass
class SignedNew(New):
desc = 'signed key images'
ext = 'sig'
class Signed(Completed,SignedNew):
pass
class MoneroWalletDumpFile:
class Base:
desc = 'Monero wallet dump'
data_label = 'MoneroMMGenWalletDumpFile'
base_chksum_fields = ('seed_id','wallet_index','wallet_metadata')
full_chksum_fields = None
ext = 'dump'
ext_offset = 0
data_tuple = namedtuple('wallet_dump_data',[
'seed_id',
'wallet_index',
'wallet_metadata',
])
def get_outfile(self,cfg,wallet_fn):
return f'{wallet_fn}.{self.ext}'
class New(Base,MoneroWalletOutputsFile.New):
pass
class Completed(Base,MoneroWalletOutputsFile.Completed):
pass
class MoneroWalletOps:
ops = (
'create',
'create_offline',
'sync',
'list',
'new',
@ -345,7 +568,24 @@ class MoneroWalletOps:
'sweep',
'relay',
'txview',
'label' )
'label',
'sign',
'submit',
'dump',
'restore',
'export_outputs',
'import_key_images' )
kafile_arg_ops = (
'create',
'sync',
'list',
'label',
'new',
'transfer',
'sweep',
'dump',
'restore' )
opts = (
'wallet_dir',
@ -356,13 +596,16 @@ class MoneroWalletOps:
'restore_height',
'no_start_wallet_daemon',
'no_stop_wallet_daemon',
'no_relay' )
'no_relay',
'watch_only',
'autosign' )
pat_opts = ('daemon','tx_relay_daemon')
class base(MMGenObject):
opts = ('wallet_dir',)
trust_daemon = False
def __init__(self,cfg,uarg_tuple):
@ -448,8 +691,12 @@ class MoneroWalletOps:
'daemon',
'no_start_wallet_daemon',
'no_stop_wallet_daemon',
'autosign',
'watch_only',
)
wallet_exists = True
start_daemon = True
offline = False
skip_wallet_check = False # for debugging
def __init__(self,cfg,uarg_tuple):
@ -470,11 +717,36 @@ class MoneroWalletOps:
super().__init__(cfg,uarg_tuple)
self.kal = KeyAddrList(
cfg = cfg,
proto = self.proto,
addrfile = uarg.infile,
key_address_validity_check = True )
if self.offline:
from .wallet import Wallet
self.seed_src = Wallet(
cfg = cfg,
fn = uarg.infile,
ignore_in_fmt = True )
gmsg('\nCreating ephemeral key-address list for offline wallets')
self.kal = KeyAddrList(
cfg = cfg,
proto = self.proto,
seed = self.seed_src.seed,
addr_idxs = uarg.wallets,
skip_chksum_msg = True )
else:
# with watch_only, make a second attempt to open the file as KeyAddrList:
for first_try in (True,False):
try:
self.kal = (ViewKeyAddrList if (self.cfg.watch_only and first_try) else KeyAddrList)(
cfg = cfg,
proto = self.proto,
addrfile = self.autosign_viewkey_addr_file if self.cfg.autosign else uarg.infile,
key_address_validity_check = True,
skip_chksum_msg = True )
break
except:
if first_try:
msg(f'Attempting to open {uarg.infile} as key-address list')
continue
raise
msg('')
@ -483,17 +755,25 @@ class MoneroWalletOps:
if not self.skip_wallet_check:
check_wallets()
relay_opt = self.parse_tx_relay_opt() if self.name == 'submit' and self.cfg.tx_relay_daemon else None
self.wd = MoneroWalletDaemon(
cfg = self.cfg,
proto = self.proto,
wallet_dir = self.cfg.wallet_dir or '.',
test_suite = self.cfg.test_suite,
daemon_addr = self.cfg.daemon or None,
daemon_addr = relay_opt[1] if relay_opt else (self.cfg.daemon or None),
trust_daemon = self.trust_daemon,
)
u = self.wd.usr_daemon_args = []
if self.name == 'create' and self.cfg.restore_height is None:
if self.offline or (self.name in ('create','restore') and self.cfg.restore_height is None):
u.append('--offline')
if relay_opt:
if self.cfg.test_suite:
u.append('--daemon-ssl-allow-any-cert')
if relay_opt[2]:
u.append(f'--proxy={relay_opt[2]}')
self.c = MoneroWalletRPCClient(
cfg = self.cfg,
@ -501,9 +781,13 @@ class MoneroWalletOps:
test_connection = False,
)
if not self.cfg.no_start_wallet_daemon:
if self.start_daemon and not self.cfg.no_start_wallet_daemon:
async_run(self.c.restart_daemon())
@classmethod
def get_idx_from_fn(cls,fn):
return int( re.match(r'[0-9a-fA-F]{8}-(\d+)-Monero(WatchOnly)?Wallet.*',fn)[1] )
def get_coin_daemon_rpc(self):
host,port = self.cfg.daemon.split(':') if self.cfg.daemon else ('localhost',self.wd.daemon_port)
@ -518,6 +802,22 @@ class MoneroWalletOps:
user = None,
passwd = None )
@property
def autosign_viewkey_addr_file(self):
from .addrfile import ViewKeyAddrFile
mpdir = get_autosign_obj(self.cfg).xmr_dir
fnlist = [f for f in os.listdir(mpdir) if f.endswith(ViewKeyAddrFile.ext)]
if len(fnlist) != 1:
die(2,
'{a} viewkey-address files found in autosign mountpoint directory {b!r}!\n'.format(
a = 'Multiple' if fnlist else 'No',
b = mpdir
)
+ 'Have you run ‘mmgen-autosign setup’ on your offline machine with the --xmrwallets option?'
)
else:
return os.path.join( mpdir, fnlist[0] )
def create_addr_data(self):
if uarg.wallets:
idxs = AddrIdxList(uarg.wallets)
@ -531,18 +831,22 @@ class MoneroWalletOps:
if not self.cfg.no_stop_wallet_daemon:
await self.c.stop_daemon()
def get_wallet_fn(self,data):
def get_wallet_fn(self,data,watch_only=None):
if watch_only is None:
watch_only = self.cfg.watch_only
return os.path.join(
self.cfg.wallet_dir or '.','{a}-{b}-MoneroWallet{c}'.format(
self.cfg.wallet_dir or '.','{a}-{b}-Monero{c}Wallet{d}'.format(
a = self.kal.al_id.sid,
b = data.idx,
c = f'.{self.cfg.network}' if self.cfg.network != 'mainnet' else ''))
c = 'WatchOnly' if watch_only else '',
d = f'.{self.cfg.network}' if self.cfg.network != 'mainnet' else ''))
async def main(self):
gmsg('\n{a}ing {b} wallet{c}'.format(
gmsg('\n{a}ing {b} {c}wallet{d}'.format(
a = self.stem.capitalize(),
b = len(self.addr_data),
c = suf(self.addr_data) ))
c = 'watch-only ' if self.cfg.watch_only else '',
d = suf(self.addr_data) ))
processed = 0
for n,d in enumerate(self.addr_data): # [d.sec,d.addr,d.wallet_passwd,d.viewkey]
fn = self.get_wallet_fn(d)
@ -559,6 +863,14 @@ class MoneroWalletOps:
gmsg(f'\n{processed} wallet{suf(processed)} {self.stem}ed')
return processed
def head_msg(self,wallet_idx,fn):
gmsg('\n{} {} wallet #{} ({})'.format(
self.action.capitalize(),
self.wallet_desc,
wallet_idx,
os.path.basename(fn)
))
class rpc:
def __init__(self,parent,d):
@ -567,6 +879,9 @@ class MoneroWalletOps:
self.c = parent.c
self.d = d
self.fn = parent.get_wallet_fn(d)
self.new_tx_cls = (
MoneroMMGenTX.NewUnsigned if self.cfg.watch_only else
MoneroMMGenTX.NewSigned )
def open_wallet(self,desc,refresh=True):
gmsg_r(f'\n Opening {desc} wallet...')
@ -577,7 +892,8 @@ class MoneroWalletOps:
gmsg('done')
if refresh:
gmsg_r(f' Refreshing {desc} wallet...')
m = ' and contacting relay' if self.parent.name == 'submit' and self.cfg.tx_relay_daemon else ''
gmsg_r(f' Refreshing {desc} wallet{m}...')
ret = self.c.call('refresh')
gmsg('done')
if ret['received_money']:
@ -695,7 +1011,7 @@ class MoneroWalletOps:
get_tx_hex = True,
get_tx_metadata = True
)
return MoneroMMGenTX.NewSigned(
return self.new_tx_cls(
cfg = self.cfg,
op = self.parent.name,
network = self.parent.proto.network,
@ -708,6 +1024,7 @@ class MoneroWalletOps:
fee = res['fee'],
blob = res['tx_blob'],
metadata = res['tx_metadata'],
unsigned_txset = res['unsigned_txset'] if self.cfg.watch_only else None,
)
def make_sweep_tx(self,account,dest_acct,dest_addr_idx,addr):
@ -723,7 +1040,7 @@ class MoneroWalletOps:
if len(res['tx_hash_list']) > 1:
die(3,'More than one TX required. Cannot perform this sweep')
return MoneroMMGenTX.NewSigned(
return self.new_tx_cls(
cfg = self.cfg,
op = self.parent.name,
network = self.parent.proto.network,
@ -739,6 +1056,7 @@ class MoneroWalletOps:
fee = res['fee_list'][0],
blob = res['tx_blob_list'][0],
metadata = res['tx_metadata_list'][0],
unsigned_txset = res['unsigned_txset'] if self.cfg.watch_only else None,
)
def relay_tx(self,tx_hex):
@ -766,26 +1084,137 @@ class MoneroWalletOps:
else:
restore_height = self.cfg.restore_height
from .xmrseed import xmrseed
ret = self.c.call(
'restore_deterministic_wallet',
filename = os.path.basename(fn),
password = d.wallet_passwd,
seed = xmrseed().fromhex(d.sec.wif,tostr=True),
restore_height = restore_height,
language = 'English' )
if self.cfg.watch_only:
ret = self.c.call(
'generate_from_keys',
filename = os.path.basename(fn),
password = d.wallet_passwd,
address = d.addr,
viewkey = d.viewkey,
restore_height = restore_height )
else:
from .xmrseed import xmrseed
ret = self.c.call(
'restore_deterministic_wallet',
filename = os.path.basename(fn),
password = d.wallet_passwd,
seed = xmrseed().fromhex(d.sec.wif,tostr=True),
restore_height = restore_height,
language = 'English' )
pp_msg(ret) if self.cfg.debug else msg(' Address: {}'.format( ret['address'] ))
return True
class sync(wallet):
opts = ('rescan_blockchain',)
class create_offline(create):
offline = True
def __init__(self,cfg,uarg_tuple):
super().__init__(cfg,uarg_tuple)
host,port = self.cfg.daemon.split(':') if self.cfg.daemon else ('localhost',self.wd.daemon_port)
gmsg('\nCreating viewkey-address file for watch-only wallets')
vkal = ViewKeyAddrList(
cfg = self.cfg,
proto = self.proto,
addrfile = None,
addr_idxs = uarg.wallets,
seed = self.seed_src.seed,
skip_chksum_msg = True )
vkf = vkal.file
# before writing viewkey-address file, delete any old ones in the directory:
for fn in os.listdir(self.cfg.outdir):
if fn.endswith(vkf.ext):
os.unlink(os.path.join(self.cfg.outdir,fn))
vkf.write() # write file to self.cfg.outdir
class restore(create):
def check_uopts(self):
if self.cfg.restore_height is not None:
die(1,f'--restore-height must be unset when running the ‘restore’ command')
async def process_wallet(self,d,fn,last):
def get_dump_data():
fns = [fn for fn in
[self.get_wallet_fn(d,watch_only=wo) + '.dump' for wo in (True,False)]
if os.path.exists(fn)]
if not fns:
die(1,f'No suitable dump file found for {fn!r}')
elif len(fns) > 1:
ymsg(f'Warning: more than one dump file found for {fn!r} - using the first!')
return MoneroWalletDumpFile.Completed(
parent = self,
fn = fns[0] ).data._asdict()['wallet_metadata']
def restore_accounts():
bmsg(' Restoring accounts:')
for acct_idx,acct_data in enumerate(data[1:],1):
msg(fs.format(acct_idx, 0, acct_data['address']))
self.c.call('create_account')
def restore_subaddresses():
bmsg(' Restoring subaddresses:')
for acct_idx,acct_data in enumerate(data):
for addr_idx,addr_data in enumerate(acct_data['addresses'][1:],1):
msg(fs.format(acct_idx, addr_idx, addr_data['address']))
ret = self.c.call( 'create_address', account_index=acct_idx )
def restore_labels():
bmsg(' Restoring labels:')
for acct_idx,acct_data in enumerate(data):
for addr_idx,addr_data in enumerate(acct_data['addresses']):
addr_data['used'] = False # do this so that restored data matches
msg(fs.format(acct_idx, addr_idx, addr_data['label']))
self.c.call(
'label_address',
index = { 'major': acct_idx, 'minor': addr_idx },
label = addr_data['label'],
)
def make_format_str():
return ' acct {:O>%s}, addr {:O>%s} [{}]' % (
len(str( len(data) - 1 )),
len(str( max(len(acct_data['addresses']) for acct_data in data) - 1))
)
def check_restored_data():
restored_data = h.get_accts(print=False)[1]
if restored_data != data:
rmsg(f'Restored data does not match original dump! Dumping bad data.')
MoneroWalletDumpFile.New(
parent = self,
wallet_fn = fn,
data = {'wallet_metadata': restored_data} ).write(add_suf='.bad')
die(3,'Fatal error')
res = await super().process_wallet(d,fn,last)
h = self.rpc(self,d)
h.open_wallet('newly created')
msg('')
data = get_dump_data()
fs = make_format_str()
gmsg('\nRestoring accounts, subaddresses and labels from dump file:\n')
restore_accounts()
restore_subaddresses()
restore_labels()
check_restored_data()
return True
class sync(wallet):
opts = ('rescan_blockchain',)
def __init__(self,cfg,uarg_tuple):
super().__init__(cfg,uarg_tuple)
self.dc = self.get_coin_daemon_rpc()
@ -947,7 +1376,11 @@ class MoneroWalletOps:
class sweep(spec):
spec_id = 'sweep_spec'
spec_key = ( (1,'source'), (3,'dest') )
opts = ('no_relay','tx_relay_daemon')
opts = ('no_relay','tx_relay_daemon','watch_only')
def check_uopts(self):
if self.cfg.tx_relay_daemon and (self.cfg.no_relay or self.cfg.autosign):
die(1,'--tx-relay-daemon makes no sense in this context!')
def init_tx_relay_daemon(self):
@ -1037,7 +1470,7 @@ class MoneroWalletOps:
msg('Saving TX data to file')
new_tx.write(delete_metadata=True)
if self.cfg.no_relay:
if self.cfg.no_relay or self.cfg.autosign:
return True
if keypress_confirm( self.cfg, f'Relay {self.name} transaction?' ):
@ -1142,11 +1575,183 @@ class MoneroWalletOps:
else:
ymsg('\nOperation cancelled by user request')
class sign(wallet):
wallet_desc = 'offline signing'
action = 'signing transaction with'
start_daemon = False
offline = True
async def main(self,fn):
await self.c.restart_daemon()
tx = MoneroMMGenTX.Unsigned( self.cfg, fn )
h = self.rpc(self,self.addr_data[0])
self.head_msg(tx.src_wallet_idx,h.fn)
h.open_wallet('offline signing')
res = self.c.call(
'sign_transfer',
unsigned_txset = tx.data.unsigned_txset,
export_raw = True,
get_tx_keys = True
)
new_tx = MoneroMMGenTX.NewColdSigned(
cfg = self.cfg,
txid = res['tx_hash_list'][0],
unsigned_txset = None,
signed_txset = res['signed_txset'],
_in_tx = tx,
)
await self.stop_wallet_daemon()
return new_tx
class submit(wallet):
wallet_desc = 'watch-only'
action = 'submitting transaction with'
opts = ('tx_relay_daemon',)
def check_uopts(self):
if self.cfg.daemon:
die(1,f'--daemon is not supported for the ‘{self.name}’ operation. Use --tx-relay-daemon instead')
def get_unsubmitted_tx_fn(self):
from .autosign import Signable
t = Signable.xmr_transaction( get_autosign_obj(self.cfg) )
if len(t.unsubmitted) != 1:
die('AutosignTXError', '{a} unsubmitted transaction{b} in {c!r}!'.format(
a = 'More than one' if t.unsubmitted else 'No',
b = suf(t.unsubmitted),
c = t.parent.xmr_tx_dir,
))
return t.unsubmitted[0].path
async def main(self):
tx = MoneroMMGenTX.ColdSigned(
cfg = self.cfg,
fn = uarg.infile or self.get_unsubmitted_tx_fn() )
h = self.rpc( self, self.kal.entry(tx.src_wallet_idx) )
self.head_msg(tx.src_wallet_idx,h.fn)
h.open_wallet(self.wallet_desc)
msg('\n' + tx.get_info())
if self.cfg.tx_relay_daemon:
self.display_tx_relay_info()
if keypress_confirm( self.cfg, 'Submit transaction?' ):
res = self.c.call(
'submit_transfer',
tx_data_hex = tx.data.signed_txset )
assert res['tx_hash_list'][0] == tx.data.txid, 'TxID mismatch in ‘submit_transfer’ result!'
else:
die(1,'Exiting at user request')
new_tx = MoneroMMGenTX.NewSubmitted(
cfg = self.cfg,
complete = True,
_in_tx = tx,
)
gmsg('\nOK')
new_tx.write(
ask_write = not self.cfg.autosign,
ask_overwrite = not self.cfg.autosign )
return new_tx
class dump(wallet):
wallet_desc = 'source'
async def process_wallet(self,d,fn,last):
h = self.rpc(self,d)
h.open_wallet(self.wallet_desc)
acct_data,addr_data = h.get_accts(print=False)
msg('')
MoneroWalletDumpFile.New(
parent = self,
wallet_fn = fn,
data = {'wallet_metadata': addr_data} ).write()
return True
class export_outputs(wallet):
wallet_desc = 'watch-only'
action = 'exporting outputs from'
stem = 'process'
opts = ('export_all',)
async def process_wallet(self,d,fn,last):
h = self.rpc(self,d)
h.open_wallet('source')
self.head_msg(d.idx,h.fn)
for ftype in ('Unsigned','Signed'):
old_fn = getattr(MoneroWalletOutputsFile,ftype).find_fn_from_wallet_fn(
cfg = self.cfg,
wallet_fn = fn,
ret_on_no_match = True )
if old_fn:
os.unlink(old_fn)
m = MoneroWalletOutputsFile.New(
parent = self,
wallet_fn = fn,
data = self.c.call('export_outputs', all=self.cfg.export_all ),
)
m.write()
return True
class export_key_images(wallet):
wallet_desc = 'offline signing'
action = 'signing wallet outputs file with'
start_daemon = False
offline = True
async def main(self,f,wallet_idx):
await self.c.restart_daemon()
h = self.rpc(self,self.addr_data[0])
self.head_msg(wallet_idx,f.name)
h.open_wallet('offline signing')
m = MoneroWalletOutputsFile.Unsigned(
parent = self,
fn = f.path )
res = self.c.call(
'import_outputs',
outputs_data_hex = m.data.outputs_data_hex )
idata = res['num_imported']
bmsg('\n {} output{} imported'.format( idata, suf(idata) ))
data = m.data._asdict()
data.update(self.c.call('export_key_images')) # for testing: all = True
m = MoneroWalletOutputsFile.SignedNew(
parent = self,
wallet_fn = m.get_wallet_fn(f.name),
data = data )
idata = m.data.signed_key_images or []
bmsg(' {} key image{} signed'.format( len(idata), suf(idata) ))
await self.stop_wallet_daemon()
return m
class import_key_images(wallet):
wallet_desc = 'watch-only'
action = 'importing key images into'
stem = 'process'
trust_daemon = True
async def process_wallet(self,d,fn,last):
h = self.rpc(self,d)
h.open_wallet(self.wallet_desc)
self.head_msg(d.idx,h.fn)
m = MoneroWalletOutputsFile.Signed(
parent = self,
fn = MoneroWalletOutputsFile.Signed.find_fn_from_wallet_fn( self.cfg, fn ),
)
data = m.data.signed_key_images or []
bmsg('\n {} signed key image{} to import'.format( len(data), suf(data) ))
if data:
res = self.c.call( 'import_key_images', signed_key_images=data )
bmsg(f' Success: {res}')
return True
class relay(base):
opts = ('tx_relay_daemon',)
def __init__(self,cfg,uarg_tuple):
check_uopts = MoneroWalletOps.submit.check_uopts
super().__init__(cfg,uarg_tuple)
self.tx = MoneroMMGenTX.Signed( self.cfg, uarg.infile )
@ -1202,5 +1807,5 @@ class MoneroWalletOps:
tx.get_info() for tx in
sorted(
(MoneroMMGenTX.Completed( self.cfg, fn ) for fn in uarg.infile),
key = lambda x: x.data.sign_time )
key = lambda x: x.data.sign_time or x.data.create_time )
))

View file

@ -125,9 +125,10 @@ init_tests() {
[ \( "$ARM32" -o "$ARM64" \) -a "$DISTRO" != 'archarm' ] && t_altgen_skip+=' e'
d_xmr="Monero xmrwallet operations using stagenet"
d_xmr="Monero xmrwallet operations"
t_xmr="
- $test_py --coin=xmr
- $test_py --coin=xmr --exclude=xmr_autosign
- $test_py --coin=xmr xmr_autosign
"
d_eth="operations for Ethereum and Ethereum Classic using devnet"

View file

@ -35,6 +35,7 @@ cmd_groups_dfl = {
# 'chainsplit': ('TestSuiteChainsplit',{}),
'ethdev': ('TestSuiteEthdev',{}),
'xmrwallet': ('TestSuiteXMRWallet',{}),
'xmr_autosign': ('TestSuiteXMRAutosign',{}),
}
cmd_groups_extra = {
@ -212,6 +213,7 @@ cfgs = { # addr_idx_lists (except 31,32,33,34) must contain exactly 8 addresses
'32': {},
'33': {},
'34': {},
'39': {},
'40': {},
'41': {},
'99': {}, # dummy

View file

@ -133,6 +133,8 @@ class TestSuiteAutosignBase(TestSuiteBase):
def gen_msg_fns():
fmap = dict(filedir_map)
for coin in self.coins:
if coin == 'xmr':
continue
sdir = os.path.join('test','ref',fmap[coin])
for fn in os.listdir(sdir):
if fn.endswith(f'[{coin.upper()}].rawmsg.json'):

281
test/test_py_d/ts_xmr_autosign.py Executable file
View file

@ -0,0 +1,281 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
# Copyright (C)2013-2023 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
# https://gitlab.com/mmgen/mmgen
"""
test.test_py_d.ts_xmr_autosign: xmr autosigning tests for the test.py test suite
"""
from .ts_xmrwallet import *
from .ts_autosign import TestSuiteAutosignBase
def make_burn_addr():
from mmgen.tool.coin import tool_cmd
return tool_cmd(
cfg = cfg,
cmdname = 'privhex2addr',
proto = cfg._proto,
mmtype = 'monero' ).privhex2addr('beadcafe'*8)
class TestSuiteXMRAutosign(TestSuiteXMRWallet,TestSuiteAutosignBase):
"""
Monero autosigning operations
"""
tmpdir_nums = [39]
# ts_xmrwallet attrs:
user_data = (
('miner', '98831F3A', False, 130, '1', []),
('alice', 'FE3C6545', True, 150, '1-2', []),
)
# ts_autosign attrs:
coins = ['xmr']
daemon_coins = []
txfile_coins = []
live = False
simulate = False
bad_tx_count = 0
tx_relay_user = 'miner'
cmd_group = (
('daemon_version', 'checking daemon version'),
('create_tmp_wallets', 'creating temporary online wallets for Alice'),
('new_account_alice', 'adding an account to Alice’s tmp wallet'),
('new_address_alice', 'adding an address to Alice’s tmp wallet'),
('new_address_alice_label', 'adding an address to Alice’s tmp wallet (with label)'),
('dump_tmp_wallets', 'dumping Alice’s tmp wallets'),
('delete_tmp_wallets', 'deleting Alice’s tmp wallets'),
('autosign_setup', 'autosign setup with Alice’s seed'),
('create_watchonly_wallets', 'creating online (watch-only) wallets for Alice'),
('delete_tmp_dump_files', 'deleting Alice’s dump files'),
('gen_kafiles', 'generating key-address files for Miner'),
('create_wallets_miner', 'creating Monero wallets for Miner'),
('mine_initial_coins', 'mining initial coins'),
('fund_alice', 'sending funds to Alice'),
('create_transfer_tx1', 'creating a transfer TX'),
('sign_transfer_tx1', 'signing the transfer TX'),
('submit_transfer_tx1', 'submitting the transfer TX'),
('create_transfer_tx2', 'creating a transfer TX (for relaying via proxy)'),
('sign_transfer_tx2', 'signing the transfer TX (for relaying via proxy)'),
('submit_transfer_tx2', 'submitting the transfer TX (relaying via proxy)'),
('list_wallets', 'listing Alice’s wallets and checking balance'),
('dump_wallets', 'dumping Alice’s wallets'),
('delete_wallets', 'deleting Alice’s wallets'),
('restore_wallets', 'creating online (watch-only) wallets for Alice'),
('delete_dump_files', 'deleting Alice’s dump files'),
('export_outputs', 'exporting outputs from Alice’s watch-only wallets'),
('export_key_images', 'exporting signed key images from Alice’s offline wallets'),
('import_key_images', 'importing signed key images into Alice’s online wallets'),
('list_wallets', 'listing Alice’s wallets and checking balance'),
)
def __init__(self,trunner,cfgs,spawn):
TestSuiteXMRWallet.__init__(self,trunner,cfgs,spawn)
TestSuiteAutosignBase.__init__(self,trunner,cfgs,spawn)
if trunner == None:
return
from mmgen.cfg import Config
self.cfg = Config({
'coin': 'XMR',
'outdir': self.users['alice'].udir,
'wallet_dir': self.users['alice'].udir,
'wallet_rpc_password': 'passwOrd',
})
self.burn_addr = make_burn_addr()
self.opts.append('--xmrwallets={}'.format( self.users['alice'].kal_range )) # mmgen-autosign opts
self.autosign_opts = [f'--autosign-mountpoint={self.mountpoint}'] # mmgen-xmrwallet opts
self.tx_count = 1
def create_tmp_wallets(self):
self.spawn('',msg_only=True)
data = self.users['alice']
from mmgen.wallet import Wallet
from mmgen.xmrwallet import MoneroWalletOps,xmrwallet_uargs
silence()
kal = KeyAddrList(
cfg = self.cfg,
proto = self.proto,
addr_idxs = '1-2',
seed = Wallet(cfg,data.mmwords).seed )
kal.file.write(ask_overwrite=False)
fn = get_file_with_ext(data.udir,'akeys')
m = MoneroWalletOps.create(
self.cfg,
xmrwallet_uargs(fn, '1-2', None))
async_run(m.main())
async_run(m.stop_wallet_daemon())
end_silence()
return 'ok'
def _new_addr_alice(self,*args):
data = self.users['alice']
return self.new_addr_alice(
*args,
kafile = get_file_with_ext(data.udir,'akeys') )
def new_account_alice(self):
return self._new_addr_alice(
'2',
'start',
fr'Creating new account.*Index:\s+{self.na_idx}\s')
def new_address_alice(self):
return self._new_addr_alice(
'2:1',
'continue',
fr'Account index:\s+1\s+Creating new address' )
def new_address_alice_label(self):
return self._new_addr_alice(
'2:1,Alice’s new address',
'stop',
fr'Account index:\s+1\s+Creating new address.*Alice’s new address' )
def dump_tmp_wallets(self):
return self._dump_wallets(autosign=False)
def dump_wallets(self):
return self._dump_wallets(autosign=True)
def _dump_wallets(self,autosign):
data = self.users['alice']
t = self.spawn(
'mmgen-xmrwallet',
self.extra_opts
+ [f'--wallet-dir={data.udir}', f'--daemon=localhost:{data.md.rpc_port}']
+ (self.autosign_opts if autosign else [])
+ ['dump']
+ ([] if autosign else [get_file_with_ext(data.udir,'akeys')]) )
t.expect('2 wallets dumped')
return t
def _delete_files(self,*ext_list):
data = self.users['alice']
self.spawn('',msg_only=True)
for ext in ext_list:
get_file_with_ext(data.udir,ext,no_dot=True,delete_all=True)
return 'ok'
def delete_tmp_wallets(self):
return self._delete_files( 'MoneroWallet', 'MoneroWallet.keys', '.akeys' )
def delete_wallets(self):
return self._delete_files( 'MoneroWatchOnlyWallet', '.keys', '.address.txt' )
def delete_tmp_dump_files(self):
return self._delete_files( '.dump' )
def delete_dump_files(self):
return self._delete_files( '.dump' )
def autosign_setup(self):
from pathlib import Path
Path(self.autosign_xmr_dir).mkdir(parents=True,exist_ok=True)
Path(self.autosign_xmr_dir,'old.vkeys').touch()
t = self.make_wallet(
mn_type = 'mmgen',
mn_file = self.users['alice'].mmwords,
use_dfl_wallet = None )
t.expect('Continue with Monero setup? (Y/n): ','y')
t.written_to_file('View keys')
return t
def create_watchonly_wallets(self):
return self.create_wallets( 'alice', op='restore' )
def restore_wallets(self):
return self.create_wallets( 'alice', op='restore' )
def list_wallets(self):
return self.sync_wallets(
'alice',
op = 'list',
bal_chk_func = lambda n,bal: (0.83 < bal < 0.8536) if n == 0 else True )
# 1.234567891234 - 0.124 - 0.257 = 0.853567891234 (minus fees)
def _create_transfer_tx(self,amt):
return self.do_op('transfer','alice',f'1:0:{self.burn_addr},{amt}',no_relay=True,do_ret=True)
def create_transfer_tx1(self):
return self._create_transfer_tx('0.124')
def create_transfer_tx2(self):
get_file_with_ext(self.asi.xmr_tx_dir,'rawtx',delete_all=True)
get_file_with_ext(self.asi.xmr_tx_dir,'sigtx',delete_all=True)
return self._create_transfer_tx('0.257')
def _sign_transfer_tx(self):
return self.do_sign(['--full-summary'],tx_name='Monero transaction')
def sign_transfer_tx1(self):
return self._sign_transfer_tx()
def sign_transfer_tx2(self):
return self._sign_transfer_tx()
def _xmr_autosign_op(self,op,desc,dtype=None,ext=None,wallet_arg=None,add_opts=[]):
data = self.users['alice']
args = (
self.extra_opts
+ self.autosign_opts
+ [f'--wallet-dir={data.udir}']
+ ([f'--daemon=localhost:{data.md.rpc_port}'] if not op == 'submit' else [])
+ add_opts
+ [ op ]
+ ([get_file_with_ext(self.asi.xmr_tx_dir,ext)] if ext else [])
+ ([wallet_arg] if wallet_arg else [])
)
t = self.spawn( 'mmgen-xmrwallet', args, extra_desc=f'({desc}, Alice)' )
if dtype:
t.written_to_file(dtype.capitalize())
return t
def submit_transfer_tx1(self):
return self._submit_transfer_tx( self.tx_relay_daemon_parm, ext='sigtx' )
def submit_transfer_tx2(self):
return self._submit_transfer_tx( self.tx_relay_daemon_proxy_parm, ext=None )
def _submit_transfer_tx(self,relay_parm,ext):
t = self._xmr_autosign_op(
op = 'submit',
desc = 'submitting TX',
add_opts = [f'--tx-relay-daemon={relay_parm}'],
ext = ext )
t.expect( 'Submit transaction? (y/N): ', 'y' )
t.written_to_file('Submitted transaction')
t.ok()
return self.mine_chk(
'alice', 1, 0,
lambda x: 0 < x < 1.234567891234,
'unlocked balance 0 < 1.234567891234' )
def export_outputs(self):
return self._xmr_autosign_op(
op = 'export-outputs',
desc = 'exporting outputs',
dtype = 'wallet outputs',
wallet_arg = '1-2' )
def export_key_images(self):
self.tx_count = 2
return self.do_sign(['--full-summary'],tx_name='Monero wallet outputs file')
def import_key_images(self):
return self._xmr_autosign_op(
op = 'import-key-images',
desc = 'importing key images' )

View file

@ -26,7 +26,7 @@ from subprocess import run,PIPE
from mmgen.cfg import gc
from mmgen.obj import MMGenRange
from mmgen.amt import XMRAmt
from mmgen.addrlist import KeyAddrList,AddrIdxList
from mmgen.addrlist import ViewKeyAddrList,KeyAddrList,AddrIdxList
from ..include.common import *
from .common import *
@ -44,10 +44,11 @@ class TestSuiteXMRWallet(TestSuiteBase):
color = True
socks_port = 49237
user_data = (
('miner', '98831F3A', 130, '1-2', []),
('bob', '1378FC64', 140, None, ['--restricted-rpc']),
('alice', 'FE3C6545', 150, '1-4', []),
('miner', '98831F3A', False, 130, '1-2', []),
('bob', '1378FC64', False, 140, None, ['--restricted-rpc']),
('alice', 'FE3C6545', False, 150, '1-4', []),
)
tx_relay_user = 'bob'
cmd_group = (
('daemon_version', 'checking daemon version'),
@ -91,15 +92,18 @@ class TestSuiteXMRWallet(TestSuiteBase):
self.proto = init_proto( cfg, 'XMR', network='mainnet' )
self.datadir_base = os.path.join('test','daemons','xmrtest')
self.extra_opts = ['--wallet-rpc-password=passw0rd']
self.autosign_mountpoint = os.path.join(self.tmpdir,'mmgen_autosign')
self.autosign_xmr_dir = os.path.join(self.tmpdir,'mmgen_autosign','xmr')
self.init_users()
self.init_daemon_args()
self.autosign_opts = []
for v in self.users.values():
run(['mkdir','-p',v.udir])
self.init_proxy()
self.tx_relay_daemon_parm = 'localhost:{}'.format( self.users['bob'].md.rpc_port )
self.tx_relay_daemon_parm = 'localhost:{}'.format( self.users[self.tx_relay_user].md.rpc_port )
self.tx_relay_daemon_proxy_parm = (
self.tx_relay_daemon_parm + f':127.0.0.1:{self.socks_port}' ) # must be IP, not 'localhost'
@ -205,6 +209,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
ud = namedtuple('user_data',[
'sid',
'mmwords',
'autosign',
'udir',
'datadir',
'kal_range',
@ -220,6 +225,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
# kal_range must be None, a single digit, or a single hyphenated range
for ( user,
sid,
autosign,
shift,
kal_range,
add_coind_args ) in self.user_data:
@ -260,15 +266,24 @@ class TestSuiteXMRWallet(TestSuiteBase):
daemon = wd,
test_connection = False,
)
if autosign:
kafile_suf = 'vkeys'
fn_stem = 'MoneroWatchOnlyWallet'
kafile_dir = self.autosign_xmr_dir
else:
kafile_suf = 'akeys'
fn_stem = 'MoneroWallet'
kafile_dir = udir
self.users[user] = ud(
sid = sid,
mmwords = f'test/ref/{sid}.mmwords',
autosign = autosign,
udir = udir,
datadir = datadir,
kal_range = kal_range,
kafile = f'{udir}/{sid}-XMR-M[{kal_range}].akeys',
walletfile_fs = f'{udir}/{sid}-{{}}-MoneroWallet',
addrfile_fs = f'{udir}/{sid}-{{}}-MoneroWallet.address.txt',
kafile = f'{kafile_dir}/{sid}-XMR-M[{kal_range}].{kafile_suf}',
walletfile_fs = f'{udir}/{sid}-{{}}-{fn_stem}',
addrfile_fs = f'{udir}/{sid}-{{}}-{fn_stem}.address.txt',
md = md,
md_rpc = md_rpc,
wd = wd,
@ -296,6 +311,8 @@ class TestSuiteXMRWallet(TestSuiteBase):
for user,data in self.users.items():
if not data.kal_range:
continue
if data.autosign:
continue
run(['mkdir','-p',data.udir])
run(f'rm -f {data.kafile}',shell=True)
t = self.spawn(
@ -312,7 +329,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
def create_wallets_miner(self): return self.create_wallets('miner')
def create_wallets_alice(self): return self.create_wallets('alice')
def create_wallets(self,user,wallet=None,add_opts=[]):
def create_wallets(self,user,wallet=None,add_opts=[],op='create'):
assert wallet is None or is_int(wallet), 'wallet arg'
data = self.users[user]
stem_glob = data.walletfile_fs.format(wallet or '*')
@ -325,9 +342,10 @@ class TestSuiteXMRWallet(TestSuiteBase):
'mmgen-xmrwallet',
[f'--wallet-dir={data.udir}']
+ self.extra_opts
+ (self.autosign_opts if data.autosign else [])
+ add_opts
+ ['create']
+ [data.kafile]
+ [op]
+ ([] if data.autosign else [data.kafile])
+ [wallet or data.kal_range]
)
for i in MMGenRange(wallet or data.kal_range).items:
@ -339,7 +357,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
)
return t
def new_addr_alice(self,spec,cfg,expect):
def new_addr_alice(self,spec,cfg,expect,kafile=None):
data = self.users['alice']
t = self.spawn(
'mmgen-xmrwallet',
@ -348,7 +366,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
[f'--daemon=localhost:{data.md.rpc_port}'] +
(['--no-start-wallet-daemon'] if cfg in ('continue','stop') else []) +
(['--no-stop-wallet-daemon'] if cfg in ('start','continue') else []) +
[ 'new', data.kafile, spec ] )
['new', (kafile or data.kafile), spec] )
res = strip_ansi_escapes(t.read()).replace('\r','')
m = re.search(expect,res,re.DOTALL)
assert m, f'no match found for {expect!r}'
@ -426,7 +444,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
def list_wallets_all(self):
return self.sync_wallets('alice',op='list')
def sync_wallets(self,user,op='sync',wallets=None,add_opts=[]):
def sync_wallets(self,user,op='sync',wallets=None,add_opts=[],bal_chk_func=None):
data = self.users[user]
cmd_opts = list_gen(
[f'--wallet-dir={data.udir}'],
@ -436,9 +454,10 @@ class TestSuiteXMRWallet(TestSuiteBase):
'mmgen-xmrwallet',
self.extra_opts
+ cmd_opts
+ self.autosign_opts
+ add_opts
+ [op]
+ [data.kafile]
+ ([] if data.autosign else [data.kafile])
+ ([wallets] if wallets else [])
)
wlist = AddrIdxList(wallets) if wallets else MMGenRange(data.kal_range).items
@ -450,7 +469,10 @@ class TestSuiteXMRWallet(TestSuiteBase):
))
t.expect('Chain height: ')
t.expect('Wallet height: ')
t.expect('Balance: ')
res = t.expect_getend('Unlocked balance: ')
if bal_chk_func:
bal = XMRAmt(strip_ansi_escapes(res))
assert bal_chk_func(n,bal), f'balance check for wallet {n} failed!'
return t
def do_op(self, op, user, arg2,
@ -464,10 +486,11 @@ class TestSuiteXMRWallet(TestSuiteBase):
data = self.users[user]
cmd_opts = list_gen(
[f'--wallet-dir={data.udir}'],
[f'--outdir={data.udir}'],
[f'--outdir={data.udir}', not data.autosign],
[f'--daemon=localhost:{data.md.rpc_port}'],
[f'--tx-relay-daemon={tx_relay_parm}', tx_relay_parm],
['--no-relay', no_relay]
['--no-relay', no_relay and not data.autosign],
[f'--autosign-mountpoint={self.autosign_mountpoint}', data.autosign],
)
add_desc = (', ' + add_desc) if add_desc else ''
@ -476,10 +499,13 @@ class TestSuiteXMRWallet(TestSuiteBase):
self.extra_opts
+ cmd_opts
+ [op]
+ [data.kafile]
+ ([] if data.autosign else [data.kafile])
+ [arg2],
extra_desc = f'({capfirst(user)}{add_desc})' )
if op == 'sign':
return t
if op == 'sweep':
t.expect(
r'Create new {} .* \(y/N\): '.format(('address','account')[',' in arg2]),
@ -491,7 +517,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
if return_amt:
amt = XMRAmt(strip_ansi_escapes(t.expect_getend('Amount: ')).replace('XMR','').strip())
dtype = 'signed'
dtype = 'unsigned' if data.autosign else 'signed'
t.expect(f'Save {dtype} transaction? (y/N): ','y')
t.written_to_file(f'{dtype.capitalize()} transaction')
@ -597,7 +623,7 @@ class TestSuiteXMRWallet(TestSuiteBase):
async def open_wallet_user(self,user,wnum):
data = self.users[user]
silence()
kal = KeyAddrList(
kal = (ViewKeyAddrList if data.autosign else KeyAddrList)(
cfg = cfg,
proto = self.proto,
addrfile = data.kafile,