THORChain XMR swaps

See: ffe488ff, ff35b01c

Enabled for test suite only pending activation on THORChain mainnet.

Swapping to and from ETH and ERC20 tokens is currently untested.

Sample LTC-to-XMR swap workflow (assumes --autosign):

    # Create the registration transaction (pass 1) (amount is optional):
    $ mmgen-swaptxcreate --memoless LTC 5.4321 XMR

    # Autosign the registration transaction and send it:
    $ mmgen-txsend
    => TxID

    # Get the reference number from the THORChain network:
    $ mmgen-remote --coin=rune get_refnum TxID
    => REFNUM

    # Create the swap transaction (pass 2) (amount is optional):
    $ mmgen-swaptxcreate --refnum=REFNUM LTC 5.4321 XMR

    # Sign and send the swap transaction:
    $ mmgen-txsend

    # Monitor your XMR wallet for the funds:
    $ mmgen-tool --coin=xmr twview

Sample XMR-to-LTC swap workflow (assumes --autosign):

    # Create the registration transaction (pass 1) (amount is required):
    $ mmgen-swaptxcreate --memoless XMR 1.2345 LTC

    # Sign and send registration TX and get reference number as shown above

    # Create the swap transaction (pass 2) (amount is required):
    $ mmgen-swaptxcreate --refnum=REFNUM XMR 1.2345 LTC

    # Sign and send the swap transaction as shown above

    # Monitor your LTC wallet for the funds:
    $ mmgen-tool --coin=ltc twview

Testing/demo:

    $ test/cmdtest.py --coin=ltc -e xmrswap

For more information and examples, invoke `mmgen-swaptxcreate --help`

More complete documentation will be provided at a later date.
This commit is contained in:
The MMGen Project 2026-10-20 17:38:48 +00:00
commit ea9c2d296e
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
26 changed files with 556 additions and 19 deletions

View file

@ -1 +1 @@
16.3.0dev18
16.3.0dev19

View file

@ -45,6 +45,10 @@ forks, types ‘B’, ‘S’ and ‘C’ (see ADDRESS TYPES below) are searched
order for unused addresses. Note that sending to an unused address may be
undesirable for Ethereum, where address (i.e. account) reuse is the norm. In
that case, the user should specify a destination address on the command line.
Monero tracking wallets are likely to contain no unused addresses in the
desired destination account, or even in the entire wallet, in which case the
user must create a new address via ‘mmgen-tool --coin=xmr listaddresses
interactive=1’.
If the wallet contains eligible unused addresses with multiple Seed IDs, the
user will be presented with a list of the lowest-indexed addresses of
@ -52,8 +56,10 @@ preferred type for each Seed ID and prompted to choose from among them.
Change and destination addresses may also be specified manually with the
CHG_ADDR and DEST_ADDR arguments. These are given either as full MMGen IDs
or in the form [SEED_ID:]ADDRTYPE_CODE, with the following exception: for
Ethereum the CHG_ADDR argument is not supported. See EXAMPLES below and the
or in the form [SEED_ID:]ADDRTYPE_CODE, with the following exceptions: for
Ethereum and Monero the CHG_ADDR argument is not supported, and for Monero
the DEST_ADDR argument, if given, is replaced by an extended MMGen ID or a
specifier in the form WALLET_NUM:ACCT_IDX. See EXAMPLES below and the
‘mmgen-txcreate’ help screen for details.
While discouraged, sending change or swapping to non-wallet addresses is also

View file

@ -98,4 +98,46 @@ EXAMPLES:
Same as above, but proxy requests via the I2P router running on host gw1:
$ https_proxy=http://gw1:4444 {gc.prog_name} --proxy=env RUNE 1000 BTC
MEMOLESS SWAP EXAMPLES:
The following examples assume --autosign
Initiate a BTC-to-XMR swap with autoselected destination address. The
command produces a THORChain registration transaction:
$ {gc.prog_name} --memoless BTC 0.1234 XMR
Send the registration transaction:
$ mmgen-txsend
=> TxID
Get the reference number from the THORChain network:
$ mmgen-remote --coin=rune get_refnum TxID
Create a BTC-to-XMR swap transaction using the reference number:
$ {gc.prog_name} --refnum=REFNUM BTC 0.1234 XMR
Send the swap transaction:
$ mmgen-txsend
Monitor your XMR wallet for the funds:
$ mmgen-tool --coin=xmr twview
Initiate a BTC-to-XMR swap with destination argument in the form
WALLET_NUM:ACCT_IDX:
$ {gc.prog_name} --memoless BTC 0.1234 XMR 3:12
Initiate a BTC-to-XMR swap with destination argument being a specific
address denoted by an extended MMGen ID in the form SEED_ID:ADDR_TYPE:
WALLET_NUM/ACCT_IDX/ADDR_IDX:
$ {gc.prog_name} --memoless BTC 0.1234 XMR 4BA5D19B:M:3/12/7
"""

View file

@ -84,10 +84,14 @@ opts_data = {
+ the reference request TX (activates second pass).
-s -N, --no-refreq-tx With --memoless, do not create a reference request TX.
+ Instead, display the reference memo and exit.
m- -p, --priority=N Specify an integer priority ‘N’ for inclusion of trans-
mt -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
-s -p, --priority=N Specify an integer priority ‘N’ for inclusion of trans-
+ action in blockchain (higher number means higher fee).
+ XMR only. Valid params: {tp}
+ If option is omitted, the default priority will be used
-- -q, --quiet Suppress warnings; overwrite files without prompting
-s -Q, --quote Get a swap quote and exit. Do not create transaction
-s -r, --stream-interval=N Set block interval for streaming swap (default: {si})

View file

@ -0,0 +1,25 @@
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2026 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
proto.xmr.addrdata: Monero AddrData and related classes
"""
from ...base_obj import AsyncInit
from ...addrdata import AddrData
from ...tw.addresses import TwAddresses
class MoneroTwAddrData(AddrData, metaclass=AsyncInit):
async def __init__(self, cfg, proto, *, twctl=None):
twaddrs = await TwAddresses(cfg, proto, add_twctl=False)
rpc_data = await twaddrs.get_rpc_data()
self.data = {k: v['addr'] for k, v in rpc_data.items()}
def mmaddr2coinaddr(self, mmaddr):
return self.data.get(mmaddr)

View file

@ -31,6 +31,7 @@ class mainnet(CoinProtocol.RPC, CoinProtocol.DummyWIF, CoinProtocol.Base):
addr_ver_info = {'12': 'monero', '2a': 'monero_sub', '13': 'monero_integrated'}
pubkey_types = ('monero',)
mmtypes = ('M',)
preferred_mmtypes = ('M',)
dfl_mmtype = 'M'
pubkey_type = 'monero' # required by DummyWIF
avg_bdi = 120
@ -39,6 +40,7 @@ class mainnet(CoinProtocol.RPC, CoinProtocol.DummyWIF, CoinProtocol.Base):
coin_amt = 'XMRAmt'
sign_mode = 'standalone'
has_usr_fee = False
address_reuse_ok = False
coin_cfg_opts = (
'ignore_daemon_version',

View file

@ -38,6 +38,8 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses):
include_empty = True
has_used = True
autochg_ignore_labels = True
autochg_match_full_list = True
prompt_fs_repl = {'XMR': (
(1, 'Filters: show [E]mpty addrs, [u]sed addrs, all [L]abels'),
(3, 'Actions: [q]uit menu, add [l]abel, [N]ew acct, [n]ew addr, [R]efresh bals:'))}
@ -50,6 +52,23 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses):
removed_key_mappings = {
'D': 'i_addr_delete'}
def get_target(self, key_in):
from ....addr import AddrIdx, MoneroIdx
return '{}:{}'.format(
self.al_id,
'{:0{w1}}:{:0{w2}}'.format(
*map(int, key_in.split(':')),
w1 = AddrIdx.max_digits,
w2 = MoneroIdx.max_digits))
@staticmethod
def get_bisect_key(d):
return d.twmmid.addr.sort_key
@staticmethod
def get_match_key(d):
return d.twmmid.addr.acct_sort_key
class display_type:
class squeezed(MoneroTwView.display_type.squeezed):

View file

@ -10,7 +10,10 @@
proto.xmr.tw.ctl: Monero tracking wallet control class
"""
from ....tw.ctl import TwCtl, write_mode
from ....addr import CoinAddr
from ....tw.shared import TwLabel
from ....tw.ctl import TwCtl, write_mode, label_addr_pair
from ....tw.addresses import TwAddresses
class MoneroTwCtl(TwCtl):
@ -46,3 +49,11 @@ class MoneroTwCtl(TwCtl):
compat_call = True)
op.c.call('close_wallet')
return await op.main(add_timestr=add_timestr)
async def get_label_addr_pairs(self):
twaddrs = await TwAddresses(self.cfg, self.proto, add_twctl=False)
rpc_data = await twaddrs.get_rpc_data()
return [label_addr_pair(
TwLabel(self.proto, k),
CoinAddr(self.proto, v['addr'])
) for k, v in rpc_data.items()]

View file

@ -43,9 +43,10 @@ class MoneroTwView:
'twmmid': lambda i: i.twmmid.sort_key} # sort_key begins with acct_sort_key
class MoneroTwViewItem(TwUnspentOutputs.MMGenTwUnspentOutput):
valid_attrs = {'amt', 'unlocked_amt', 'comment', 'twmmid', 'addr', 'confs', 'is_used', 'skip'}
valid_attrs = {'amt', 'unlocked_amt', 'comment', 'twmmid', 'al_id', 'addr', 'confs', 'is_used', 'skip'}
unlocked_amt = ImmutableAttr(CoinAmtChk, include_proto=True)
is_used = ImmutableAttr(bool)
al_id = ImmutableAttr(str)
class rpc:
caps = ()
@ -60,6 +61,7 @@ class MoneroTwView:
if dump_data:
self.sid = SeedID(sid=dump_data[0]['seed_id'])
self.al_id = self.sid + ':M'
self.total = self.unlocked_total = self.proto.coin_amt('0')
@ -112,6 +114,7 @@ class MoneroTwView:
self.MoneroTwViewItem(
self.proto,
twmmid = twmmid,
al_id = twmmid.addr.al_id,
addr = data['addr'],
confs = data['confs'],
is_used = data['is_used'],

View file

@ -13,3 +13,7 @@ proto.xmr.tx.base: Monero base transaction class
class Base:
is_compat = True
has_comment = False
@property
def nondata_outputs(self):
return ()

View file

@ -27,6 +27,12 @@ class New(Base, TxNew):
async def set_gas(self, *, to_addr=None, force=False):
pass
def process_data_output_arg(self, arg):
pass
def get_addrdata_from_files(self, proto, addrfiles):
pass
async def get_funds_remaining(self, fee, outputs_sum, start_fee_desc):
return True

View file

@ -0,0 +1,18 @@
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2026 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
proto.xmr.tx.new_swap: Monero new swap transaction class
"""
from ....tx.new_swap import NewSwap as TxNewSwap
from .new import New
class NewSwap(New, TxNewSwap):
desc = 'Monero swap transaction'

View file

@ -99,9 +99,9 @@ class TwAddresses(TwView):
def coinaddr_list(self):
return [d.addr for d in self.data]
async def __init__(self, cfg, proto, *, minconf=1, mmgen_addrs='', get_data=False):
async def __init__(self, cfg, proto, *, minconf=1, mmgen_addrs='', get_data=False, add_twctl=True):
await super().__init__(cfg, proto)
await super().__init__(cfg, proto, add_twctl=add_twctl)
self.minconf = NonNegativeInt(minconf)
self.spc_w = 5 + self.has_age + self.has_used # 1 space between cols + 1 leading space in fs

View file

@ -195,12 +195,12 @@ class TwView(MMGenObject, metaclass=AsyncInit):
def __new__(cls, cfg, proto, *args, **kwargs):
return MMGenObject.__new__(proto.base_proto_subclass(cls, cls.mod_subpath))
async def __init__(self, cfg, proto):
async def __init__(self, cfg, proto, *, add_twctl=True):
self.cfg = cfg
self.proto = proto
if have_rpc := 'rpc_init' in proto.mmcaps:
self.rpc = await rpc_init(cfg, proto)
if self.has_twctl:
if self.has_twctl and add_twctl:
from .ctl import TwCtl
self.twctl = await TwCtl(cfg, proto, mode='w', no_rpc=not have_rpc)
self.amt_iwidth_keys = {'amt': 'iwidth'} | ({'amt2': 'iwidth2'} if self.has_amt2 else {})

View file

@ -220,6 +220,9 @@ class New(Base):
elif arg is None:
res = al.get_change_address_by_addrtype(None, exclude=exclude, desc=desc)
req_desc = 'of any allowed address type'
elif al.is_account_based: # arg is an acct_id
res = al.get_change_address(arg, exclude=exclude, desc=desc)
req_desc = f'matching account {al.al_id}:{arg}'
elif obj := get_obj(MMGenAddrType, proto=proto, id_str=arg, silent=True):
res = al.get_change_address_by_addrtype(obj, exclude=exclude, desc=desc)
req_desc = f'of address type {arg!r}'

View file

@ -48,7 +48,11 @@ class NewSwap(BaseSwapMethods, New):
self._non_wallet_addr_confirm(message)
async def get_swap_output(self, proto, arg, addrfiles, desc):
if arg:
def is_acct_id(arg):
return (ss := arg.split(':', 1)) and all(map(is_int, ss))
if arg and not (desc == 'destination' and is_acct_id(arg)):
from ..addrdata import TwAddrData
pa = self.parse_cmdline_arg(
proto,
@ -204,7 +208,8 @@ class NewSwap(BaseSwapMethods, New):
from ..rpc import rpc_init
if not args.send_amt:
die(1, 'With --memoless (reference request transaction), you must supply a send amount')
self.rpc = await rpc_init(self.cfg, self.proto)
if 'rpc_init' in self.proto.mmcaps:
self.rpc = await rpc_init(self.cfg, self.proto)
qd = self.swap_proto_mod.quote_client(self, args.send_amt)
await self.display_quote_loop(qd, deduct_est_fee=False)
self.reference_memo = self.swap_proto_mod.reference_memo(

View file

@ -120,6 +120,7 @@ def op(op, cfg, infile, wallets, *, spec=None, compat_call=False):
from ..cfg import Config
cfg = Config({
'_clone': cfg,
'coin': 'XMR',
'compat': True,
'full_address': cfg.full_address or compat_call,
'xmrwallet_compat': True} | ({} if cfg.offline else {

View file

@ -582,6 +582,7 @@ class CmdTestAutosignThreaded(CmdTestAutosignBase):
need_rbf = False,
dump_hex = False,
expect_pat = None,
is_xmr = False,
mark_sent = False):
if need_rbf and not self.proto.cap('rbf'):
@ -604,6 +605,11 @@ class CmdTestAutosignThreaded(CmdTestAutosignBase):
t.expect(expect_pat, regex=True)
elif mark_sent:
t.written_to_file('Sent automount transaction')
elif is_xmr:
if expect_pat:
t.expect(expect_pat, regex=True)
t.expect('Submit transaction? (y/N): ', 'y')
t.written_to_file('Submitted transaction')
else:
t.view_tx('t', expect_pat=expect_pat)
t.do_comment(comment)

View file

@ -50,7 +50,19 @@ class ThornodeRPCServer(ThornodeServer):
'reference': '13579',
'asset': 'LTC.LTC',
'memo': '=:c:qq6myf0qd6q5tln96ntl37hq0u7940w2xvs9jsuqv0:7967e4/3/0',
'height': '12345678'}}
'height': '12345678'},
'fbbbd382': { # fbbbd382edd2d5478e0316b6c31ef6a0bbad57bcebc9d8b30cc52e5dbf107622
'reference': '78787',
'asset': 'XMR.XMR',
'memo': '=:l:rltc1q6rs4znw8kajd6n6yn2snlfra3pluz3x5paj6x3:0/3/0',
'height': '12349876'},
'ae04ecd8': { # ae04ecd8ff1f0c13861bd2d6123ce33ea03a731ccd142f5ca2f2450a54f215ba
'reference': '98765',
'asset': 'LTC.LTC',
'memo': '=:XMR.XMR:88u4waXXrPUD9EkCGMcFifKJakDZ6vx6zNpePuS238SLP7ziiMuqeP31c8ftJEDSymcSK88FFzKvQfoohF4LSyFu5E3yWTH:0/3/0',
'height': '12346789'}}
refreq_txid_xmr = 'fbbbd382edd2d5478e0316b6c31ef6a0bbad57bcebc9d8b30cc52e5dbf107622'
inbounds_info = get_inbounds_info()

View file

@ -26,16 +26,19 @@ prices = {
'LTC': 115,
'BCH': 330,
'ETH': 2304,
'XMR': 550,
'MM1': 0.998,
'RUNE': 1.4}
gas_rate_units = {
'BTC': 'satsperbyte',
'ETH': 'gwei'}
'ETH': 'gwei',
'XMR': 'atomic'}
recommended_gas_rate = {
'BTC': '6',
'ETH': '1'}
'ETH': '1',
'XMR': '600'}
data_template_from_rune = {
'outbound_delay_blocks': 0,

View file

@ -51,12 +51,14 @@ cmd_groups_dfl = {
'xmrwallet': gd('CmdTestXMRWallet', {}),
'xmr_autosign': gd('CmdTestXMRAutosign', {}),
'xmr_compat': gd('CmdTestXMRCompat', {'modname': 'xmr_autosign'}),
'xmrswap': gd('CmdTestXMRSwap', {}),
}
cmd_groups_extra = {
'ethswap_eth': gd('CmdTestEthSwapEth', {'modname': 'ethswap'}),
'ethbump_ltc': gd('CmdTestEthBumpLTC', {'modname': 'ethbump'}),
'runeswap_rune': gd('CmdTestRuneSwapRune', {'modname': 'runeswap'}),
'xmrswap_xmr': gd('CmdTestXMRSwapXMR', {'modname': 'xmrswap'}),
'dev': gd('CmdTestDev', {'modname': 'misc'}),
'regtest_legacy': gd('CmdTestRegtestBDBWallet', {'modname': 'regtest'}),
'autosign_btc': gd('CmdTestAutosignBTC', {'modname': 'autosign'}),
@ -263,6 +265,7 @@ cfgs = { # addr_idx_lists (except 31, 32, 33, 34) must contain exactly 8 address
'57': {}, # runeswap
'58': {}, # runeswap_rune
'59': {}, # autosign_eth
'77': {}, # xmrswap
'99': {}, # dummy
}

362
test/cmdtest_d/xmrswap.py Normal file
View file

@ -0,0 +1,362 @@
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2026 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
test.cmdtest_d.xmrswap: Monero swap tests for the cmdtest.py test suite
"""
import json
from .xmr_autosign import CmdTestXMRCompat
from .swap import CmdTestSwap, CmdTestSwapMethods, create_cross_methods
class CmdTestXMRSwapMethods:
def _swaptxcreate_ui(
self,
t,
prompt,
*,
tx_type = 'swap',
inputs = '1',
interactive_fee = None,
file_desc = 'Unsigned transaction',
tweaks = [],
auto_chg_addr = None,
auto_dest_addr = None,
reload_quote = False,
menu_prompt = None,
expect_pats = []):
if auto_chg_addr:
t.expect(rf'Using.*{auto_chg_addr}.*as change address.*: ', 'y', regex=True)
if tx_type == 'registration':
if auto_dest_addr:
t.expect(rf'Using.*{auto_dest_addr}.*as destination address.*: ', 'y', regex=True)
t.expect('continue: ', '\n')
t.expect(prompt, 'q')
t.expect('spend from: ', f'{inputs}\n')
elif tx_type in ('swap', 'xmr_swap'):
t.expect(prompt, 'q')
t.expect('to spend: ', f'{inputs}\n')
t.expect('continue: ', '\n')
if tx_type == 'xmr_swap':
for pat in expect_pats:
t.expect(pat, regex=True)
t.expect('(y/N): ', 'y') # save?
t.written_to_file('Unsigned transaction')
return t
t.expect('(Y/n): ', 'y') # fee OK?
t.expect('(Y/n): ', 'y') # change OK?
t.expect('(y/N): ', 'n') # add comment?
t.expect('view: ', 't')
for pat in expect_pats:
t.expect(pat, regex=True)
t.expect('to continue: ', '\n')
t.expect('(y/N): ', 'y') # save?
t.written_to_file('Unsigned automount transaction')
return t
def _swaptxcreate_refreq(
self,
*,
args,
inputs,
auto_chg_addr = None,
auto_dest_addr = None,
expect_pats = None):
return self._user_txcreate(
'bob',
progname = 'swaptxcreate',
opts = ['--monero-wallet-rpc-password=abc', '--memoless'],
ui_handler = self._swaptxcreate_ui,
ui_handler_args = {
'prompt': self.rune_menu_prompt,
'tx_type': 'registration',
'inputs': inputs,
'auto_chg_addr': auto_chg_addr,
'auto_dest_addr': auto_dest_addr,
'expect_pats': expect_pats},
output_args = args)
def _swaptxcreate_refnum(
self,
*,
args,
inputs,
tx_type = 'swap',
add_opts = [],
auto_chg_addr = None,
auto_dest_addr = None,
prompt = None,
expect_pats = None):
data = json.loads(self.read_from_tmpfile('memo_info.json'))
return self._user_txcreate(
'bob',
progname = 'swaptxcreate',
opts = ['--monero-wallet-rpc-password=abc', f'--refnum={data["reference"]}'] + add_opts,
ui_handler = self._swaptxcreate_ui,
ui_handler_args = {
'prompt': prompt or self.menu_prompt,
'tx_type': tx_type,
'inputs': inputs,
'auto_chg_addr': auto_chg_addr,
'auto_dest_addr': auto_dest_addr,
'expect_pats': expect_pats},
output_args = args)
def _swaptxsend_refnum(self, expect_pat, *, add_opts=[], is_xmr=False):
return self._user_txsend(
'bob',
add_opts = ['--monero-wallet-rpc-password=abc'] + add_opts,
expect_pat = expect_pat,
is_xmr = is_xmr,
need_rbf = True)
class CmdTestXMRSwap(CmdTestXMRSwapMethods, CmdTestSwap):
'Monero swap operations'
bdb_wallet = True
tmpdir_nums = [77]
networks = ('ltc',)
thornode_servers = ('rpc', 'swap')
cross_group = 'xmrswap_xmr'
cross_coin = 'xmr'
refreq_txid = 'ae04ecd8ff1f0c13861bd2d6123ce33ea03a731ccd142f5ca2f2450a54f215ba'
refreq_txid_xmr = 'fbbbd382edd2d5478e0316b6c31ef6a0bbad57bcebc9d8b30cc52e5dbf107622'
xmr_prompt = 'efresh balances:\b'
cmd_group_in = (
('setup', 'regtest (Bob and Alice) mode setup'),
('subgroup.ltc_setup', []),
('subgroup.xmr_setup', []),
('subgroup.swap', ['ltc_setup', 'xmr_setup']),
('subgroup.xmr_swap', ['swap']),
('xmr_wait_loop_kill', 'stopping autosign wait loop'),
('xmr_stop_daemons', 'stopping the XMR daemons'))
cmd_subgroups = {
'ltc_setup': (
'creating Bob’s MMGen wallet and tracking wallet',
('walletconv_bob', 'wallet creation (Bob)'),
('addrgen_bob', 'address generation (Bob)'),
('addrimport_bob', 'importing Bob’s addresses'),
('bob_import_miner_addr', 'importing miner’s coinbase addr into Bob’s wallet'),
('fund_bob1', 'funding Bob’s wallet'),
('fund_bob2', 'funding Bob’s wallet'),
('fund_bob3', 'funding Bob’s wallet'),
('generate_ltc', 'mining a block'),
('bob_bal1', 'Bob’s balance'),
),
'xmr_setup': (
'setting miner’s and user’s XMR tracking wallets',
('xmr_autosign_setup', ''),
('xmr_autosign_xmr_setup', ''),
('xmr_addrimport_user', ''),
('xmr_gen_kafile_miner', ''),
('xmr_gen_vkafile_user', ''),
('xmr_create_wallet_miner', ''),
('xmr_mine_initial_coins', ''),
('xmr_fund_user1', ''),
('xmr_mine_blocks_10', ''),
('xmr_new_address_user1', ''),
('xmr_new_address_user2', ''),
('xmr_new_account_user', ''),
('xmr_mine_blocks_10', ''),
('xmr_user_listaddresses_refresh', ''),
('xmr_wait_loop_start_compat', ''),
),
'swap': (
'swap operations (LTC -> XMR)',
('rune_addrgen', 'generating RUNE addresses'),
('rune_addrimport', 'importing RUNE addresses'),
('rune_bal_refresh', 'refreshing balances in RUNE tracking wallet'),
('remote_get_refnum', 'getting the reference number'),
('swaptxcreate_refreq1', 'creating an LTC->XMR swap transaction (refreq, auto chg, auto dest)'),
('swaptxcreate_abort1', 'aborting the transaction'),
('swaptxcreate_refreq2', 'creating an LTC->XMR swap transaction (refreq, auto chg, 1:0)'),
('swaptxcreate_abort2', 'aborting the transaction'),
('swaptxcreate_refreq3', 'creating an LTC->XMR swap transaction (refreq, auto chg, mmid dest)'),
('swaptxsend_refreq1', 'sending the transaction'),
('swaptxcreate_refreq4', 'creating an LTC->XMR swap transaction (refreq, auto chg, 1:1)'),
('swaptxsend_refreq2', 'sending the transaction'),
('swaptxcreate_refnum1', 'creating an LTC->XMR swap transaction (swap, auto chg, auto dest)'),
('swaptxsend_refnum1', 'sending the swap transaction'),
('generate_ltc', 'generating a block'),
('swaptxcreate_refnum2', 'creating an LTC->XMR swap transaction (swap, no chg, auto dest)'),
('swaptxsend_refnum2', 'sending the swap transaction'),
('generate_ltc', 'generating a block'),
('bob_bal2', 'Bob’s balance')),
'xmr_swap': (
'swap operations (XMR -> LTC)',
('remote_get_refnum_xmr', 'getting the reference number'),
('swaptxcreate_refreq7', 'creating an XMR->LTC swap transaction (refreq, auto chg, auto dest)'),
('swaptxsend_refreq7', 'sending the transaction'),
('swaptxcreate_refnum7', 'creating an XMR->LTC swap transaction (swap, auto chg, auto dest)'),
('swaptxsend_refnum7', 'sending the transaction'),
('xmr_mine_blocks_10', ''),
('swaptxstatus_refnum7', 'getting the transaction status'))}
exec(create_cross_methods(cross_coin, cross_group, cmd_group_in, cmd_subgroups))
def __init__(self, cfg, trunner, cfgs, spawn):
super().__init__(cfg, trunner, cfgs, spawn)
self.spawn_env['MMGEN_TEST_SUITE_ENABLE_USER_PORT_SHIFT'] = '1'
if not trunner:
return
globals()[self.cross_group] = self.create_cross_runner(trunner, add_cfg={})
def fund_bob1(self):
return self.fund_wallet('bob', 500, mmtype='C')
def fund_bob2(self):
return self.fund_wallet('bob', 50, mmtype='S')
def fund_bob3(self):
return self.fund_wallet('bob', 25, mmtype='B')
def bob_bal1(self):
return self._bob_bal(0, '575')
def swaptxcreate_refreq1(self):
return self._swaptxcreate_refreq(
args = ['LTC', '5.4321', 'XMR'],
inputs = '3',
auto_chg_addr = '98831F3A:B:2',
auto_dest_addr = '98831F3A:M:1/0/1',
expect_pats = ['88u4waXXrPUD9EkCGMcFif', r'to spend:.*\b0\b.*RUNE'])
def swaptxcreate_abort1(self):
return self._txsend_abort()
def swaptxcreate_refreq2(self):
return self._swaptxcreate_refreq(
args = ['LTC', '5.4321', 'XMR', '1:0'],
inputs = '3',
auto_chg_addr = '98831F3A:B:2',
auto_dest_addr = '98831F3A:M:1/0/1',
expect_pats = ['88u4waXXrPUD9EkCGMcFif', r'to spend:.*\b0\b.*RUNE'])
def swaptxcreate_abort2(self):
return self._txsend_abort()
def swaptxcreate_refreq3(self):
return self._swaptxcreate_refreq(
args = ['LTC', '5.4321', 'XMR', '98831F3A:M:1/0/1'],
inputs = '3',
auto_chg_addr = '98831F3A:B:2',
expect_pats = ['88u4waXXrPUD9EkCGMcFif', r'to spend:.*\b0\b.*RUNE'])
def swaptxsend_refreq1(self):
return self._user_txsend('bob', need_rbf=True)
def swaptxcreate_refreq4(self):
return self._swaptxcreate_refreq(
args = ['LTC', '5.4321', 'XMR', '1:1'],
inputs = '3',
auto_chg_addr = '98831F3A:B:2',
auto_dest_addr = '98831F3A:M:1/1/0',
expect_pats = ['89NVJJPWPw18P7XoqiYgj4', r'to spend:.*\b0\b.*RUNE'])
swaptxsend_refreq2 = swaptxsend_refreq1
def swaptxcreate_refnum1(self):
return self._swaptxcreate_refnum(
args = ['LTC', '5.4321', 'XMR'],
inputs = '1',
auto_chg_addr = '98831F3A:B:2',
expect_pats = ['88u4waXXrPUD9EkCGMcFif', r'Spend amount:.*\b5\.43198765\b.*LTC'])
def swaptxsend_refnum1(self):
return self._swaptxsend_refnum(r'Spend amount:.*\b5\.43198765\b.*LTC')
def swaptxcreate_refnum2(self):
return self._swaptxcreate_refnum(
args = ['LTC', 'XMR'],
inputs = '2',
expect_pats = ['88u4waXXrPUD9EkCGMcFif', r'Spend amount:.*24\.99998765'])
def swaptxsend_refnum2(self):
return self._swaptxsend_refnum(r'Spend amount:.*24\.99998765')
def bob_bal2(self):
return self._bob_bal(0, '544.56799927')
def remote_get_refnum_xmr(self):
return self._remote_get_refnum(self.refreq_txid_xmr)
def swaptxcreate_refreq7(self):
return self._swaptxcreate_refreq(
args = ['XMR', '0.2345', 'LTC'],
inputs = '3',
auto_dest_addr = '98831F3A:B:3',
expect_pats = ['rltc1q6rs4znw8k', r'to spend:.*\b0\b.*RUNE'])
def swaptxsend_refreq7(self):
return self._user_txsend('bob', need_rbf=True)
def swaptxcreate_refnum7(self):
return self._swaptxcreate_refnum(
add_opts = ['--priority=3'],
args = ['XMR', '0.2345', 'LTC'],
inputs = '1',
tx_type = 'xmr_swap',
prompt = self.xmr_prompt,
auto_dest_addr = '98831F3A:B:3',
expect_pats = [r'Amount:.*0.234499978787', r'Priority:.*high'])
def swaptxsend_refnum7(self):
return self._swaptxsend_refnum(r'Amount:.*0.234499978787', is_xmr=True)
def swaptxstatus_refnum7(self):
return self._swaptxsend_refnum(r'Amount:.*0.234499978787', add_opts=['--status', '--verbose'])
class CmdTestXMRSwapXMR(CmdTestXMRSwapMethods, CmdTestSwapMethods, CmdTestXMRCompat):
'Monero swap operations - Monero wallet'
networks = ('xmr',)
tmpdir_nums = [77]
is_helper = True
tx_relay_user = None
user = 'bob'
user_data = (
# user sid autosign kal_range add_coind_args
('miner', 'FE3C6545', False, '1', []),
('bob', '98831F3A', True, '1-2', []))
cmd_group = CmdTestXMRCompat.cmd_group + (
('user_txcreate_sweep1', 'creating a sweep transaction (address sweep)'),
('new_address_user1', 'adding an address to user’s wallet (acct 0)'),
('new_address_user2', 'adding an address to user’s wallet (acct 0)'),
('new_account_user', 'adding an account to user’s wallet'),
('user_listaddresses_refresh', 'listing the user’s wallet, with refresh'))
def user_txcreate_sweep1(self):
return self._user_txops('txcreate', menu='s', sweep_menu='1', sweep_type='sweep')
def _new_address_user(self, wallet, acct, action):
return self._new_addr_user_autosign(
f'{wallet}:{acct}',
action,
rf'Creating new address for wallet .*{wallet}.*, account .*#{acct}.* with label '
r'.*‘xmrwallet new address .*y/N\): ')
def new_address_user1(self):
return self._new_address_user(1, 0, 'start')
def new_address_user2(self):
return self._new_address_user(1, 0, 'continue')
def new_account_user(self):
return self._new_addr_user_autosign(
'1',
'stop',
r'Creating new account for wallet .*1.* with label '
r'.*‘xmrwallet new account .*y/N\): ')

View file

@ -31,8 +31,7 @@ class overlay_fake_Daemon:
'miner': 1,
'bob': 2,
'alice': 3}
if os.getenv('MMGEN_TEST_SUITE_ENABLE_USER_PORT_SHIFT'):
if os.getenv('MMGEN_TEST_SUITE_ENABLE_USER_PORT_SHIFT') and self.proto.coin == 'XMR':
return 1 << 7 | udata[self.test_user or self.cfg.test_user] << 5
else:
return 1 << 7

View file

@ -3,6 +3,7 @@ from .asset_orig import *
class overlay_fake_THORChainSwapAsset:
assets_data = {
'XMR': THORChainSwapAsset._ad('Monero', 'XMR', 'XMR.XMR', None, None, True),
'ETH.MM1': THORChainSwapAsset._ad('MM1 Token (ETH)', None, 'ETH.MM1', None, 18, True),
'ETH.JUNK': THORChainSwapAsset._ad('Junk Token (ETH)', None, 'ETH.JUNK', None, 6, True),
'ETH.NONE': THORChainSwapAsset._ad('Banned Token (ETH)', None, 'ETH.NONE', None, 18, True)}

View file

@ -1 +1 @@
[{"chain":"BCH","pub_key":"thorpub1addwnpepqvg02tp8umuqp36wy5ptpeqavkdu58gk0z8e7amnjyh3cpslfq9fqxhlmvp","address":"qpxmqf20pnzywzmjevclquekjzlvq5hx25s4ucj53r","halted":false,"global_trading_paused":false,"chain_trading_paused":false,"chain_lp_actions_paused":false,"observed_fee_rate":"2","gas_rate":"3","gas_rate_units":"satsperbyte","outbound_tx_size":"1500","outbound_fee":"74450","dust_threshold":"10000"},{"chain":"BTC","pub_key":"thorpub1addwnpepqvg02tp8umuqp36wy5ptpeqavkdu58gk0z8e7amnjyh3cpslfq9fqxhlmvp","address":"bc1qfkcz2ncvc3rskuktx8c8xd5shmq99ej45gq8wu","halted":false,"global_trading_paused":false,"chain_trading_paused":false,"chain_lp_actions_paused":false,"observed_fee_rate":"4","gas_rate":"6","gas_rate_units":"satsperbyte","outbound_tx_size":"1000","outbound_fee":"2120","dust_threshold":"1000"},{"chain":"ETH","pub_key":"thorpub1addwnpepqvg02tp8umuqp36wy5ptpeqavkdu58gk0z8e7amnjyh3cpslfq9fqxhlmvp","address":"0x41c88d3c0eda3f4df0bb4e5d0f4c81cc0232902c","router":"0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146","halted":false,"global_trading_paused":false,"chain_trading_paused":false,"chain_lp_actions_paused":false,"observed_fee_rate":"10","gas_rate":"15","gas_rate_units":"gwei","outbound_tx_size":"100000","outbound_fee":"9190","dust_threshold":"1000"},{"chain":"LTC","pub_key":"thorpub1addwnpepqvg02tp8umuqp36wy5ptpeqavkdu58gk0z8e7amnjyh3cpslfq9fqxhlmvp","address":"rltc1qlvwym7fha5cuvs0c2vy2l86r7a8ayt8chwqsyr","halted":false,"global_trading_paused":false,"chain_trading_paused":false,"chain_lp_actions_paused":false,"observed_fee_rate":"74","gas_rate":"111","gas_rate_units":"satsperbyte","outbound_tx_size":"250","outbound_fee":"355075","dust_threshold":"100000"}]
[{"chain":"BCH","pub_key":"thorpub1addwnpepqvg02tp8umuqp36wy5ptpeqavkdu58gk0z8e7amnjyh3cpslfq9fqxhlmvp","address":"qpxmqf20pnzywzmjevclquekjzlvq5hx25s4ucj53r","halted":false,"global_trading_paused":false,"chain_trading_paused":false,"chain_lp_actions_paused":false,"observed_fee_rate":"2","gas_rate":"3","gas_rate_units":"satsperbyte","outbound_tx_size":"1500","outbound_fee":"74450","dust_threshold":"10000"},{"chain":"BTC","pub_key":"thorpub1addwnpepqvg02tp8umuqp36wy5ptpeqavkdu58gk0z8e7amnjyh3cpslfq9fqxhlmvp","address":"bc1qfkcz2ncvc3rskuktx8c8xd5shmq99ej45gq8wu","halted":false,"global_trading_paused":false,"chain_trading_paused":false,"chain_lp_actions_paused":false,"observed_fee_rate":"4","gas_rate":"6","gas_rate_units":"satsperbyte","outbound_tx_size":"1000","outbound_fee":"2120","dust_threshold":"1000"},{"chain":"ETH","pub_key":"thorpub1addwnpepqvg02tp8umuqp36wy5ptpeqavkdu58gk0z8e7amnjyh3cpslfq9fqxhlmvp","address":"0x41c88d3c0eda3f4df0bb4e5d0f4c81cc0232902c","router":"0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146","halted":false,"global_trading_paused":false,"chain_trading_paused":false,"chain_lp_actions_paused":false,"observed_fee_rate":"10","gas_rate":"15","gas_rate_units":"gwei","outbound_tx_size":"100000","outbound_fee":"9190","dust_threshold":"1000"},{"chain":"LTC","pub_key":"thorpub1addwnpepqvg02tp8umuqp36wy5ptpeqavkdu58gk0z8e7amnjyh3cpslfq9fqxhlmvp","address":"rltc1qlvwym7fha5cuvs0c2vy2l86r7a8ayt8chwqsyr","halted":false,"global_trading_paused":false,"chain_trading_paused":false,"chain_lp_actions_paused":false,"observed_fee_rate":"74","gas_rate":"111","gas_rate_units":"satsperbyte","outbound_tx_size":"250","outbound_fee":"355075","dust_threshold":"100000"},{"chain":"XMR","pub_key":"thorpub1addwnpepqvg02tp8umuqp36wy5ptpeqavkdu58gk0z8e7amnjyh3cpslfq9fqxhlmvp","address":"46aRdC5abaWj3jRyyCVsr8fdhSgyxhg2wfgnifctA1gAAvsMT4YiX48VhzUUNvDmdH4ucwpQouWXj7Dta95qCcC7SsxkZoP","halted":false,"global_trading_paused":false,"chain_trading_paused":false,"chain_lp_actions_paused":false,"observed_fee_rate":"8","gas_rate":"4","gas_rate_units":"atomic","outbound_tx_size":"250","outbound_fee":"355075","dust_threshold":"100000"}]

View file

@ -267,7 +267,7 @@ init_tests() {
d_ltc="overall operations with emulated RPC data (Litecoin)"
t_ltc="
- $cmdtest_py --coin=ltc --exclude regtest,autosign_automount,help,runeswap
- $cmdtest_py --coin=ltc --exclude regtest,autosign_automount,help,runeswap,xmrswap
- $cmdtest_py --coin=ltc --segwit
- $cmdtest_py --coin=ltc --segwit-random
- $cmdtest_py --coin=ltc --bech32
@ -322,6 +322,8 @@ init_tests() {
d_xmr="Monero xmrwallet operations"
t_xmr="
- test/clean.py
- ${xmr_env[*]} $cmdtest_py --coin=ltc xmrswap
- ${xmr_env[*]} $cmdtest_py --coin=xmr --exclude help
s ${xmr_env[*]} $cmdtest_py --coin=xmr xmr_autosign_nocompat
"