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:
parent
3988aa8c59
commit
de77f9c27d
14 changed files with 1437 additions and 96 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
13.3.dev47
|
||||
13.3.dev48
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
It’s 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, you’re
|
||||
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, you’re 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, you’ll 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 seed’s 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
|
||||
they’re 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
|
||||
you’ve 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 it’s 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 you’re ready to begin transacting. Let’s 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, you’ve 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 wallet’s 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, it’s 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 you’ve 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, you’ll 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 you’ll 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 you’ve 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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 Don’t start the wallet daemon at startup
|
||||
-S, --no-stop-wallet-daemon Don’t 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()
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
281
test/test_py_d/ts_xmr_autosign.py
Executable 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' )
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue