memoless swaps: set trade limit and destination address from registered memo

Revises ffe488ff (THORChain memoless swaps)

For memoless swaps, it is now forbidden to specify a trade limit or destination
address in the swap transaction invocation (second pass), as these are already
set in the reference memo registered on the THORChain network, so duplicating
them on the command line only leads to confusion.

If a trade limit was specified in the registration transaction, it’s up to the
user to ensure that the swap transaction spend amount is appropriate for the
trade limit.  Increasing the spend amount is possible, but any significant
increase effectively nullifies the protection the trade limit provides.
This commit is contained in:
The MMGen Project 2026-10-19 09:06:13 +00:00
commit ff35b01c45
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
7 changed files with 28 additions and 30 deletions

View file

@ -1 +1 @@
16.3.0dev17
16.3.0dev18

View file

@ -34,7 +34,9 @@ opts_data = {
('yes', True, 'quiet', True),
('refnum', bool, 'memoless', True),
('no_refreq_tx', True, 'memoless', True)],
'requires': [('coin', 'XMR', 'autosign', True)],
'requires': [
('coin', 'XMR', 'autosign', True),
('refnum', bool, 'trade_limit', False)],
'text': {
'desc': {
'tx': f'Create a transaction with outputs to specified coin or {gc.proj_name} addresses',

View file

@ -11,7 +11,6 @@ tx.base_swap: base swap transaction mixin class
"""
from ..util import ymsg, die, cached_property
from ..addr import CoinAddr
class BaseSwapMethods:
@ -85,7 +84,7 @@ class BaseSwapMethods:
if rpc.data_for_coin(data, asset.coin)[key]:
die(2, f'Trading is {state} on the {self.swap_proto_mod.name} network')
async def parse_network_memo(self, *, data, recv_asset_spec, recv_twmmid, chk_only=False):
async def parse_network_memo(self, *, data, recv_asset_spec, recv_twmmid=None):
from ..swap.util import init_swap_proto
from ..tw.ctl import TwCtl
pm = self.swap_proto_mod.Memo.parse(data['memo'])
@ -100,6 +99,4 @@ class BaseSwapMethods:
ymsg(
f'Warning: address from registered memo ({ra.twmmid!r}) '
f'does not match our destination address ({recv_twmmid!r})')
return None if chk_only else (
self._output(pm.asset.coin, self.cfg.network, CoinAddr(pm_proto, pm.address), ra.twmmid),
pm)
return (pm, ra)

View file

@ -13,6 +13,7 @@ tx.new_swap: new swap transaction class
import sys, time
from collections import namedtuple
from ..addr import CoinAddr
from ..amt import UniAmt
from ..util import msg, ymsg, is_int, die
from ..swap.util import get_swap_asset, init_swap_proto
@ -60,7 +61,8 @@ class NewSwap(BaseSwapMethods, New):
full_desc = f'{desc} address on the {proto.coin} {proto.network} network'
res = await self.get_autochg_addr(proto, arg, exclude=[], desc=full_desc)
self.confirm_autoselected_addr(res.twmmid, res.comment, full_desc)
if not (self.refnum and desc == 'destination'):
self.confirm_autoselected_addr(res.twmmid, res.comment, full_desc)
return self._output(proto.coin, proto.network, res.addr, res.twmmid)
async def get_chg_output(self, arg, addrfiles):
@ -70,17 +72,6 @@ class NewSwap(BaseSwapMethods, New):
message = 'Change address is not an MMGen wallet address!')
return chg_output
def check_ref_memo(self, cls, memo, pm_ref):
pm = cls.parse(str(memo))
if cls.digest(pm) != cls.digest(pm_ref):
if pm.trade_limit != pm_ref.trade_limit:
ymsg(
f'Trade limit has changed! {pm.trade_limit} != {pm_ref.trade_limit}\n'
'Cannot create this transaction!')
die(2,
'Newly-created memo does not match registered memo:'
f'\n new: {cls.digest(pm)}\n registered: {cls.digest(pm_ref)}')
async def process_swap_cmdline_args(self, cmd_args, addrfiles):
class CmdlineArgs: # listed in command-line order
@ -129,6 +120,8 @@ class NewSwap(BaseSwapMethods, New):
# arg 5: recv_spec (receive address spec)
if args_in:
if self.refnum:
die(1, 'destination address must be omitted with --refnum')
args.recv_spec = get_arg()
if args_in: # done parsing, all args consumed
@ -165,10 +158,16 @@ class NewSwap(BaseSwapMethods, New):
chg_output = await self.get_chg_output(None, addrfiles)
if self.refnum:
recv_output, parsed_ref_memo = await self.parse_network_memo(
pm, resolved_addr = await self.parse_network_memo(
data = self.swap_proto_rpc.validate_refnum(self.proto.coin, self.refnum),
recv_asset_spec = self.swap_recv_asset_spec,
recv_twmmid = args.recv_spec)
recv_asset_spec = self.swap_recv_asset_spec)
if pm.trade_limit:
self.swap_cfg.trade_limit = UniAmt(pm.trade_limit, from_unit='satoshi')
recv_output = self._output(
self.recv_proto.coin,
self.recv_proto.network,
CoinAddr(self.recv_proto, pm.address),
resolved_addr.twmmid)
else:
recv_output = await self.get_swap_output(
self.recv_proto,
@ -193,9 +192,6 @@ class NewSwap(BaseSwapMethods, New):
self.swap_cfg.trade_limit if isinstance(self.swap_cfg.trade_limit, UniAmt)
else None))
if self.refnum:
self.check_ref_memo(sp.Memo, memo, parsed_ref_memo)
if self.is_memoless:
self.memoless_memo = str(memo)

View file

@ -42,8 +42,7 @@ class OnlineSigned(Signed):
await self.parse_network_memo(
data = data,
recv_asset_spec = self.swap_recv_asset_spec,
recv_twmmid = self.swap_recv_addr_mmid,
chk_only = True)
recv_twmmid = self.swap_recv_addr_mmid)
def check_swap_expiry(self):
from ..util2 import format_elapsed_hr

View file

@ -49,7 +49,7 @@ class ThornodeRPCServer(ThornodeServer):
'0c7454c0': { # 0c7454c06d3fc0e520d34283e6f491ce7afe5abb3f569bbdfea1732e04b716ec
'reference': '13579',
'asset': 'LTC.LTC',
'memo': '=:c:qq6myf0qd6q5tln96ntl37hq0u7940w2xvs9jsuqv0:0/3/0',
'memo': '=:c:qq6myf0qd6q5tln96ntl37hq0u7940w2xvs9jsuqv0:7967e4/3/0',
'height': '12345678'}}
inbounds_info = get_inbounds_info()

View file

@ -372,6 +372,7 @@ class CmdTestSwap(
('swaptxcreate_bad8', 'creating a swap transaction (bad, non-MMGen change address)'),
('swaptxcreate_bad9', 'creating a swap transaction (bad, invalid addrtype)'),
('swaptxcreate_bad10', 'creating a swap transaction (bad, --quote with no amount)'),
('swaptxcreate_bad11', 'creating a swap transaction (bad, --refnum with --trade-limit)'),
),
'create_neterror': (
'Swap TX create operations: network error handling',
@ -656,6 +657,9 @@ class CmdTestSwap(
def swaptxcreate_bad10(self):
return self._swaptxcreate_bad(['BCH', 'LTC'], add_opts=['--quote'], exit_val=1, expect1='send amount')
def swaptxcreate_bad11(self):
return self._swaptxcreate_bad(['BCH', 'LTC'], add_opts=['--refnum=123', '--trade-limit=1%'], exit_val=1, expect1='requires option')
def swaptxcreate_neterror1(self):
return self._swaptxcreate_neterror('ltc_global_paused', 'Trading is paused on the')
@ -868,7 +872,7 @@ class CmdTestSwap(
self.get_file_with_ext('rawtx', delete_all=True)
return self._memoless_swaptxcreate(
['LTC', '2.345', f'{self.subsid}:B:1', 'BCH', f'{self.sid}:C:2'],
opts = ['--memoless'],
opts = ['--memoless', '--trade-limit=2.5%'],
refreq = True)
refreq_swaptxsign = rune_txsign_refreq
@ -887,7 +891,7 @@ class CmdTestSwap(
refnum = True)
def refnum_swaptxcreate1(self):
return self._refnum_swaptxcreate(['LTC', '2.345', f'{self.subsid}:B:1', 'BCH', f'{self.sid}:C:2'])
return self._refnum_swaptxcreate(['LTC', '2.345', f'{self.subsid}:B:1', 'BCH'])
def refnum_swaptxsign1(self):
return self._swaptxsign()