XMR compat: basic transaction support
- This functionality is work-in-progress: sweep transactions, spending to
MMGen IDs and certain options are not supported yet.
- The `--autosign` option is required. It may be set in the config file.
- On the offline signing machine, `--xmrwallet-compat` is required, or it
may be set in the config file. On the online machine, the option is
required to permit interoperability with `mmgen-xmrwallet`.
- Note that transactions created with `mmgen-txcreate` must be sent with
`mmgen-txsend`, not `mmgen-xmrwallet submit`, which looks for signed
transactions in a different directory on the removable device.
- Signed or unsigned transactions may be aborted with `mmgen-txsend --abort`.
- Compat mode uses `mmgen-xmrwallet` as its backend, and that command is still
required for all operations beyond simple create-sign-send to address. For
details, invoke `mmgen-xmrwallet --help`.
Sample workflow:
$ mmgen-txcreate --coin=xmr XMR_ADDRESS,AMOUNT
(remove - insert - wait for signing - remove - insert)
$ mmgen-txsend
Testing/demo:
$ test/cmdtest.py --coin=xmr -e xmr_compat
This commit is contained in:
parent
6ef5f6c797
commit
cb99e13cd5
30 changed files with 366 additions and 58 deletions
|
|
@ -189,6 +189,11 @@ class Signable:
|
||||||
cfg = self.cfg,
|
cfg = self.cfg,
|
||||||
filename = f,
|
filename = f,
|
||||||
automount = self.automount)
|
automount = self.automount)
|
||||||
|
if tx1.proto.coin == 'XMR':
|
||||||
|
ctx = Signable.xmr_compat_transaction(self.parent)
|
||||||
|
for k in ('desc', 'print_summary', 'print_bad_list'):
|
||||||
|
setattr(self, k, getattr(ctx, k))
|
||||||
|
return await ctx.sign(f, compat_call=True)
|
||||||
if tx1.proto.sign_mode == 'daemon':
|
if tx1.proto.sign_mode == 'daemon':
|
||||||
from .rpc import rpc_init
|
from .rpc import rpc_init
|
||||||
tx1.rpc = await rpc_init(self.cfg, tx1.proto, ignore_wallet=True)
|
tx1.rpc = await rpc_init(self.cfg, tx1.proto, ignore_wallet=True)
|
||||||
|
|
@ -357,7 +362,7 @@ class Signable:
|
||||||
sigext = 'sigtx'
|
sigext = 'sigtx'
|
||||||
subext = 'subtx'
|
subext = 'subtx'
|
||||||
|
|
||||||
async def sign(self, f):
|
async def sign(self, f, compat_call=False):
|
||||||
from . import xmrwallet
|
from . import xmrwallet
|
||||||
from .xmrwallet.file.tx import MoneroMMGenTX
|
from .xmrwallet.file.tx import MoneroMMGenTX
|
||||||
tx1 = MoneroMMGenTX.Completed(self.parent.xmrwallet_cfg, f)
|
tx1 = MoneroMMGenTX.Completed(self.parent.xmrwallet_cfg, f)
|
||||||
|
|
@ -365,11 +370,19 @@ class Signable:
|
||||||
'sign',
|
'sign',
|
||||||
self.parent.xmrwallet_cfg,
|
self.parent.xmrwallet_cfg,
|
||||||
infile = str(self.parent.wallet_files[0]), # MMGen wallet file
|
infile = str(self.parent.wallet_files[0]), # MMGen wallet file
|
||||||
wallets = str(tx1.src_wallet_idx))
|
wallets = str(tx1.src_wallet_idx),
|
||||||
|
compat_call = compat_call)
|
||||||
tx2 = await m.main(f, restart_daemon=self.need_daemon_restart(m, tx1.src_wallet_idx))
|
tx2 = await m.main(f, restart_daemon=self.need_daemon_restart(m, tx1.src_wallet_idx))
|
||||||
tx2.write(ask_write=False)
|
tx2.write(ask_write=False)
|
||||||
return tx2
|
return tx2
|
||||||
|
|
||||||
|
class xmr_compat_transaction(xmr_transaction):
|
||||||
|
desc = 'Monero compat transaction'
|
||||||
|
dir_name = 'txauto_dir'
|
||||||
|
rawext = 'arawtx'
|
||||||
|
sigext = 'asigtx'
|
||||||
|
subext = 'asubtx'
|
||||||
|
|
||||||
class xmr_wallet_outputs_file(xmr_signable, base):
|
class xmr_wallet_outputs_file(xmr_signable, base):
|
||||||
desc = 'Monero wallet outputs file'
|
desc = 'Monero wallet outputs file'
|
||||||
dir_name = 'xmr_outputs_dir'
|
dir_name = 'xmr_outputs_dir'
|
||||||
|
|
@ -553,8 +566,10 @@ class Autosign:
|
||||||
self.signables += Signable.non_xmr_signables
|
self.signables += Signable.non_xmr_signables
|
||||||
|
|
||||||
if self.have_xmr:
|
if self.have_xmr:
|
||||||
self.dirs |= self.xmr_dirs
|
self.dirs |= self.xmr_dirs | (
|
||||||
self.signables += Signable.xmr_signables
|
{'txauto_dir': 'txauto'} if cfg.xmrwallet_compat and self.xmr_only else {})
|
||||||
|
self.signables += Signable.xmr_signables + (
|
||||||
|
('automount_transaction',) if cfg.xmrwallet_compat and self.xmr_only else ())
|
||||||
|
|
||||||
for name, path in self.dirs.items():
|
for name, path in self.dirs.items():
|
||||||
setattr(self, name, self.mountpoint / path)
|
setattr(self, name, self.mountpoint / path)
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,8 @@
|
||||||
# monero_wallet_rpc_password passw0rd
|
# monero_wallet_rpc_password passw0rd
|
||||||
|
|
||||||
# Configure mmgen-xmrwallet for compatibility with mmgen-tx{create,sign,send}
|
# Configure mmgen-xmrwallet for compatibility with mmgen-tx{create,sign,send}
|
||||||
# family of commands (equivalent to mmgen-xmrwallet --compat option)
|
# family of commands (equivalent to mmgen-xmrwallet --compat option). This
|
||||||
|
# option also enables signing of XMR compat transactions by `mmgen-autosign`.
|
||||||
# xmrwallet_compat true
|
# xmrwallet_compat true
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
November 2025
|
December 2025
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
16.1.dev19
|
16.1.dev20
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ class help_notes:
|
||||||
match self.proto.base_proto:
|
match self.proto.base_proto:
|
||||||
case 'Bitcoin':
|
case 'Bitcoin':
|
||||||
return '[ADDR,AMT ... | DATA_SPEC] ADDR [addr file ...]'
|
return '[ADDR,AMT ... | DATA_SPEC] ADDR [addr file ...]'
|
||||||
|
case 'Monero':
|
||||||
|
return 'ADDR,AMT'
|
||||||
case _:
|
case _:
|
||||||
return 'ADDR,AMT [addr file ...]'
|
return 'ADDR,AMT [addr file ...]'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,15 @@ EXAMPLES:
|
||||||
$ {gc.prog_name} {mmtype}
|
$ {gc.prog_name} {mmtype}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
case 'Monero':
|
||||||
|
return f"""
|
||||||
|
EXAMPLES:
|
||||||
|
|
||||||
|
Send 0.123 {proto.coin} to an external {proto.name} address:
|
||||||
|
|
||||||
|
$ {gc.prog_name} {sample_addr},0.123
|
||||||
|
"""
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
return f"""
|
return f"""
|
||||||
EXAMPLES:
|
EXAMPLES:
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ mmgen-txcreate: Create a cryptocoin transaction with MMGen- and/or non-MMGen
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .cfg import gc, Config
|
from .cfg import gc, Config
|
||||||
from .util import Msg, fmt_list, async_run
|
from .util import Msg, fmt_list, fmt_dict, async_run
|
||||||
|
from .xmrwallet import tx_priorities
|
||||||
|
|
||||||
target = gc.prog_name.split('-')[1].removesuffix('create')
|
target = gc.prog_name.split('-')[1].removesuffix('create')
|
||||||
|
|
||||||
|
|
@ -72,6 +73,10 @@ opts_data = {
|
||||||
b- -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
|
b- -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
|
||||||
-- -m, --minconf= n Minimum number of confirmations required to spend
|
-- -m, --minconf= n Minimum number of confirmations required to spend
|
||||||
+ outputs (default: 1)
|
+ outputs (default: 1)
|
||||||
|
m- -p, --priority=N Specify an integer priority ‘N’ for inclusion of trans-
|
||||||
|
+ action in blockchain (higher number means higher fee).
|
||||||
|
+ Valid parameters: {tp}.
|
||||||
|
+ If option is omitted, the default priority will be used
|
||||||
-- -q, --quiet Suppress warnings; overwrite files without prompting
|
-- -q, --quiet Suppress warnings; overwrite files without prompting
|
||||||
-s -r, --stream-interval=N Set block interval for streaming swap (default: {si})
|
-s -r, --stream-interval=N Set block interval for streaming swap (default: {si})
|
||||||
bt -R, --no-rbf Make transaction non-replaceable (non-replace-by-fee
|
bt -R, --no-rbf Make transaction non-replaceable (non-replace-by-fee
|
||||||
|
|
@ -101,6 +106,7 @@ opts_data = {
|
||||||
a_info = help_notes('account_info_desc'),
|
a_info = help_notes('account_info_desc'),
|
||||||
fu = help_notes('rel_fee_desc'),
|
fu = help_notes('rel_fee_desc'),
|
||||||
fl = help_notes('fee_spec_letters', use_quotes=True),
|
fl = help_notes('fee_spec_letters', use_quotes=True),
|
||||||
|
tp = fmt_dict(tx_priorities, fmt='equal_compact'),
|
||||||
si = help_notes('stream_interval'),
|
si = help_notes('stream_interval'),
|
||||||
fe_all = fmt_list(cfg._autoset_opts['fee_estimate_mode'].choices, fmt='no_spc'),
|
fe_all = fmt_list(cfg._autoset_opts['fee_estimate_mode'].choices, fmt='no_spc'),
|
||||||
fe_dfl = cfg._autoset_opts['fee_estimate_mode'].choices[0],
|
fe_dfl = cfg._autoset_opts['fee_estimate_mode'].choices[0],
|
||||||
|
|
@ -132,7 +138,7 @@ async def main():
|
||||||
if cfg.autosign:
|
if cfg.autosign:
|
||||||
from .tx.util import mount_removable_device
|
from .tx.util import mount_removable_device
|
||||||
from .autosign import Signable
|
from .autosign import Signable
|
||||||
asi = mount_removable_device(cfg)
|
asi = mount_removable_device(cfg, add_cfg={'xmrwallet_compat': True})
|
||||||
Signable.automount_transaction(asi).check_create_ok()
|
Signable.automount_transaction(asi).check_create_ok()
|
||||||
|
|
||||||
if target == 'swaptx':
|
if target == 'swaptx':
|
||||||
|
|
@ -149,10 +155,11 @@ async def main():
|
||||||
locktime = int(cfg.locktime or 0),
|
locktime = int(cfg.locktime or 0),
|
||||||
do_info = cfg.info)
|
do_info = cfg.info)
|
||||||
|
|
||||||
tx2.file.write(
|
if not tx1.is_compat:
|
||||||
outdir = asi.txauto_dir if cfg.autosign else None,
|
tx2.file.write(
|
||||||
ask_write = not cfg.yes,
|
outdir = asi.txauto_dir if cfg.autosign else None,
|
||||||
ask_overwrite = not cfg.yes,
|
ask_write = not cfg.yes,
|
||||||
ask_write_default_yes = False)
|
ask_overwrite = not cfg.yes,
|
||||||
|
ask_write_default_yes = False)
|
||||||
|
|
||||||
async_run(cfg, main)
|
async_run(cfg, main)
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,9 @@ async def main():
|
||||||
automount = cfg.autosign,
|
automount = cfg.autosign,
|
||||||
quiet_open = True)
|
quiet_open = True)
|
||||||
|
|
||||||
|
if tx.is_compat:
|
||||||
|
return await tx.compat_send()
|
||||||
|
|
||||||
cfg = Config({'_clone': cfg, 'proto': tx.proto, 'coin': tx.proto.coin})
|
cfg = Config({'_clone': cfg, 'proto': tx.proto, 'coin': tx.proto.coin})
|
||||||
|
|
||||||
if cfg.tx_proxy:
|
if cfg.tx_proxy:
|
||||||
|
|
|
||||||
|
|
@ -308,9 +308,10 @@ class UserOpts(Opts):
|
||||||
br --rpc-user=USER Authenticate to coin daemon using username USER
|
br --rpc-user=USER Authenticate to coin daemon using username USER
|
||||||
br --rpc-password=PASS Authenticate to coin daemon using password PASS
|
br --rpc-password=PASS Authenticate to coin daemon using password PASS
|
||||||
Rr --rpc-backend=backend Use backend 'backend' for JSON-RPC communications
|
Rr --rpc-backend=backend Use backend 'backend' for JSON-RPC communications
|
||||||
mr --monero-wallet-rpc-user=USER Monero wallet RPC username
|
-r --monero-wallet-rpc-user=USER Monero wallet RPC username
|
||||||
mr --monero-wallet-rpc-password=USER Monero wallet RPC password
|
-r --monero-wallet-rpc-password=USER Monero wallet RPC password
|
||||||
mr --monero-daemon=HOST:PORT Connect to the monerod at HOST:PORT
|
-r --monero-daemon=HOST:PORT Connect to the monerod at HOST:PORT
|
||||||
|
-r --xmrwallet-compat Enable XMR compatibility mode
|
||||||
Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
|
Rr --aiohttp-rpc-queue-len=N Use N simultaneous RPC connections with aiohttp
|
||||||
-p --regtest=0|1 Disable or enable regtest mode
|
-p --regtest=0|1 Disable or enable regtest mode
|
||||||
-- --testnet=0|1 Disable or enable testnet
|
-- --testnet=0|1 Disable or enable testnet
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,10 @@ class mainnet(CoinProtocol.RPC, CoinProtocol.DummyWIF, CoinProtocol.Base):
|
||||||
pubkey_type = 'monero' # required by DummyWIF
|
pubkey_type = 'monero' # required by DummyWIF
|
||||||
avg_bdi = 120
|
avg_bdi = 120
|
||||||
privkey_len = 32
|
privkey_len = 32
|
||||||
mmcaps = ('rpc',)
|
mmcaps = ('rpc', 'tw')
|
||||||
coin_amt = 'XMRAmt'
|
coin_amt = 'XMRAmt'
|
||||||
sign_mode = 'standalone'
|
sign_mode = 'standalone'
|
||||||
|
has_usr_fee = False
|
||||||
|
|
||||||
coin_cfg_opts = (
|
coin_cfg_opts = (
|
||||||
'ignore_daemon_version',
|
'ignore_daemon_version',
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from ....tw.unspent import TwUnspentOutputs
|
||||||
|
|
||||||
class MoneroTwView:
|
class MoneroTwView:
|
||||||
|
|
||||||
|
is_account_based = True
|
||||||
item_desc = 'account'
|
item_desc = 'account'
|
||||||
nice_addr_w = {'addr': 20}
|
nice_addr_w = {'addr': 20}
|
||||||
total = None
|
total = None
|
||||||
|
|
|
||||||
17
mmgen/proto/xmr/tx/base.py
Executable file
17
mmgen/proto/xmr/tx/base.py
Executable file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
||||||
|
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
|
||||||
|
# Licensed under the GNU General Public License, Version 3:
|
||||||
|
# https://www.gnu.org/licenses
|
||||||
|
# Public project repositories:
|
||||||
|
# https://github.com/mmgen/mmgen-wallet
|
||||||
|
# https://gitlab.com/mmgen/mmgen-wallet
|
||||||
|
|
||||||
|
"""
|
||||||
|
proto.xmr.tx.base: Monero base transaction class
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Base:
|
||||||
|
is_compat = True
|
||||||
|
has_comment = False
|
||||||
27
mmgen/proto/xmr/tx/completed.py
Executable file
27
mmgen/proto/xmr/tx/completed.py
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
||||||
|
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
|
||||||
|
# Licensed under the GNU General Public License, Version 3:
|
||||||
|
# https://www.gnu.org/licenses
|
||||||
|
# Public project repositories:
|
||||||
|
# https://github.com/mmgen/mmgen-wallet
|
||||||
|
# https://gitlab.com/mmgen/mmgen-wallet
|
||||||
|
|
||||||
|
"""
|
||||||
|
proto.xmr.tx.completed: Monero completed transaction class
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ....cfg import Config
|
||||||
|
|
||||||
|
from .base import Base
|
||||||
|
|
||||||
|
class Completed(Base):
|
||||||
|
|
||||||
|
def __init__(self, cfg, *args, proto, filename, **kwargs):
|
||||||
|
self.cfg = Config({
|
||||||
|
'_clone': cfg,
|
||||||
|
'coin': 'XMR',
|
||||||
|
'network': proto.network})
|
||||||
|
self.proto = proto
|
||||||
|
self.filename = filename
|
||||||
55
mmgen/proto/xmr/tx/new.py
Executable file
55
mmgen/proto/xmr/tx/new.py
Executable file
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
||||||
|
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
|
||||||
|
# Licensed under the GNU General Public License, Version 3:
|
||||||
|
# https://www.gnu.org/licenses
|
||||||
|
# Public project repositories:
|
||||||
|
# https://github.com/mmgen/mmgen-wallet
|
||||||
|
# https://gitlab.com/mmgen/mmgen-wallet
|
||||||
|
|
||||||
|
"""
|
||||||
|
proto.xmr.tx.new: Monero new transaction class
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ....tx.new import New as TxNew
|
||||||
|
|
||||||
|
from .base import Base
|
||||||
|
|
||||||
|
class New(Base, TxNew):
|
||||||
|
|
||||||
|
async def get_input_addrs_from_inputs_opt(self):
|
||||||
|
return [] # TODO
|
||||||
|
|
||||||
|
async def set_gas(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_fee(self, fee, outputs_sum, start_fee_desc):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def copy_inputs_from_tw(self, tw_unspent_data):
|
||||||
|
self.inputs = tw_unspent_data
|
||||||
|
|
||||||
|
def get_unspent_nums_from_user(self, accts_data):
|
||||||
|
from ....util import msg, is_int
|
||||||
|
from ....ui import line_input
|
||||||
|
prompt = 'Enter an account number to spend from: '
|
||||||
|
while True:
|
||||||
|
if reply := line_input(self.cfg, prompt).strip():
|
||||||
|
if is_int(reply) and 1 <= int(reply) <= len(accts_data):
|
||||||
|
return [int(reply)]
|
||||||
|
msg(f'Account number must be an integer between 1 and {len(accts_data)} inclusive')
|
||||||
|
|
||||||
|
async def compat_create(self):
|
||||||
|
from ....xmrwallet import op as xmrwallet_op
|
||||||
|
i = self.inputs[0]
|
||||||
|
o = self.outputs[0]
|
||||||
|
op = xmrwallet_op(
|
||||||
|
'transfer',
|
||||||
|
self.cfg,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
spec = f'{i.idx}:{i.acct_idx}:{o.addr},{o.amt}',
|
||||||
|
compat_call = True)
|
||||||
|
await op.restart_wallet_daemon()
|
||||||
|
return await op.main()
|
||||||
32
mmgen/proto/xmr/tx/online.py
Executable file
32
mmgen/proto/xmr/tx/online.py
Executable file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
||||||
|
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
|
||||||
|
# Licensed under the GNU General Public License, Version 3:
|
||||||
|
# https://www.gnu.org/licenses
|
||||||
|
# Public project repositories:
|
||||||
|
# https://github.com/mmgen/mmgen-wallet
|
||||||
|
# https://gitlab.com/mmgen/mmgen-wallet
|
||||||
|
|
||||||
|
"""
|
||||||
|
proto.xmr.tx.online: Monero online signed transaction class
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .completed import Completed
|
||||||
|
|
||||||
|
class OnlineSigned(Completed):
|
||||||
|
|
||||||
|
async def compat_send(self):
|
||||||
|
from ....xmrwallet import op as xmrwallet_op
|
||||||
|
op = xmrwallet_op('submit', self.cfg, self.filename, None, compat_call=True)
|
||||||
|
await op.restart_wallet_daemon()
|
||||||
|
return await op.main()
|
||||||
|
|
||||||
|
class Sent(OnlineSigned):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AutomountOnlineSigned(OnlineSigned):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AutomountSent(AutomountOnlineSigned):
|
||||||
|
pass
|
||||||
21
mmgen/proto/xmr/tx/unsigned.py
Executable file
21
mmgen/proto/xmr/tx/unsigned.py
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# MMGen Wallet, a terminal-based cryptocurrency wallet
|
||||||
|
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
|
||||||
|
# Licensed under the GNU General Public License, Version 3:
|
||||||
|
# https://www.gnu.org/licenses
|
||||||
|
# Public project repositories:
|
||||||
|
# https://github.com/mmgen/mmgen-wallet
|
||||||
|
# https://gitlab.com/mmgen/mmgen-wallet
|
||||||
|
|
||||||
|
"""
|
||||||
|
proto.xmr.tx.unsigned: Monero unsigned transaction class
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .completed import Completed
|
||||||
|
|
||||||
|
class Unsigned(Completed):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AutomountUnsigned(Unsigned):
|
||||||
|
pass
|
||||||
|
|
@ -80,6 +80,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
||||||
def do(method, data, cw, fs, color, fmt_method):
|
def do(method, data, cw, fs, color, fmt_method):
|
||||||
return [l.rstrip() for l in method(data, cw, fs, color, fmt_method)]
|
return [l.rstrip() for l in method(data, cw, fs, color, fmt_method)]
|
||||||
|
|
||||||
|
is_account_based = False
|
||||||
has_age = False
|
has_age = False
|
||||||
has_used = False
|
has_used = False
|
||||||
has_wallet = True
|
has_wallet = True
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,8 @@ class Base(MMGenObject):
|
||||||
signed = False
|
signed = False
|
||||||
is_bump = False
|
is_bump = False
|
||||||
is_swap = False
|
is_swap = False
|
||||||
|
is_compat = False
|
||||||
|
has_comment = True
|
||||||
swap_attrs = {
|
swap_attrs = {
|
||||||
'swap_proto': None,
|
'swap_proto': None,
|
||||||
'swap_quote_expiry': None,
|
'swap_quote_expiry': None,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ import os, json
|
||||||
from ..util import ymsg, make_chksum_6, die
|
from ..util import ymsg, make_chksum_6, die
|
||||||
from ..obj import MMGenObject, HexStr, MMGenTxID, CoinTxID, MMGenTxComment
|
from ..obj import MMGenObject, HexStr, MMGenTxID, CoinTxID, MMGenTxComment
|
||||||
|
|
||||||
|
def get_monero_proto(tx, data):
|
||||||
|
from ..protocol import init_proto
|
||||||
|
return init_proto(tx.cfg, 'XMR', network=data['MoneroMMGenTX']['data']['network'])
|
||||||
|
|
||||||
class txdata_json_encoder(json.JSONEncoder):
|
class txdata_json_encoder(json.JSONEncoder):
|
||||||
def default(self, o):
|
def default(self, o):
|
||||||
if type(o).__name__.endswith('Amt'):
|
if type(o).__name__.endswith('Amt'):
|
||||||
|
|
@ -90,6 +94,9 @@ class MMGenTxFile(MMGenObject):
|
||||||
tx = self.tx
|
tx = self.tx
|
||||||
tx.file_format = 'json'
|
tx.file_format = 'json'
|
||||||
outer_data = json.loads(data)
|
outer_data = json.loads(data)
|
||||||
|
if 'MoneroMMGenTX' in outer_data:
|
||||||
|
tx.proto = get_monero_proto(tx, outer_data)
|
||||||
|
return None
|
||||||
data = outer_data[self.data_label]
|
data = outer_data[self.data_label]
|
||||||
if outer_data['chksum'] != make_chksum_6(json_dumps(data)):
|
if outer_data['chksum'] != make_chksum_6(json_dumps(data)):
|
||||||
chk = make_chksum_6(json_dumps(data))
|
chk = make_chksum_6(json_dumps(data))
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,9 @@ class New(Base):
|
||||||
is_chg = not a.amt,
|
is_chg = not a.amt,
|
||||||
is_vault = a.is_vault)
|
is_vault = a.is_vault)
|
||||||
|
|
||||||
|
if self.is_compat:
|
||||||
|
return
|
||||||
|
|
||||||
if self.chg_idx is None:
|
if self.chg_idx is None:
|
||||||
die(2,
|
die(2,
|
||||||
fmt(self.msg_no_change_output.format(self.dcoin)).strip()
|
fmt(self.msg_no_change_output.format(self.dcoin)).strip()
|
||||||
|
|
@ -386,7 +389,7 @@ class New(Base):
|
||||||
|
|
||||||
async def get_inputs(self, outputs_sum):
|
async def get_inputs(self, outputs_sum):
|
||||||
|
|
||||||
data = self.twuo.data
|
data = self.twuo.accts_data if self.twuo.is_account_based else self.twuo.data
|
||||||
|
|
||||||
sel_nums = (
|
sel_nums = (
|
||||||
self.get_unspent_nums_from_inputs_opt if self.cfg.inputs else
|
self.get_unspent_nums_from_inputs_opt if self.cfg.inputs else
|
||||||
|
|
@ -399,10 +402,10 @@ class New(Base):
|
||||||
' '.join(str(n) for n in sel_nums)))
|
' '.join(str(n) for n in sel_nums)))
|
||||||
sel_unspent = MMGenList(data[i-1] for i in sel_nums)
|
sel_unspent = MMGenList(data[i-1] for i in sel_nums)
|
||||||
|
|
||||||
if not await self.precheck_sufficient_funds(
|
if not (self.is_compat or await self.precheck_sufficient_funds(
|
||||||
sum(s.amt for s in sel_unspent),
|
sum(s.amt for s in sel_unspent),
|
||||||
sel_unspent,
|
sel_unspent,
|
||||||
outputs_sum):
|
outputs_sum)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.copy_inputs_from_tw(sel_unspent) # makes self.inputs
|
self.copy_inputs_from_tw(sel_unspent) # makes self.inputs
|
||||||
|
|
@ -456,13 +459,16 @@ class New(Base):
|
||||||
cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args)
|
cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args)
|
||||||
if self.is_swap:
|
if self.is_swap:
|
||||||
cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args)
|
cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args)
|
||||||
from ..rpc import rpc_init
|
if self.is_compat:
|
||||||
self.rpc = await rpc_init(self.cfg, self.proto)
|
await self.process_cmdline_args(cmd_args, None, None)
|
||||||
from ..addrdata import TwAddrData
|
else:
|
||||||
await self.process_cmdline_args(
|
from ..rpc import rpc_init
|
||||||
cmd_args,
|
self.rpc = await rpc_init(self.cfg, self.proto)
|
||||||
self.get_addrdata_from_files(self.proto, addrfile_args),
|
from ..addrdata import TwAddrData
|
||||||
await TwAddrData(self.cfg, self.proto, twctl=self.twctl))
|
await self.process_cmdline_args(
|
||||||
|
cmd_args,
|
||||||
|
self.get_addrdata_from_files(self.proto, addrfile_args),
|
||||||
|
await TwAddrData(self.cfg, self.proto, twctl=self.twctl))
|
||||||
|
|
||||||
if not self.is_bump:
|
if not self.is_bump:
|
||||||
self.twuo = await TwUnspentOutputs(
|
self.twuo = await TwUnspentOutputs(
|
||||||
|
|
@ -510,13 +516,12 @@ class New(Base):
|
||||||
desc)) is not None:
|
desc)) is not None:
|
||||||
break
|
break
|
||||||
|
|
||||||
self.check_non_mmgen_inputs(caller=caller)
|
if not self.is_compat:
|
||||||
|
self.check_non_mmgen_inputs(caller=caller)
|
||||||
|
self.update_change_output(funds_left)
|
||||||
|
self.check_chg_addr_is_wallet_addr()
|
||||||
|
|
||||||
self.update_change_output(funds_left)
|
if self.has_comment and not self.cfg.yes:
|
||||||
|
|
||||||
self.check_chg_addr_is_wallet_addr()
|
|
||||||
|
|
||||||
if not self.cfg.yes:
|
|
||||||
self.add_comment() # edits an existing comment
|
self.add_comment() # edits an existing comment
|
||||||
|
|
||||||
if self.is_swap:
|
if self.is_swap:
|
||||||
|
|
@ -524,6 +529,9 @@ class New(Base):
|
||||||
if time.time() > self.swap_quote_refresh_time + self.swap_quote_refresh_timeout:
|
if time.time() > self.swap_quote_refresh_time + self.swap_quote_refresh_timeout:
|
||||||
await self.update_vault_output(self.vault_output.amt)
|
await self.update_vault_output(self.vault_output.amt)
|
||||||
|
|
||||||
|
if self.is_compat:
|
||||||
|
return await self.compat_create()
|
||||||
|
|
||||||
await self.create_serialized(locktime=locktime) # creates self.txid too
|
await self.create_serialized(locktime=locktime) # creates self.txid too
|
||||||
|
|
||||||
self.add_timestamp()
|
self.add_timestamp()
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
tx.util: transaction utilities
|
tx.util: transaction utilities
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_autosign_obj(cfg):
|
def get_autosign_obj(cfg, add_cfg={}):
|
||||||
from ..cfg import Config
|
from ..cfg import Config
|
||||||
from ..autosign import Autosign
|
from ..autosign import Autosign
|
||||||
return Autosign(
|
return Autosign(
|
||||||
|
|
@ -21,10 +21,10 @@ def get_autosign_obj(cfg):
|
||||||
'mountpoint': cfg.autosign_mountpoint,
|
'mountpoint': cfg.autosign_mountpoint,
|
||||||
'coins': cfg.coin,
|
'coins': cfg.coin,
|
||||||
# used only in online environment (xmrwallet, txcreate, txsend, txbump):
|
# used only in online environment (xmrwallet, txcreate, txsend, txbump):
|
||||||
'online': not cfg.offline}))
|
'online': not cfg.offline} | add_cfg))
|
||||||
|
|
||||||
def mount_removable_device(cfg):
|
def mount_removable_device(cfg, add_cfg={}):
|
||||||
asi = get_autosign_obj(cfg)
|
asi = get_autosign_obj(cfg, add_cfg=add_cfg)
|
||||||
if not asi.device_inserted:
|
if not asi.device_inserted:
|
||||||
from ..util import die
|
from ..util import die
|
||||||
die(1, 'Removable device not present!')
|
die(1, 'Removable device not present!')
|
||||||
|
|
|
||||||
|
|
@ -116,16 +116,17 @@ def op_cls(op_name):
|
||||||
|
|
||||||
def op(op, cfg, infile, wallets, *, spec=None, compat_call=False):
|
def op(op, cfg, infile, wallets, *, spec=None, compat_call=False):
|
||||||
if compat_call or (cfg.compat if cfg.compat is not None else cfg.xmrwallet_compat):
|
if compat_call or (cfg.compat if cfg.compat is not None else cfg.xmrwallet_compat):
|
||||||
if cfg.wallet_dir:
|
if cfg.wallet_dir and not cfg.offline:
|
||||||
die(1, '--wallet-dir can not be specified in xmrwallet compatibility mode')
|
die(1, '--wallet-dir cannot be specified in xmrwallet compatibility mode')
|
||||||
from ..tw.ctl import TwCtl
|
from ..tw.ctl import TwCtl
|
||||||
from ..cfg import Config
|
from ..cfg import Config
|
||||||
twctl_cls = cfg._proto.base_proto_subclass(TwCtl, 'tw.ctl')
|
twctl_cls = cfg._proto.base_proto_subclass(TwCtl, 'tw.ctl')
|
||||||
cfg = Config({
|
cfg = Config({
|
||||||
'_clone': cfg,
|
'_clone': cfg,
|
||||||
'compat': True,
|
'compat': True,
|
||||||
'no_start_wallet_daemon': cfg.no_start_wallet_daemon or compat_call,
|
'xmrwallet_compat': True} | ({} if cfg.offline else {
|
||||||
'daemon': cfg.daemon or cfg.monero_daemon,
|
'no_start_wallet_daemon': cfg.no_start_wallet_daemon or compat_call,
|
||||||
'watch_only': cfg.watch_only or cfg.autosign or bool(cfg.autosign_mountpoint),
|
'daemon': cfg.daemon or cfg.monero_daemon,
|
||||||
'wallet_dir': twctl_cls.get_tw_dir(cfg, cfg._proto)})
|
'watch_only': cfg.watch_only or cfg.autosign or bool(cfg.autosign_mountpoint),
|
||||||
|
'wallet_dir': twctl_cls.get_tw_dir(cfg, cfg._proto)}))
|
||||||
return op_cls(op)(cfg, uargs(infile, wallets, spec, compat_call))
|
return op_cls(op)(cfg, uargs(infile, wallets, spec, compat_call))
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ class MoneroMMGenTX:
|
||||||
d = self.ext)
|
d = self.ext)
|
||||||
|
|
||||||
if self.cfg.autosign:
|
if self.cfg.autosign:
|
||||||
fn = get_autosign_obj(self.cfg).xmr_tx_dir / fn
|
fn = getattr(get_autosign_obj(self.cfg), self.tx_dir) / fn
|
||||||
|
|
||||||
from ...fileutil import write_data_to_file
|
from ...fileutil import write_data_to_file
|
||||||
write_data_to_file(
|
write_data_to_file(
|
||||||
|
|
@ -183,6 +183,7 @@ class MoneroMMGenTX:
|
||||||
is_submitting = False
|
is_submitting = False
|
||||||
is_complete = False
|
is_complete = False
|
||||||
signed = False
|
signed = False
|
||||||
|
tx_dir = 'xmr_tx_dir'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
|
@ -245,6 +246,18 @@ class MoneroMMGenTX:
|
||||||
is_submitting = True
|
is_submitting = True
|
||||||
is_complete = True
|
is_complete = True
|
||||||
|
|
||||||
|
class NewUnsignedCompat(NewUnsigned):
|
||||||
|
tx_dir = 'txauto_dir'
|
||||||
|
ext = 'arawtx'
|
||||||
|
|
||||||
|
class NewColdSignedCompat(NewColdSigned):
|
||||||
|
tx_dir = 'txauto_dir'
|
||||||
|
ext = 'asigtx'
|
||||||
|
|
||||||
|
class NewSubmittedCompat(NewSubmitted):
|
||||||
|
tx_dir = 'txauto_dir'
|
||||||
|
ext = 'asubtx'
|
||||||
|
|
||||||
class Completed(Base):
|
class Completed(Base):
|
||||||
desc = 'transaction'
|
desc = 'transaction'
|
||||||
forbidden_fields = ()
|
forbidden_fields = ()
|
||||||
|
|
@ -333,3 +346,12 @@ class MoneroMMGenTX:
|
||||||
|
|
||||||
class View(Completed):
|
class View(Completed):
|
||||||
silent_load = True
|
silent_load = True
|
||||||
|
|
||||||
|
class UnsignedCompat(Unsigned):
|
||||||
|
ext = 'arawtx'
|
||||||
|
|
||||||
|
class ColdSignedCompat(ColdSigned):
|
||||||
|
ext = 'asigtx'
|
||||||
|
|
||||||
|
class SubmittedCompat(Submitted):
|
||||||
|
ext = 'asubtx'
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ class OpBase:
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.uargs = uarg_tuple
|
self.uargs = uarg_tuple
|
||||||
self.compat_call = self.uargs.compat_call
|
self.compat_call = self.uargs.compat_call
|
||||||
|
self.tx_dir = 'txauto_dir' if self.compat_call else 'xmr_tx_dir'
|
||||||
|
|
||||||
classes = tuple(gen_classes())
|
classes = tuple(gen_classes())
|
||||||
self.opts = tuple(set(opt for cls in classes for opt in xmrwallet.opts))
|
self.opts = tuple(set(opt for cls in classes for opt in xmrwallet.opts))
|
||||||
|
|
@ -102,6 +103,10 @@ class OpBase:
|
||||||
self.cfg.tx_relay_daemon,
|
self.cfg.tx_relay_daemon,
|
||||||
re.ASCII)
|
re.ASCII)
|
||||||
|
|
||||||
|
def get_tx_cls(self, clsname):
|
||||||
|
from ..file.tx import MoneroMMGenTX
|
||||||
|
return getattr(MoneroMMGenTX, clsname + ('Compat' if self.compat_call else ''))
|
||||||
|
|
||||||
def display_tx_relay_info(self, *, indent=''):
|
def display_tx_relay_info(self, *, indent=''):
|
||||||
m = self.parse_tx_relay_opt()
|
m = self.parse_tx_relay_opt()
|
||||||
msg(fmt(f"""
|
msg(fmt(f"""
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
xmrwallet.ops.sign: Monero wallet ops for the MMGen Suite
|
xmrwallet.ops.sign: Monero wallet ops for the MMGen Suite
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ..file.tx import MoneroMMGenTX
|
|
||||||
from ..rpc import MoneroWalletRPC
|
from ..rpc import MoneroWalletRPC
|
||||||
|
|
||||||
from .wallet import OpWallet
|
from .wallet import OpWallet
|
||||||
|
|
@ -24,7 +23,7 @@ class OpSign(OpWallet):
|
||||||
async def main(self, fn, *, restart_daemon=True):
|
async def main(self, fn, *, restart_daemon=True):
|
||||||
if restart_daemon:
|
if restart_daemon:
|
||||||
await self.restart_wallet_daemon()
|
await self.restart_wallet_daemon()
|
||||||
tx = MoneroMMGenTX.Unsigned(self.cfg, fn)
|
tx = self.get_tx_cls('Unsigned')(self.cfg, fn)
|
||||||
h = MoneroWalletRPC(self, self.addr_data[0])
|
h = MoneroWalletRPC(self, self.addr_data[0])
|
||||||
self.head_msg(tx.src_wallet_idx, h.fn)
|
self.head_msg(tx.src_wallet_idx, h.fn)
|
||||||
if restart_daemon:
|
if restart_daemon:
|
||||||
|
|
@ -34,7 +33,7 @@ class OpSign(OpWallet):
|
||||||
unsigned_txset = tx.data.unsigned_txset,
|
unsigned_txset = tx.data.unsigned_txset,
|
||||||
export_raw = True,
|
export_raw = True,
|
||||||
get_tx_keys = True)
|
get_tx_keys = True)
|
||||||
new_tx = MoneroMMGenTX.NewColdSigned(
|
new_tx = self.get_tx_cls('NewColdSigned')(
|
||||||
cfg = self.cfg,
|
cfg = self.cfg,
|
||||||
txid = res['tx_hash_list'][0],
|
txid = res['tx_hash_list'][0],
|
||||||
unsigned_txset = None,
|
unsigned_txset = None,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ from ...ui import keypress_confirm
|
||||||
from ...proto.xmr.daemon import MoneroWalletDaemon
|
from ...proto.xmr.daemon import MoneroWalletDaemon
|
||||||
from ...proto.xmr.rpc import MoneroWalletRPCClient
|
from ...proto.xmr.rpc import MoneroWalletRPCClient
|
||||||
|
|
||||||
from ..file.tx import MoneroMMGenTX
|
|
||||||
from ..rpc import MoneroWalletRPC
|
from ..rpc import MoneroWalletRPC
|
||||||
|
|
||||||
from . import OpBase
|
from . import OpBase
|
||||||
|
|
@ -45,7 +44,7 @@ class OpSubmit(OpWallet):
|
||||||
else:
|
else:
|
||||||
from ...autosign import Signable
|
from ...autosign import Signable
|
||||||
fn = Signable.xmr_transaction(self.asi).get_unsubmitted()
|
fn = Signable.xmr_transaction(self.asi).get_unsubmitted()
|
||||||
return MoneroMMGenTX.ColdSigned(cfg=self.cfg, fn=fn)
|
return self.get_tx_cls('ColdSigned')(cfg=self.cfg, fn=fn)
|
||||||
|
|
||||||
def get_relay_rpc(self):
|
def get_relay_rpc(self):
|
||||||
|
|
||||||
|
|
@ -100,7 +99,7 @@ class OpSubmit(OpWallet):
|
||||||
from ...util2 import format_elapsed_hr
|
from ...util2 import format_elapsed_hr
|
||||||
msg(f'success\nRelay time: {format_elapsed_hr(t_start, rel_now=False, show_secs=True)}')
|
msg(f'success\nRelay time: {format_elapsed_hr(t_start, rel_now=False, show_secs=True)}')
|
||||||
|
|
||||||
new_tx = MoneroMMGenTX.NewSubmitted(cfg=self.cfg, _in_tx=tx)
|
new_tx = self.get_tx_cls('NewSubmitted')(cfg=self.cfg, _in_tx=tx)
|
||||||
|
|
||||||
gmsg('\nOK')
|
gmsg('\nOK')
|
||||||
new_tx.write(
|
new_tx.write(
|
||||||
|
|
@ -118,7 +117,7 @@ class OpResubmit(OpSubmit):
|
||||||
def get_tx(self):
|
def get_tx(self):
|
||||||
from ...autosign import Signable
|
from ...autosign import Signable
|
||||||
fns = Signable.xmr_transaction(self.asi).get_submitted()
|
fns = Signable.xmr_transaction(self.asi).get_submitted()
|
||||||
cls = MoneroMMGenTX.Submitted
|
cls = self.get_tx_cls('Submitted')
|
||||||
return sorted((cls(self.cfg, Path(fn)) for fn in fns),
|
return sorted((cls(self.cfg, Path(fn)) for fn in fns),
|
||||||
key = lambda x: getattr(x.data, 'submit_time', None) or x.data.create_time)[-1]
|
key = lambda x: getattr(x.data, 'submit_time', None) or x.data.create_time)[-1]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class OpTxview(OpBase):
|
||||||
self.mount_removable_device()
|
self.mount_removable_device()
|
||||||
|
|
||||||
if self.cfg.autosign:
|
if self.cfg.autosign:
|
||||||
files = [f for f in self.asi.xmr_tx_dir.iterdir()
|
files = [f for f in getattr(self.asi, self.tx_dir).iterdir()
|
||||||
if f.name.endswith('.' + mtx.Submitted.ext)]
|
if f.name.endswith('.' + mtx.Submitted.ext)]
|
||||||
else:
|
else:
|
||||||
files = self.uargs.infile
|
files = self.uargs.infile
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ class MoneroWalletRPC:
|
||||||
self.d = d
|
self.d = d
|
||||||
self.fn = parent.get_wallet_fn(d)
|
self.fn = parent.get_wallet_fn(d)
|
||||||
self.new_tx_cls = (
|
self.new_tx_cls = (
|
||||||
|
mtx.NewUnsignedCompat if self.parent.compat_call else
|
||||||
mtx.NewUnsigned if self.cfg.watch_only else
|
mtx.NewUnsigned if self.cfg.watch_only else
|
||||||
mtx.NewSigned)
|
mtx.NewSigned)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ packages =
|
||||||
mmgen.proto.xchain
|
mmgen.proto.xchain
|
||||||
mmgen.proto.xmr
|
mmgen.proto.xmr
|
||||||
mmgen.proto.xmr.tw
|
mmgen.proto.xmr.tw
|
||||||
|
mmgen.proto.xmr.tx
|
||||||
mmgen.proto.zec
|
mmgen.proto.zec
|
||||||
mmgen.rpc
|
mmgen.rpc
|
||||||
mmgen.rpc.backends
|
mmgen.rpc.backends
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from mmgen.color import blue, cyan, brown
|
||||||
|
|
||||||
from ..include.common import (
|
from ..include.common import (
|
||||||
imsg,
|
imsg,
|
||||||
|
oqmsg,
|
||||||
silence,
|
silence,
|
||||||
end_silence,
|
end_silence,
|
||||||
strip_ansi_escapes,
|
strip_ansi_escapes,
|
||||||
|
|
@ -507,6 +508,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
|
||||||
Monero autosigning operations (compat mode)
|
Monero autosigning operations (compat mode)
|
||||||
"""
|
"""
|
||||||
menu_prompt = 'efresh balances:\b'
|
menu_prompt = 'efresh balances:\b'
|
||||||
|
extra_daemons = ['ltc']
|
||||||
|
|
||||||
cmd_group = (
|
cmd_group = (
|
||||||
('autosign_setup', 'autosign setup with Alice’s seed'),
|
('autosign_setup', 'autosign setup with Alice’s seed'),
|
||||||
|
|
@ -535,6 +537,17 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
|
||||||
('alice_twview2', 'viewing Alice’s tracking wallets (reload, sort options)'),
|
('alice_twview2', 'viewing Alice’s tracking wallets (reload, sort options)'),
|
||||||
('alice_twview3', 'viewing Alice’s tracking wallets (check balances)'),
|
('alice_twview3', 'viewing Alice’s tracking wallets (check balances)'),
|
||||||
('alice_listaddresses2', 'listing Alice’s addresses (sort options)'),
|
('alice_listaddresses2', 'listing Alice’s addresses (sort options)'),
|
||||||
|
('wait_loop_start_compat', 'starting autosign wait loop in XMR compat mode [--coins=xmr]'),
|
||||||
|
('alice_txcreate1', 'creating a transaction'),
|
||||||
|
('alice_txabort1', 'aborting the transaction'),
|
||||||
|
('alice_txcreate2', 'recreating the transaction'),
|
||||||
|
('wait_signed1', 'autosigning the transaction'),
|
||||||
|
('wait_loop_kill', 'stopping autosign wait loop'),
|
||||||
|
('alice_txabort2', 'aborting the raw and signed transactions'),
|
||||||
|
('alice_txcreate3', 'recreating the transaction'),
|
||||||
|
('wait_loop_start_ltc', 'starting autosign wait loop in XMR compat mode [--coins=ltc,xmr]'),
|
||||||
|
('alice_txsend1', 'sending the transaction'),
|
||||||
|
('wait_loop_kill', 'stopping autosign wait loop'),
|
||||||
('stop_daemons', 'stopping all wallet and coin daemons'),
|
('stop_daemons', 'stopping all wallet and coin daemons'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -546,11 +559,10 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
|
||||||
self.alice_dump_file = os.path.join(
|
self.alice_dump_file = os.path.join(
|
||||||
self.alice_tw_dir,
|
self.alice_tw_dir,
|
||||||
'{}-2-MoneroWatchOnlyWallet.dump'.format(self.users['alice'].sid))
|
'{}-2-MoneroWatchOnlyWallet.dump'.format(self.users['alice'].sid))
|
||||||
self.alice_opts = [
|
self.alice_daemon_opts = [
|
||||||
'--alice',
|
f'--monero-daemon=localhost:{self.users["alice"].md.rpc_port}',
|
||||||
'--coin=xmr',
|
'--monero-wallet-rpc-password=passwOrd']
|
||||||
'--monero-wallet-rpc-password=passwOrd',
|
self.alice_opts = ['--alice', '--coin=xmr'] + self.alice_daemon_opts
|
||||||
f'--monero-daemon=localhost:{self.users["alice"].md.rpc_port}']
|
|
||||||
|
|
||||||
def create_watchonly_wallets(self):
|
def create_watchonly_wallets(self):
|
||||||
return self._create_wallets()
|
return self._create_wallets()
|
||||||
|
|
@ -660,3 +672,61 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
|
||||||
assert s in text
|
assert s in text
|
||||||
self.remove_device_online()
|
self.remove_device_online()
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
def wait_loop_start_compat(self):
|
||||||
|
return self.wait_loop_start(opts=['--xmrwallet-compat', '--coins=xmr'])
|
||||||
|
|
||||||
|
def wait_loop_start_ltc(self):
|
||||||
|
return self.wait_loop_start(opts=['--xmrwallet-compat', '--coins=ltc,xmr'])
|
||||||
|
|
||||||
|
def alice_txcreate1(self):
|
||||||
|
return self._alice_txops('txcreate', [f'{self.burn_addr},0.012345'], acct_num=1)
|
||||||
|
|
||||||
|
alice_txcreate3 = alice_txcreate2 = alice_txcreate1
|
||||||
|
|
||||||
|
def alice_txabort1(self):
|
||||||
|
return self._alice_txops('txsend', opts=['--alice', '--abort'])
|
||||||
|
|
||||||
|
alice_txabort2 = alice_txabort1
|
||||||
|
|
||||||
|
def alice_txsend1(self):
|
||||||
|
return self._alice_txops(
|
||||||
|
'txsend',
|
||||||
|
opts = ['--alice', '--quiet'],
|
||||||
|
add_opts = self.alice_daemon_opts,
|
||||||
|
acct_num = 1,
|
||||||
|
wait_signed = True)
|
||||||
|
|
||||||
|
def wait_signed1(self):
|
||||||
|
self.spawn(msg_only=True)
|
||||||
|
oqmsg('')
|
||||||
|
self._wait_signed('transaction')
|
||||||
|
return 'silent'
|
||||||
|
|
||||||
|
def _alice_txops(
|
||||||
|
self,
|
||||||
|
op,
|
||||||
|
args = [],
|
||||||
|
*,
|
||||||
|
opts = [],
|
||||||
|
add_opts = [],
|
||||||
|
menu = '',
|
||||||
|
acct_num = None,
|
||||||
|
wait_signed = False,
|
||||||
|
signable_desc = 'transaction'):
|
||||||
|
if wait_signed:
|
||||||
|
self._wait_signed(signable_desc)
|
||||||
|
self.insert_device_online()
|
||||||
|
t = self.spawn(f'mmgen-{op}', (opts or self.alice_opts) + self.autosign_opts + add_opts + args)
|
||||||
|
if '--abort' in opts:
|
||||||
|
t.expect('(y/N): ', 'y')
|
||||||
|
elif op == 'txcreate':
|
||||||
|
for ch in menu + 'q':
|
||||||
|
t.expect(self.menu_prompt, ch)
|
||||||
|
t.expect('to spend from: ', f'{acct_num}\n')
|
||||||
|
t.expect('(y/N): ', 'y') # save?
|
||||||
|
elif op == 'txsend':
|
||||||
|
t.expect('(y/N): ', 'y') # view?
|
||||||
|
t.read() # required!
|
||||||
|
self.remove_device_online()
|
||||||
|
return t
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue