THORChain memoless swaps
Memoless swaps permit coins with long addresses and/or restricted memo space, such as Monero, for example, to be swapped on THORChain. They introduce added complexity, along with certain privacy and security tradeoffs, so should only be used for assets that cannot be swapped the usual way. A reference request transaction (a.k.a. registration transaction) containing the memo is sent to the THORChain network, which assigns it a number. This number is then encoded in the send amount of the swap transaction in lieu of a memo. See https://dev.thorchain.org/swap-guide/memoless-swaps.html for technical details. While this patch fully implements the feature in MMGen Wallet, it’s disabled on mainnet for the time being. Mainnet will be enabled after memoless swaps are unhalted on THORChain mainnet and the feature can undergo live testing. The following example is provided for purposes of illustration only. Normally we would swap LTC to BCH the standard way. Sample LTC-to-BCH memoless swap workflow (assumes --autosign): # Create the reference request transaction (pass 1): $ mmgen-swaptxcreate --memoless LTC 1.2345 BCH # Autosign the reference request 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): $ mmgen-swaptxcreate --refnum=REFNUM LTC 1.2345 BCH # Sign and send the swap transaction: $ mmgen-txsend # Monitor your BCH wallet for the funds: $ mmgen-tool --coin=bch twview Testing/demo: $ test/cmdtest.py -e swap.memoless Note that the real send amount is the amount specified in the second invocation of ‘mmgen-swaptxcreate’ (i.e. the swap transaction or second pass). If you omit the --trade-limit option, the amounts in the two invocations can differ, otherwise amount and trade limit must be identical for both invocations. Before sending, the software checks that the provided reference number is valid and points to an address in the user’s tracking wallet. Thus funds cannot be sent to the wrong destination as a result of user error. References remain valid for approximately six hours. Before sending, the software checks that the reference is valid, but not whether it is close to expiration. If the reference later expires before the swap transaction is confirmed, there’s a danger of funds loss. Therefore, it’s recommended to create and broadcast the swap transaction without delay. If necessary, the transaction can be bumped to ensure timely confirmation. In order to create and send the reference request transaction, the user must have some RUNE in their wallet. The current cost per transaction is 0.2 RUNE. Also be aware of the privacy implications. Swaps can easily be correlated if their registration transactions spend from the same RUNE address, or from RUNE addresses that can be linked via chain analysis. More complete documentation will be provided at a later date.
This commit is contained in:
parent
9ad34f5001
commit
ffe488ffbd
33 changed files with 839 additions and 45 deletions
|
|
@ -220,7 +220,7 @@ class ETHAmt(CoinAmt):
|
|||
szabo = Decimal('0.000001')
|
||||
finney = Decimal('0.001')
|
||||
atomic = wei
|
||||
units = ('wei', 'Kwei', 'Mwei', 'Gwei', 'szabo', 'finney')
|
||||
units = ('atomic', 'wei', 'Kwei', 'Mwei', 'Gwei', 'szabo', 'finney')
|
||||
|
||||
def toWei(self):
|
||||
return int(Decimal(self) // self.wei)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
16.3.0dev15
|
||||
16.3.0dev16
|
||||
|
|
|
|||
|
|
@ -131,6 +131,9 @@ mods = {
|
|||
'wif2redeem_script',
|
||||
'wif2segwit_pair',
|
||||
),
|
||||
'amt': (
|
||||
'memoless_amt',
|
||||
),
|
||||
'mnemonic': (
|
||||
'hex2mn',
|
||||
'mn2hex',
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ opts_data = {
|
|||
'tx': ['-', 't'],
|
||||
'swaptx': ['-', 's'],
|
||||
}[target],
|
||||
'sets': [('yes', True, 'quiet', True)],
|
||||
'sets': [
|
||||
('yes', True, 'quiet', True),
|
||||
('refnum', bool, 'memoless', True),
|
||||
('no_refreq_tx', True, 'memoless', True)],
|
||||
'requires': [('coin', 'XMR', 'autosign', True)],
|
||||
'text': {
|
||||
'desc': {
|
||||
|
|
@ -73,6 +76,12 @@ opts_data = {
|
|||
b- -L, --autochg-ignore-labels Ignore labels when autoselecting change addresses
|
||||
-- -m, --minconf= n Minimum number of confirmations required to spend
|
||||
+ outputs (default: 1)
|
||||
-s -M, --memoless Perform a memoless swap, creating a reference request
|
||||
+ TX (activates first pass).
|
||||
-s -n, --refnum=N The memoless reference number obtained after sending
|
||||
+ 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-
|
||||
+ action in blockchain (higher number means higher fee).
|
||||
+ Valid parameters: {tp}.
|
||||
|
|
@ -155,7 +164,7 @@ async def main():
|
|||
from .tx import NewTX
|
||||
if target == 'swaptx':
|
||||
from .tx.new_swap import get_send_proto
|
||||
tx1 = NewTX(cfg=cfg, proto=get_send_proto(cfg), target=target)
|
||||
tx1 = NewTX(cfg=cfg, proto=get_send_proto(cfg), target=target, is_memoless=cfg.memoless)
|
||||
else:
|
||||
tx1 = NewTX(cfg=cfg, proto=cfg._proto, target=target)
|
||||
|
||||
|
|
@ -164,7 +173,18 @@ async def main():
|
|||
locktime = int(cfg.locktime or 0),
|
||||
do_info = cfg.info)
|
||||
|
||||
if not tx1.is_compat:
|
||||
if tx1.is_compat:
|
||||
return
|
||||
|
||||
if tx1.is_swap and tx1.is_refreq: # memoless first pass
|
||||
if cfg.no_refreq_tx:
|
||||
Msg(f'MEMO: {tx1.reference_memo}')
|
||||
else:
|
||||
rune_cfg = Config({'_clone': cfg, 'coin': 'rune', 'fee': tx1.memoless_tx_fee})
|
||||
tx2 = NewTX(cfg=rune_cfg, proto=rune_cfg._proto, target='tx', refreq_src=tx1)
|
||||
tx3 = await tx2.create(args=('deposit,0', f'data:{tx1.reference_memo}'))
|
||||
write_to_file(tx3, asi)
|
||||
else:
|
||||
write_to_file(tx2, asi)
|
||||
|
||||
async_run(cfg, main)
|
||||
|
|
|
|||
|
|
@ -175,8 +175,12 @@ async def process_tx(tx):
|
|||
await tx.post_send(asi)
|
||||
return 0
|
||||
|
||||
if tx.is_swap:
|
||||
if tx.is_swap or tx.is_refreq:
|
||||
tx.check_swap_network()
|
||||
|
||||
if tx.is_swap:
|
||||
if tx.is_memoless:
|
||||
await tx.check_memoless_presend()
|
||||
if not tx.check_swap_expiry():
|
||||
die(1, 'Swap quote has expired. Please re-create the transaction')
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ proto.btc.tx.info: Bitcoin transaction info class
|
|||
|
||||
from ....tx.info import TxInfo
|
||||
from ....util import fmt, die
|
||||
from ....color import red, green, blue, pink
|
||||
from ....color import red, green, blue, pink, orange
|
||||
from ....addr import MMGenID
|
||||
|
||||
class TxInfo(TxInfo):
|
||||
|
|
@ -62,10 +62,15 @@ class TxInfo(TxInfo):
|
|||
append_chars = ('', ' (chg)')[bool(is_output and e.is_chg and terse)],
|
||||
append_color = 'green')
|
||||
else:
|
||||
return MMGenID.fmtc(
|
||||
'[vault address]' if not is_input and e.is_vault else nonmm_str,
|
||||
max_mmwid,
|
||||
color = True)
|
||||
return (
|
||||
(orange('[vault refnum ')
|
||||
+ red(str(tx.refnum))
|
||||
+ orange(']')
|
||||
+ (' ' * (max_mmwid - 15 - len(str(tx.refnum))))
|
||||
if tx.is_memoless and terse and tx.refnum else
|
||||
MMGenID.fmtc('[vault address]', max_mmwid, color=True))
|
||||
if is_output and e.is_vault else
|
||||
MMGenID.fmtc(nonmm_str, max_mmwid, color=True))
|
||||
|
||||
def format_io(desc):
|
||||
io = getattr(tx, desc)
|
||||
|
|
@ -121,6 +126,8 @@ class TxInfo(TxInfo):
|
|||
if have_bch and e.addr:
|
||||
yield ('', '', f'[{e.addr.hl(vp2, color=False)}]')
|
||||
yield from gen_common(e)
|
||||
if e.is_vault and tx.is_memoless and tx.refnum:
|
||||
yield ('', 'refnum:', red(str(tx.refnum)))
|
||||
if e.is_chg:
|
||||
yield ('', 'change:', green('True'))
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,10 @@ class THORChainRemoteRPCClient(RemoteRPCClient):
|
|||
'acct_info',
|
||||
'balance',
|
||||
'quote',
|
||||
'tx_info')}
|
||||
'tx_info',
|
||||
'get_refnum',
|
||||
'validate_refnum',
|
||||
'validate_amt')}
|
||||
|
||||
def __init__(self, cfg, proto):
|
||||
for k, v in proto.rpc_remote_params.items():
|
||||
|
|
@ -160,6 +163,34 @@ class THORChainRemoteRPCClient(RemoteRPCClient):
|
|||
self.swap_api.get(path=f'/thorchain/quote/swap?{qs}'),
|
||||
errmsg = 'swap quote request failed')
|
||||
|
||||
def get_refnum(self, txid: str):
|
||||
"get reference memo info for a reference request transaction by TxID"
|
||||
return process_response(
|
||||
self.rest_api.get(path=f'/thorchain/memo/{txid}'),
|
||||
errmsg = f'get reference memo info for txid ‘{txid}’ failed')
|
||||
|
||||
def validate_refnum(self, asset: str, refnum: str):
|
||||
"check that given asset/reference combination is valid"
|
||||
from ....swap.util import get_swap_asset
|
||||
swap_asset = get_swap_asset(self.cfg, asset, sanitize_input=True)
|
||||
return process_response(
|
||||
self.rest_api.get(path=f'/thorchain/memo/{swap_asset.full_name}/{refnum}'),
|
||||
errmsg = f'asset/reference combination {swap_asset.full_name}/{refnum} is invalid')
|
||||
|
||||
def validate_amt(self, asset: str, amt: Decimal):
|
||||
"check that given asset/amount combination resolves to a valid reference"
|
||||
from ....swap.util import get_swap_asset, init_swap_proto
|
||||
swap_asset = get_swap_asset(self.cfg, asset, sanitize_input=True)
|
||||
return self.do_validate_amt(
|
||||
swap_asset,
|
||||
init_swap_proto(self.cfg, swap_asset).coin_amt(amt))
|
||||
|
||||
def do_validate_amt(self, asset, amt):
|
||||
int_amt = amt.to_unit('atomic')
|
||||
return process_response(
|
||||
self.rest_api.get(path=f'/thorchain/memo/check/{asset.full_name}/{int_amt}'),
|
||||
errmsg = f'asset/amount combination {asset.full_name}/{int_amt} is invalid')
|
||||
|
||||
def tx_info(self, txid: str):
|
||||
"get information for a transaction by TxID"
|
||||
return process_response(
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class Base(TxBase):
|
|||
o = self.txobj
|
||||
b = tx.body.messages[0].body
|
||||
|
||||
if self.is_swap:
|
||||
if self.is_swap or self.is_deposit:
|
||||
from_k, amt_k = ('signer', 'coins')
|
||||
if b.memo != self.swap_memo:
|
||||
raise ValueError(f'{b.memo}: invalid swap memo in serialized data')
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class TxInfo(VmTxInfo, TxInfo):
|
|||
def format_body(self, blockcount, nonmm_str, max_mmwid, enl, *, terse, sort):
|
||||
tx = self.tx
|
||||
t = tx.txobj
|
||||
has_memo = tx.is_swap
|
||||
has_memo = tx.is_swap or tx.is_deposit
|
||||
fs = """
|
||||
From: {f}{f_mmid}
|
||||
Amount: {a} {c}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,14 @@ from .base import Base
|
|||
|
||||
class New(VmNew, Base, TxBase.New):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, refreq_src=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.usr_fee = None if self.cfg.fee is None else self.proto.coin_amt(self.cfg.fee)
|
||||
if refreq_src:
|
||||
d = refreq_src.__dict__
|
||||
self.__dict__ |= {k: d[k] for k in self.swap_attrs if k in d and k != 'swap_recv_addr_mmid'}
|
||||
self.is_refreq = True
|
||||
self.swap_send_asset_spec = refreq_src.send_asset.name
|
||||
|
||||
async def get_funds_remaining(self, fee, outputs_sum, start_fee_desc):
|
||||
res = await self.twctl.get_balance(self.inputs[0].addr)
|
||||
|
|
|
|||
|
|
@ -25,11 +25,13 @@ class Signed(Completed, TxBase.Signed):
|
|||
b = tx.body.messages[0].body
|
||||
i = tx.authInfo
|
||||
|
||||
from_k, amt_k = ('signer', 'coins') if self.is_swap else ('fromAddress', 'amount')
|
||||
msg_dep = self.is_swap or self.is_deposit
|
||||
|
||||
from_k, amt_k = ('signer', 'coins') if msg_dep else ('fromAddress', 'amount')
|
||||
|
||||
self.txobj = {
|
||||
'from': self.proto.encode_addr_bech32x(getattr(b, from_k)),
|
||||
'to': None if self.is_swap else self.proto.encode_addr_bech32x(b.toAddress),
|
||||
'to': None if msg_dep else self.proto.encode_addr_bech32x(b.toAddress),
|
||||
'amt': self.proto.coin_amt(int(getattr(b, amt_k)[0].amount), from_unit='satoshi'),
|
||||
'fee': parse_fee(self.proto, tx),
|
||||
'gas': NonNegativeInt(i.fee.gasLimit),
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ class Unsigned(VmUnsigned, Completed, TxBase.Unsigned):
|
|||
if self.is_swap:
|
||||
from .protobuf import swap_tx_parms as tx_parms
|
||||
add_parms = {'memo': self.swap_memo}
|
||||
elif self.is_deposit:
|
||||
from .protobuf import swap_tx_parms as tx_parms
|
||||
add_parms = {'memo': self.swap_memo, 'trade': False, 'synth': False}
|
||||
else:
|
||||
from .protobuf import send_tx_parms as tx_parms
|
||||
add_parms = {'to_addr': o['to']}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ class New:
|
|||
return
|
||||
case 2 if self.is_swap:
|
||||
process_data_arg(cmd_args[1])
|
||||
case 2 if cmd_args[0].startswith('deposit,'):
|
||||
process_data_arg(cmd_args[1])
|
||||
self.is_deposit = True
|
||||
case n:
|
||||
die(1, f'{n} output{suf(n)} specified, but VM transactions must have exactly one')
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@ def quote_client(tx, amt):
|
|||
from .quote import ThornodeQuote
|
||||
return ThornodeQuote(tx, amt)
|
||||
|
||||
def reference_memo(send_asset, memo):
|
||||
from .memoless import THORChainReferenceMemo
|
||||
return THORChainReferenceMemo(send_asset, memo)
|
||||
|
||||
def memoless_amt(amt, refnum=None, *, round_down=False):
|
||||
from .memoless import THORChainMemolessAmount
|
||||
return THORChainMemolessAmount(amt, refnum=refnum, round_down=round_down)
|
||||
|
||||
from .cfg import THORChainSwapCfg as SwapCfg
|
||||
|
||||
from .asset import THORChainSwapAsset as SwapAsset
|
||||
|
|
|
|||
|
|
@ -39,6 +39,21 @@ class THORChainMemo:
|
|||
pat = r'^(' + '|'.join('|'.join(pats) for pats in ops.values()) + r'):\S\S+'
|
||||
return bool(re.search(pat.encode(), bytes_data))
|
||||
|
||||
@classmethod
|
||||
def digest(cls, parsed_memo):
|
||||
"""
|
||||
Produce a human-readable digest from a parsed memo
|
||||
"""
|
||||
m = parsed_memo
|
||||
return '{} {} {} {} {}/{}/{}'.format(
|
||||
m.proto,
|
||||
m.function,
|
||||
m.asset.full_name,
|
||||
m.address,
|
||||
m.trade_limit,
|
||||
m.stream_interval,
|
||||
m.stream_quantity)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, s):
|
||||
"""
|
||||
|
|
|
|||
53
mmgen/swap/proto/thorchain/memoless.py
Normal file
53
mmgen/swap/proto/thorchain/memoless.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# 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
|
||||
|
||||
"""
|
||||
swap.proto.thorchain.memoless: THORChain swap protocol memoless classes for the MMGen Wallet suite
|
||||
"""
|
||||
|
||||
from ....amt import CoinAmt, TokenAmt
|
||||
|
||||
class THORChainReferenceMemo:
|
||||
|
||||
pfx = 'REFERENCE'
|
||||
|
||||
def __init__(self, send_asset, memo):
|
||||
self.send_asset = send_asset
|
||||
self.memo = memo
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.pfx}:{self.send_asset.full_name}:{self.memo}'
|
||||
|
||||
def parse(self, s):
|
||||
from .asset import THORChainSwapAsset
|
||||
_, name, self.memo = s.split(':', 2)
|
||||
self.asset = THORChainSwapAsset(name, 'send', sanitize_input=True)
|
||||
|
||||
class THORChainMemolessAmount:
|
||||
|
||||
mod = 100000
|
||||
|
||||
def __init__(self, amt, refnum=None, *, round_down=False):
|
||||
assert isinstance(amt, CoinAmt), 'amount must be CoinAmt instance'
|
||||
if refnum:
|
||||
adj = lambda diff: diff - (
|
||||
(self.mod if diff > 0 else 0) if round_down else
|
||||
(0 if abs(diff) <= self.mod // 2 else self.mod if diff > 0 else -self.mod))
|
||||
from ....util import is_int
|
||||
assert is_int(refnum) and refnum < self.mod, f'refnum must be an integer < {self.mod}'
|
||||
orig_amt = amt.to_unit('atomic')
|
||||
self.int_amt = orig_amt + adj(refnum - orig_amt % self.mod)
|
||||
self.amt = type(amt)(
|
||||
self.int_amt,
|
||||
from_unit = 'atomic',
|
||||
decimals = amt.max_prec if isinstance(amt, TokenAmt) else None)
|
||||
self.refnum = refnum
|
||||
else:
|
||||
self.int_amt = amt.to_unit('atomic')
|
||||
self.amt = amt
|
||||
self.refnum = self.int_amt % self.mod
|
||||
|
|
@ -31,3 +31,13 @@ def get_swap_asset(cfg, asset, *, direction='send', sanitize_input=False):
|
|||
"given an asset symbol, and optionally direction, return a swap asset instance"
|
||||
assert isinstance(asset, str), '‘asset’ must be a string'
|
||||
return get_swap_proto_mod(cfg.swap_proto).SwapAsset(asset, direction, sanitize_input=sanitize_input)
|
||||
|
||||
def get_coin_amt(cfg, asset, amt):
|
||||
"given an asset symbol and amount (as string), return a coin amount instance"
|
||||
from ..amt import TokenAmt
|
||||
assert isinstance(asset, str), '‘asset’ must be a string'
|
||||
assert isinstance(amt, str), '‘amt’ must be a string'
|
||||
swap_asset = get_swap_asset(cfg, asset, sanitize_input=True)
|
||||
return (
|
||||
TokenAmt(amt, decimals=swap_asset.decimals) if swap_asset.tokensym else
|
||||
init_swap_proto(cfg, swap_asset).coin_amt(amt))
|
||||
|
|
|
|||
30
mmgen/tool/amt.py
Normal file
30
mmgen/tool/amt.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# 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
|
||||
|
||||
"""
|
||||
tool.amt: coin amount-related commands for the 'mmgen-tool' utility
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from .common import tool_cmd_base
|
||||
|
||||
class tool_cmd(tool_cmd_base):
|
||||
"coin amount utilities"
|
||||
|
||||
need_proto = True
|
||||
|
||||
def memoless_amt(self, asset: str, amt: Decimal, refnum: int):
|
||||
"create a memoless swap amount from a given asset, amount and reference number"
|
||||
|
||||
assert self.cfg.coin == 'BTC', '--coin arg not supported for this command'
|
||||
|
||||
from ..swap.proto.thorchain.memoless import THORChainMemolessAmount
|
||||
from ..swap.util import get_coin_amt
|
||||
|
||||
return str(THORChainMemolessAmount(get_coin_amt(self.cfg, asset, amt), refnum).amt)
|
||||
|
|
@ -79,16 +79,22 @@ class Base(MMGenObject):
|
|||
chain = None
|
||||
signed = False
|
||||
is_swap = False
|
||||
is_deposit = False
|
||||
is_refreq = False
|
||||
is_memoless = False
|
||||
is_compat = False
|
||||
has_comment = True
|
||||
swap_attrs = {
|
||||
'swap_proto': None,
|
||||
'swap_quote_expiry': None,
|
||||
'swap_send_asset_spec': None,
|
||||
'swap_recv_addr_mmid': None,
|
||||
'swap_recv_asset_spec': None,
|
||||
'swap_memo': None,
|
||||
'token_vault_addr': None, # nosec # B105
|
||||
'serialized2': None,
|
||||
'is_memoless': None,
|
||||
'refnum': None,
|
||||
'coin_txid2': CoinTxID}
|
||||
file_format = 'json'
|
||||
non_mmgen_inputs_msg = f"""
|
||||
|
|
|
|||
|
|
@ -10,10 +10,21 @@
|
|||
tx.base_swap: base swap transaction mixin class
|
||||
"""
|
||||
|
||||
from ..util import die, cached_property
|
||||
from ..util import ymsg, die, cached_property
|
||||
from ..addr import CoinAddr
|
||||
|
||||
class BaseSwapMethods:
|
||||
data_desc = 'swap transaction data'
|
||||
|
||||
@property
|
||||
def data_desc(self):
|
||||
def gen():
|
||||
if self.is_memoless:
|
||||
yield 'memoless'
|
||||
yield 'swap'
|
||||
if self.is_refreq:
|
||||
yield 'reference request'
|
||||
yield 'transaction data'
|
||||
return ' '.join(gen())
|
||||
|
||||
@property
|
||||
def vault_idx(self):
|
||||
|
|
@ -35,7 +46,10 @@ class BaseSwapMethods:
|
|||
|
||||
@cached_property
|
||||
def send_asset(self):
|
||||
spec = self.proto.coin + (f'.{self.proto.tokensym}' if self.proto.tokensym else '')
|
||||
spec = getattr(
|
||||
self,
|
||||
'swap_send_asset_spec',
|
||||
self.proto.coin + (f'.{self.proto.tokensym}' if self.proto.tokensym else ''))
|
||||
return self.swap_proto_mod.SwapAsset(spec, 'send')
|
||||
|
||||
@cached_property
|
||||
|
|
@ -52,6 +66,14 @@ class BaseSwapMethods:
|
|||
|
||||
def check_swap_network(self):
|
||||
rpc = self.swap_proto_rpc
|
||||
if self.is_memoless:
|
||||
if not self.cfg.test_suite:
|
||||
die(2, 'Memoless swaps are currently disabled for mainnet in MMGen Wallet')
|
||||
self.mimir_data = rpc.mimir()
|
||||
fs = f'Memoless swaps are {{}} on the {self.swap_proto_mod.name} network'
|
||||
if self.mimir_data['HALTMEMOLESS']:
|
||||
die(2, fs.format('halted'))
|
||||
|
||||
data = rpc.inbound_addrs()
|
||||
for asset in (self.send_asset, self.recv_asset):
|
||||
if asset.name != 'RUNE':
|
||||
|
|
@ -61,3 +83,22 @@ class BaseSwapMethods:
|
|||
('halted', f'halted for asset {asset.name}')):
|
||||
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):
|
||||
from ..swap.util import init_swap_proto
|
||||
from ..tw.ctl import TwCtl
|
||||
pm = self.swap_proto_mod.Memo.parse(data['memo'])
|
||||
if pm.asset.name != recv_asset_spec:
|
||||
die(2,
|
||||
f'receive protocol mismatch: {pm.asset.name!r} (registered memo) '
|
||||
f'!= {recv_asset_spec!r} (cmdline)')
|
||||
pm_proto = init_swap_proto(self.cfg, pm.asset)
|
||||
ra = await (await TwCtl(self.cfg, pm_proto, mode='w')).resolve_address(pm.address)
|
||||
assert ra, f'unable to resolve address from registered memo {pm.address!r}'
|
||||
if recv_twmmid and recv_twmmid != ra.twmmid:
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -84,7 +84,11 @@ class Bump(Completed, NewSwap):
|
|||
self.rel_fee_disp))
|
||||
|
||||
if self.is_swap:
|
||||
self.recv_proto = self.check_swap_memo().proto
|
||||
if self.is_memoless:
|
||||
from ..swap.util import init_swap_proto
|
||||
self.recv_proto = init_swap_proto(self.cfg, self.recv_asset)
|
||||
else:
|
||||
self.recv_proto = self.check_swap_memo().proto
|
||||
self.swap_cfg = self.swap_proto_mod.SwapCfg(self.cfg)
|
||||
fee_hint = await self.update_vault_output(self.send_amt)
|
||||
else:
|
||||
|
|
@ -96,6 +100,9 @@ class Bump(Completed, NewSwap):
|
|||
|
||||
self.bump_fee(output_idx, self.usr_fee)
|
||||
|
||||
if self.is_memoless and output_idx == self.vault_idx:
|
||||
self.update_vault_memoless_amt(round_down=True)
|
||||
|
||||
assert self.fee <= self.proto.max_tx_fee
|
||||
|
||||
if not self.cfg.yes:
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class Completed(Base):
|
|||
|
||||
self.infile = filename
|
||||
|
||||
if self.is_swap:
|
||||
if self.is_swap or self.is_refreq:
|
||||
from .base_swap import BaseSwapMethods as m
|
||||
for k in m.__dict__:
|
||||
if not k.startswith('_'):
|
||||
|
|
@ -87,7 +87,8 @@ class Completed(Base):
|
|||
from ..protocol import init_proto
|
||||
text = memo_bytes.decode('ascii')
|
||||
p = Memo.parse(text)
|
||||
r = self.recv_asset
|
||||
# NB: short-circuit asset match test for deposit TX (nothing to match against)
|
||||
r = self.recv_asset if self.is_swap else p.asset
|
||||
assert p.function == 'SWAP', f'‘{p.function}’: unsupported function in swap memo ‘{text}’'
|
||||
assert p.asset.name == r.name, f'invalid memo: {p.asset.name} != {r.name}'
|
||||
proto = init_proto(
|
||||
|
|
@ -97,13 +98,15 @@ class Completed(Base):
|
|||
need_amt = True)
|
||||
if mmid := getattr(self, 'swap_recv_addr_mmid', None):
|
||||
pass
|
||||
elif not self.is_swap:
|
||||
pass
|
||||
elif self.cfg.allow_non_wallet_swap:
|
||||
ymsg('Warning: allowing swap to non-wallet address (--allow-non-wallet-swap)')
|
||||
else:
|
||||
raise ValueError('Swap to non-wallet address forbidden (override with --allow-non-wallet-swap)')
|
||||
return self.Output(proto, addr=p.address, mmid=mmid, amt=proto.coin_amt('0'))
|
||||
|
||||
if self.is_swap:
|
||||
if self.is_swap and not self.is_memoless:
|
||||
raise ValueError('missing or invalid memo in swap transaction')
|
||||
|
||||
def die_on_version_error(self, errmsg): # overridden by test.overlay.fakemods
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ class MMGenTxFile(MMGenObject):
|
|||
'comment': MMGenTxComment,
|
||||
'coin_txid': CoinTxID,
|
||||
'sent_timestamp': None,
|
||||
'is_swap': None}
|
||||
'is_deposit': None,
|
||||
'is_swap': None,
|
||||
'is_refreq': None}
|
||||
|
||||
def __init__(self, tx):
|
||||
self.tx = tx
|
||||
|
|
@ -116,7 +118,7 @@ class MMGenTxFile(MMGenObject):
|
|||
if k in data:
|
||||
setattr(tx, k, v(data[k]) if v else data[k])
|
||||
|
||||
if tx.is_swap:
|
||||
if tx.is_swap or tx.is_deposit:
|
||||
for k, v in tx.swap_attrs.items():
|
||||
if k in data:
|
||||
setattr(tx, k, v(data[k]) if v else data[k])
|
||||
|
|
@ -288,10 +290,10 @@ class MMGenTxFile(MMGenObject):
|
|||
'outputs': [{k: v for k,v in e._asdict().items()
|
||||
if not (type(v) is bool and v is False)} for e in tx.outputs]
|
||||
} | {
|
||||
k: getattr(tx, k) for k in self.extra_attrs if getattr(tx, k)
|
||||
k: getattr(tx, k) for k in self.extra_attrs if getattr(tx, k, None)
|
||||
} | ({
|
||||
k: getattr(tx, k) for k in tx.swap_attrs if getattr(tx, k, None)
|
||||
} if tx.is_swap else {}))
|
||||
} if tx.is_swap or tx.is_deposit else {}))
|
||||
return '{{"{}":{},"chksum":"{}"}}'.format(self.data_label, data, make_chksum_6(data))
|
||||
|
||||
fmt_data = {'json': format_data_json, 'legacy': format_data_legacy}[tx.file_format]()
|
||||
|
|
|
|||
|
|
@ -70,9 +70,10 @@ class TxInfo:
|
|||
if tx.coin_txid:
|
||||
yield f' {tx.coin} TxID: {tx.coin_txid.hl()}\n'
|
||||
|
||||
if tx.is_swap:
|
||||
if tx.is_swap and not tx.is_memoless:
|
||||
from ..swap.proto.thorchain import Memo, name
|
||||
data = (
|
||||
tx.memoless_memo.encode() if tx.is_memoless else
|
||||
tx.swap_memo.encode() if tx.proto.is_vm else
|
||||
tx.data_output.data)
|
||||
if Memo.is_partial_memo(data):
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ class New(Base):
|
|||
|
||||
coin_addr, mmid, is_vault = (None, None, False)
|
||||
|
||||
if arg == 'vault' and self.is_swap: # ‘is_swap’ test necessary because could be bump tx
|
||||
if (arg == 'vault' and self.is_swap) or (arg == 'deposit' and self.is_deposit):
|
||||
is_vault = True
|
||||
elif mmid := get_obj(MMGenID, proto=proto, id_str=arg, silent=True):
|
||||
coin_addr = mmaddr2coinaddr(self.cfg, arg, ad_w, ad_f, proto)
|
||||
|
|
@ -473,6 +473,8 @@ class New(Base):
|
|||
cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(args)
|
||||
if self.is_swap:
|
||||
cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args)
|
||||
if cmd_args is None: # memoless
|
||||
return self
|
||||
if self.is_compat:
|
||||
await self.process_cmdline_args(cmd_args, None, None)
|
||||
else:
|
||||
|
|
@ -497,6 +499,9 @@ class New(Base):
|
|||
from ..ui import do_license_msg
|
||||
do_license_msg(self.cfg)
|
||||
|
||||
if self.is_memoless and self.refnum and self.vault_output.amt:
|
||||
self.update_vault_memoless_amt()
|
||||
|
||||
if not (self.is_bump or self.cfg.inputs):
|
||||
await self.twuo.view_filter_and_sort()
|
||||
|
||||
|
|
@ -539,6 +544,8 @@ class New(Base):
|
|||
if not self.is_compat:
|
||||
self.check_non_mmgen_inputs(caller=caller)
|
||||
self.update_change_output(funds_left)
|
||||
if self.is_memoless and self.refnum:
|
||||
self.update_vault_memoless_amt(round_down=len(self.outputs) == 1)
|
||||
self.check_chg_addr_is_wallet_addr()
|
||||
|
||||
if self.has_comment and not self.cfg.yes:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import sys, time
|
|||
from collections import namedtuple
|
||||
|
||||
from ..amt import UniAmt
|
||||
from ..util import msg, ymsg, die
|
||||
from ..util import msg, ymsg, is_int, die
|
||||
from ..swap.util import get_swap_asset, init_swap_proto
|
||||
|
||||
from .base_swap import BaseSwapMethods
|
||||
|
|
@ -30,11 +30,16 @@ def get_send_proto(cfg):
|
|||
class NewSwap(BaseSwapMethods, New):
|
||||
desc = 'swap transaction'
|
||||
swap_quote_refresh_timeout = 30
|
||||
memoless_tx_fee = '0.2' # RUNE
|
||||
_output = namedtuple('swap_output', ['coin', 'network', 'addr', 'mmid'])
|
||||
|
||||
def __init__(self, *, cfg, **kwargs):
|
||||
def __init__(self, *, cfg, is_memoless=False, **kwargs):
|
||||
assert is_int(cfg.refnum) or cfg.refnum is None, '--refnum must be an integer'
|
||||
self.refnum = int(cfg.refnum) if is_int(cfg.refnum) else None
|
||||
self.swap_proto = cfg.swap_proto
|
||||
self.is_swap = True
|
||||
self.is_memoless = bool(is_memoless)
|
||||
self.is_refreq = self.is_memoless and not self.refnum
|
||||
super().__init__(cfg=cfg, **kwargs)
|
||||
|
||||
def check_addr_is_wallet_addr(self, output, *, message):
|
||||
|
|
@ -65,6 +70,17 @@ 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
|
||||
|
|
@ -126,6 +142,12 @@ class NewSwap(BaseSwapMethods, New):
|
|||
|
||||
await parse()
|
||||
|
||||
if self.refnum:
|
||||
recv_output, parsed_ref_memo = 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)
|
||||
|
||||
self.swap_cfg = self.swap_proto_mod.SwapCfg(self.cfg)
|
||||
|
||||
if self.cfg.quote:
|
||||
|
|
@ -148,11 +170,12 @@ class NewSwap(BaseSwapMethods, New):
|
|||
if args.send_amt and not (chg_output or self.proto.is_vm):
|
||||
chg_output = await self.get_chg_output(None, addrfiles)
|
||||
|
||||
recv_output = await self.get_swap_output(
|
||||
self.recv_proto,
|
||||
args.recv_spec,
|
||||
addrfiles,
|
||||
'destination')
|
||||
if not self.refnum:
|
||||
recv_output = await self.get_swap_output(
|
||||
self.recv_proto,
|
||||
args.recv_spec,
|
||||
addrfiles,
|
||||
'destination')
|
||||
|
||||
self.check_addr_is_wallet_addr(
|
||||
recv_output,
|
||||
|
|
@ -171,15 +194,37 @@ 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)
|
||||
|
||||
# this goes into the transaction file:
|
||||
self.swap_recv_addr_mmid = recv_output.mmid
|
||||
|
||||
if self.is_refreq:
|
||||
if memo.trade_limit:
|
||||
ymsg(
|
||||
'Warning: setting a trade limit on a memoless transaction is dangerous!\n'
|
||||
'On the second pass, both amount and trade limit must remain unchanged!')
|
||||
from ..rpc import rpc_init
|
||||
if not args.send_amt:
|
||||
die(1, 'With --memoless first pass, you must supply a send amount')
|
||||
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(
|
||||
self.send_asset,
|
||||
str(self.update_memo(self.memoless_memo, qd.trade_limit)))
|
||||
return None
|
||||
|
||||
ret = (
|
||||
[f'vault,{args.send_amt}'] if args.send_amt and self.proto.is_vm else
|
||||
[f'vault,{args.send_amt}', chg_output.mmid] if args.send_amt else
|
||||
['vault'])
|
||||
|
||||
return tuple(ret + [f'data:{memo}'])
|
||||
return tuple(ret if self.is_memoless else (ret + [f'data:{memo}']))
|
||||
|
||||
async def display_quote_loop(self, qd, *, deduct_est_fee=False):
|
||||
from ..term import get_char
|
||||
|
|
@ -199,6 +244,14 @@ class NewSwap(BaseSwapMethods, New):
|
|||
await self.set_gas(to_addr=qd.router if self.is_token else None)
|
||||
msg(await qd.format_quote(deduct_est_fee=deduct_est_fee, skip_est_fee=skip_est_fee))
|
||||
|
||||
def update_vault_memoless_amt(self, round_down=False):
|
||||
self.update_output_amt(
|
||||
self.vault_idx,
|
||||
self.swap_proto_mod.memoless_amt(
|
||||
self.outputs[self.vault_idx].amt,
|
||||
self.refnum,
|
||||
round_down = round_down).amt)
|
||||
|
||||
def update_vault_addr(self, qd, *, addr='inbound_address'):
|
||||
vault_idx = self.vault_idx
|
||||
assert vault_idx == 0, f'{vault_idx}: vault index is not zero!'
|
||||
|
|
@ -219,6 +272,10 @@ class NewSwap(BaseSwapMethods, New):
|
|||
await self.display_quote_loop(qd, deduct_est_fee=deduct_est_fee)
|
||||
self.swap_quote_expiry = qd.data['expiry']
|
||||
self.update_vault_addr(qd)
|
||||
self.update_data_output(qd.trade_limit)
|
||||
if self.is_memoless:
|
||||
if not self.is_bump:
|
||||
self.memoless_memo = str(self.update_memo(self.memoless_memo, qd.trade_limit))
|
||||
else:
|
||||
self.update_data_output(qd.trade_limit)
|
||||
self.quote_data = qd
|
||||
return qd.rel_fee_hint
|
||||
|
|
|
|||
|
|
@ -32,6 +32,18 @@ class OnlineSigned(Signed):
|
|||
from . import _base_proto_subclass
|
||||
return _base_proto_subclass('Status', 'status', proto=self.proto)(self)
|
||||
|
||||
async def check_memoless_presend(self):
|
||||
data = self.swap_proto_rpc.do_validate_amt(self.send_asset, self.vault_output.amt)
|
||||
if data['reference'] != self.refnum:
|
||||
die(3, f'registered reference number ({data["reference"]}) does not match ours ({self.refnum})')
|
||||
if not data['available']:
|
||||
die(3, f'reference number {self.refnum} is not available')
|
||||
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)
|
||||
|
||||
def check_swap_expiry(self):
|
||||
from ..util2 import format_elapsed_hr
|
||||
expiry = self.swap_quote_expiry
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ test.cmdtest_d.httpd.thornode.rpc: Thornode RPC HTTP server
|
|||
import re, json
|
||||
from wsgiref.util import request_uri
|
||||
|
||||
from mmgen.util import is_int
|
||||
from mmgen.cfg import Config
|
||||
|
||||
from . import ThornodeServer
|
||||
|
||||
def get_inbounds_info():
|
||||
|
|
@ -52,6 +55,10 @@ class ThornodeRPCServer(ThornodeServer):
|
|||
|
||||
inbounds_info = get_inbounds_info()
|
||||
|
||||
var_actions = {
|
||||
'halt_memoless': ('mimir_info', 'HALTMEMOLESS'),
|
||||
'memoless_tx_fee': ('mimir_info', 'MEMOLESSTXNCOST')}
|
||||
|
||||
chain_var_actions = {
|
||||
'ltc_global_paused': ('inbounds_info', 'LTC', 'global_trading_paused'),
|
||||
'ltc_paused': ('inbounds_info', 'LTC', 'chain_trading_paused'),
|
||||
|
|
@ -117,6 +124,39 @@ class ThornodeRPCServer(ThornodeServer):
|
|||
def lastblock(m, length):
|
||||
return self.lastblock_info
|
||||
|
||||
def get_refnum(m, length):
|
||||
return self.memo_info[m[1].lower()[:8]] # 0c7454c0
|
||||
|
||||
def validate_refnum(m, length):
|
||||
mi = self.memo_info['0c7454c0']
|
||||
assert m[1] == mi['asset']
|
||||
assert m[2] == mi['reference']
|
||||
return mi
|
||||
|
||||
def validate_amt(m, length):
|
||||
mi = self.memo_info['0c7454c0']
|
||||
assert m[1] == mi['asset']
|
||||
assert is_int(m[2])
|
||||
|
||||
from mmgen.swap.util import get_swap_asset, init_swap_proto # pylint: disable=all
|
||||
from mmgen.swap.proto.thorchain.memoless import THORChainMemolessAmount # pylint: disable=all
|
||||
|
||||
cfg = Config()
|
||||
swap_asset = get_swap_asset(cfg, m[1], sanitize_input=True)
|
||||
amt = init_swap_proto(cfg, swap_asset).coin_amt(int(m[2]), from_unit='atomic')
|
||||
|
||||
a = THORChainMemolessAmount(amt)
|
||||
assert a.refnum == int(mi['reference']), f'{a.refnum} != {int(mi["reference"])}'
|
||||
|
||||
return {
|
||||
'reference': a.refnum,
|
||||
'available': True,
|
||||
'can_register': True,
|
||||
'expires_at': 9999999999,
|
||||
'memo': mi['memo'],
|
||||
'usage_count': 0,
|
||||
'max_use': 1} # 0 = unlimited
|
||||
|
||||
def inbound_addrs(m, length):
|
||||
return self.inbounds_info
|
||||
|
||||
|
|
@ -125,6 +165,9 @@ class ThornodeRPCServer(ThornodeServer):
|
|||
('mimir', 'GET', r'/thorchain/mimir'),
|
||||
('lastblock', 'GET', r'/thorchain/lastblock(.*)'),
|
||||
('inbound_addrs', 'GET', r'/thorchain/inbound_addresses'),
|
||||
('validate_amt', 'GET', r'/thorchain/memo/check/(\S+)/(\S+)'),
|
||||
('validate_refnum', 'GET', r'/thorchain/memo/(\S+)/(\S+)'),
|
||||
('get_refnum', 'GET', r'/thorchain/memo/(\S+)'),
|
||||
('balance', 'GET', r'/bank/balances/(\S+)'),
|
||||
('acct_info', 'GET', r'/auth/accounts/(\S+)'),
|
||||
('tx_info', 'POST', r'/tx$'),
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@
|
|||
test.cmdtest_d.swap: asset swap tests for the cmdtest.py test suite
|
||||
"""
|
||||
|
||||
import json
|
||||
import os, json
|
||||
|
||||
from mmgen.cfg import Config
|
||||
from mmgen.util import make_chksum_6
|
||||
from mmgen.util import make_chksum_6, cached_property
|
||||
from mmgen.protocol import init_proto
|
||||
from mmgen.wallet.mmgen import wallet as MMGenWallet
|
||||
from mmgen.tx.file import txfile_json_dumps # pylint: disable=no-name-in-module
|
||||
|
|
@ -21,10 +21,11 @@ from mmgen.tx.file import txfile_json_dumps # pylint: disable=no-name-in-module
|
|||
from ..include.common import make_burn_addr, gr_uc, imsg
|
||||
|
||||
from .include.runner import CmdTestRunner
|
||||
from .include.common import dfl_words_file
|
||||
from .include.common import cleanup_env, dfl_words_file
|
||||
|
||||
from .autosign import CmdTestAutosign, CmdTestAutosignThreaded
|
||||
from .regtest import CmdTestRegtest, dfl_wcls, rt_pw, strip_ansi_escapes
|
||||
from .ethdev import CmdTestEthdevMethods
|
||||
from .rune import CmdTestRuneMethods
|
||||
|
||||
sample1 = gr_uc[:24]
|
||||
|
|
@ -90,6 +91,7 @@ class CmdTestSwapMethods:
|
|||
reload_quote = False,
|
||||
sign_and_send = False,
|
||||
need_passphrase = True,
|
||||
memoless = False,
|
||||
menu_prompt = None,
|
||||
expect = None):
|
||||
t.expect(menu_prompt or self.menu_prompt, 'q')
|
||||
|
|
@ -105,6 +107,8 @@ class CmdTestSwapMethods:
|
|||
if expect:
|
||||
t.expect(expect)
|
||||
t.expect('to continue: ', '\n')
|
||||
if memoless:
|
||||
return t
|
||||
if sign_and_send:
|
||||
if need_passphrase:
|
||||
t.passphrase(dfl_wcls.desc, rt_pw)
|
||||
|
|
@ -297,7 +301,8 @@ class CmdTestSwap(
|
|||
CmdTestSwapMethods,
|
||||
CmdTestRuneMethods,
|
||||
CmdTestRegtest,
|
||||
CmdTestAutosignThreaded):
|
||||
CmdTestAutosignThreaded,
|
||||
CmdTestEthdevMethods):
|
||||
'swap operations (LTC <=> BCH)'
|
||||
|
||||
bdb_wallet = True
|
||||
|
|
@ -308,6 +313,8 @@ class CmdTestSwap(
|
|||
coins = ['bch', 'ltc']
|
||||
daemon_coins = ['bch', 'ltc']
|
||||
subseed_idx = '29L'
|
||||
rune_menu_prompt = 'efresh balance:\b'
|
||||
refreq_txid = '0c7454c06d3fc0e520d34283e6f491ce7afe5abb3f569bbdfea1732e04b716ec'
|
||||
|
||||
cmd_group_in = (
|
||||
('list_assets', 'listing swap assets'),
|
||||
|
|
@ -318,6 +325,7 @@ class CmdTestSwap(
|
|||
('subgroup.signsend', ['init_swap']),
|
||||
('subgroup.signsend_bad', ['init_swap']),
|
||||
('subgroup.autosign', ['signsend']),
|
||||
('subgroup.memoless', ['init_swap']),
|
||||
('stop_thornode_servers', 'stopping the Thornode RPC and swap servers'),
|
||||
('stop', 'stopping regtest daemons'))
|
||||
|
||||
|
|
@ -370,6 +378,7 @@ class CmdTestSwap(
|
|||
('swaptxcreate_neterror1', 'creating a swap transaction (network error, trading paused)'),
|
||||
('swaptxcreate_neterror2', 'creating a swap transaction (network error, trading for coin paused)'),
|
||||
('swaptxcreate_neterror3', 'creating a swap transaction (network error, trading for coin halted)'),
|
||||
('swaptxcreate_neterror4', 'creating a swap transaction (network error, memoless swaps halted)'),
|
||||
),
|
||||
'signsend': (
|
||||
'Swap TX create, sign and send operations (LTC => BCH)',
|
||||
|
|
@ -416,7 +425,45 @@ class CmdTestSwap(
|
|||
('autosign_swaptxsend2', 'sending the transaction'),
|
||||
('generate_ltc', 'generating a block'),
|
||||
('swap_bal3', 'checking the balance'),
|
||||
('wait_loop_kill', 'stopping autosign wait loop'))
|
||||
('wait_loop_kill', 'stopping autosign wait loop'),
|
||||
),
|
||||
'memoless': (
|
||||
'Memoless swap TX operations (LTC => BCH)',
|
||||
('rune_addrgen', 'generating RUNE addresses'),
|
||||
('rune_addrimport', 'importing RUNE addresses'),
|
||||
('rune_bal_refresh', 'refreshing balances in RUNE tracking wallet'),
|
||||
('memoless_swaptxcreate', 'creating a memoless swap transaction (--no-refreq-tx)'),
|
||||
('rune_txcreate_refreq', 'creating a rune reference request transaction'),
|
||||
('rune_txsign_refreq', 'signing the transaction'),
|
||||
('rune_txsend_refreq', 'sending the transaction'),
|
||||
('refreq_swaptxcreate', 'creating a memoless swap transaction (first pass)'),
|
||||
('refreq_swaptxsign', 'signing the transaction'),
|
||||
('refreq_swaptxsend', 'sending the transaction'),
|
||||
('remote_get_refnum', 'getting the reference number'),
|
||||
('refnum_swaptxcreate1', 'creating a memoless swap transaction (second pass)'),
|
||||
('refnum_swaptxsign1', 'signing the transaction'),
|
||||
('refnum_swaptxsend1', 'sending the transaction'),
|
||||
('refnum_swaptxbump1', 'bumping the transaction'),
|
||||
('refnum_swaptxsign2', 'signing the bumped transaction'),
|
||||
('refnum_swaptxsend2', 'sending the bumped transaction'),
|
||||
('refnum_swaptxcreate2', 'creating a memoless swap transaction (second pass, no chg)'),
|
||||
('refnum_swaptxsign3', 'signing the transaction'),
|
||||
('refnum_swaptxbump2', 'bumping the transaction'),
|
||||
('refnum_swaptxsign4', 'signing the bumped transaction'),
|
||||
('refnum_swaptxsend3', 'sending the bumped transaction'),
|
||||
('remote_get_refnum_color', 'getting the reference number (color output)'),
|
||||
('remote_validate_refnum', 'validating the reference number'),
|
||||
('remote_validate_refnum_short', 'validating the reference number (short form for asset)'),
|
||||
('remote_get_inbound_ltc', 'getting inbound address (LTC)'),
|
||||
('tool_get_memoless_amt', 'getting the memoless amount (LTC)'),
|
||||
('remote_validate_amt', 'validating the memoless amount'),
|
||||
('remote_validate_amt_short', 'validating the memoless amount (short form for asset)'),
|
||||
('txcreate_memoless_amt', 'creating a transaction sending the memoless amount'),
|
||||
('txsign_memoless_amt', 'signing the transaction'),
|
||||
('txsend_memoless_amt', 'sending the transaction'),
|
||||
('generate_ltc', 'generating a block'),
|
||||
('bob_bal_memoless', 'displaying Bob’s balance (LTC)'),
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, cfg, trunner, cfgs, spawn):
|
||||
|
|
@ -430,6 +477,8 @@ class CmdTestSwap(
|
|||
self.protos = [init_proto(cfg, k, network='regtest', need_amt=True) for k in ('ltc', 'bch')]
|
||||
|
||||
self.opts.append('--bob')
|
||||
self.rune_opts = [f'--outdir={self.tmpdir}', '--regtest=1', '--bob', '--quiet']
|
||||
self.eth_opts = self.rune_opts
|
||||
|
||||
self.start_thornode_servers()
|
||||
|
||||
|
|
@ -437,6 +486,10 @@ class CmdTestSwap(
|
|||
def sid(self):
|
||||
return self._user_sid('bob')
|
||||
|
||||
@cached_property
|
||||
def subsid(self):
|
||||
return self._get_user_subsid('bob', self.subseed_idx)
|
||||
|
||||
def list_assets(self):
|
||||
t = self.spawn('mmgen-swaptxcreate', ['--list-assets'])
|
||||
t.expect('AVAILABLE')
|
||||
|
|
@ -612,6 +665,9 @@ class CmdTestSwap(
|
|||
def swaptxcreate_neterror3(self):
|
||||
return self._swaptxcreate_neterror('ltc_halted', 'Trading is halted for asset')
|
||||
|
||||
def swaptxcreate_neterror4(self):
|
||||
return self._swaptxcreate_neterror('halt_memoless', 'Memoless swaps are halted', add_opts=['--memoless'])
|
||||
|
||||
def _swaptxcreate_neterror(self, key, expect_str, *, add_opts=[]):
|
||||
if key:
|
||||
self.rpc_server.setvar(key, True)
|
||||
|
|
@ -748,6 +804,194 @@ class CmdTestSwap(
|
|||
def autosign_swaptxsend2(self):
|
||||
return self._user_txsend('bob', need_rbf=True)
|
||||
|
||||
# memoless:
|
||||
|
||||
def rune_addrgen(self):
|
||||
return self._addrgen(coin='RUNE')
|
||||
|
||||
def rune_addrimport(self):
|
||||
return self._addrimport(add_args=['--coin=RUNE', '--bob'])
|
||||
|
||||
def rune_bal_refresh(self):
|
||||
return self._rune_bal_refresh(
|
||||
add_opts = ['--coin=rune'],
|
||||
menu_prompt = self.rune_menu_prompt)
|
||||
|
||||
def _memoless_swaptxcreate(
|
||||
self,
|
||||
args,
|
||||
*,
|
||||
add_opts = ['--memoless'],
|
||||
refreq = False,
|
||||
refnum = False):
|
||||
t = self._swaptxcreate(args, add_opts=add_opts)
|
||||
if refnum:
|
||||
return self._swaptxcreate_ui_common(t, inputs='2')
|
||||
if args[-1] == 'BCH': # no dest addr
|
||||
t.expect('(Y/n): ', 'y')
|
||||
t.expect('to continue: ', '\n')
|
||||
if refreq:
|
||||
t.expect(self.rune_menu_prompt, 'q')
|
||||
t.expect('spend from: ', '3\n')
|
||||
t.expect('(y/N): ', 'y') # add comment?
|
||||
t.expect('Comment: ', 'Memoless!\n')
|
||||
t.expect('view: ', 'y')
|
||||
t.expect('to continue: ', 'z')
|
||||
t.expect('(y/N): ', 'y') # save?
|
||||
t.written_to_file('Unsigned transaction')
|
||||
else:
|
||||
self.write_to_tmpfile('memo', t.expect_getend('MEMO: '))
|
||||
return t
|
||||
|
||||
def memoless_swaptxcreate(self):
|
||||
return self._memoless_swaptxcreate(
|
||||
['LTC', '2.345', f'{self.subsid}:B:1', 'BCH'], # no dest addr
|
||||
add_opts = ['--no-refreq'])
|
||||
|
||||
def rune_txcreate_refreq(self):
|
||||
self.get_file_with_ext('rawtx', delete_all=True)
|
||||
memo = self.read_from_tmpfile('memo')
|
||||
return self._rune_txcreate(
|
||||
args = ['deposit,0', f'data:{memo}'],
|
||||
add_opts = ['--coin=rune', '--fee=0.2'],
|
||||
menu_prompt = self.rune_menu_prompt)
|
||||
|
||||
def rune_txsign_refreq(self):
|
||||
self.get_file_with_ext('sigtx', delete_all=True)
|
||||
return self._rune_txsign()
|
||||
|
||||
def rune_txsend_refreq(self):
|
||||
return self._rune_txsend()
|
||||
|
||||
def refreq_swaptxcreate(self):
|
||||
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'],
|
||||
refreq = True)
|
||||
|
||||
refreq_swaptxsign = rune_txsign_refreq
|
||||
refreq_swaptxsend = rune_txsend_refreq
|
||||
|
||||
def remote_get_refnum(self):
|
||||
t = self._rune_remote('get_refnum', (self.refreq_txid,), color=False)
|
||||
self.write_to_tmpfile('memo_info.json', t.read().strip())
|
||||
return t
|
||||
|
||||
def _refnum_swaptxcreate(self, args, *, chg_addr=None, dest_addr=None):
|
||||
data = json.loads(self.read_from_tmpfile('memo_info.json'))
|
||||
return self._memoless_swaptxcreate(args, add_opts=[f'--refnum={data["reference"]}'], refnum=True)
|
||||
|
||||
def refnum_swaptxcreate1(self):
|
||||
return self._refnum_swaptxcreate(['LTC', '2.345', f'{self.subsid}:B:1', 'BCH', f'{self.sid}:C:2'])
|
||||
|
||||
def refnum_swaptxsign1(self):
|
||||
return self._swaptxsign()
|
||||
|
||||
def refnum_swaptxsend1(self):
|
||||
return self._swaptxsend(expect_pats=[r'Fee:.*0\.0000084'])
|
||||
|
||||
def refnum_swaptxbump1(self):
|
||||
return self._swaptxbump('100s')
|
||||
|
||||
def refnum_swaptxsign2(self):
|
||||
return self._swaptxsign()
|
||||
|
||||
def refnum_swaptxsend2(self):
|
||||
return self._swaptxsend(expect_pats=[r'Fee:.*0\.00014'])
|
||||
|
||||
def refnum_swaptxcreate2(self):
|
||||
return self._refnum_swaptxcreate(['LTC', 'BCH'])
|
||||
|
||||
def refnum_swaptxsign3(self):
|
||||
return self._swaptxsign()
|
||||
|
||||
def refnum_swaptxbump2(self):
|
||||
return self._swaptxbump('700s', single_output=True)
|
||||
|
||||
def refnum_swaptxsign4(self):
|
||||
return self._swaptxsign()
|
||||
|
||||
def refnum_swaptxsend3(self):
|
||||
# depending on whether invoked as ‘swap’ or ‘swap.memoless’, TX spends different
|
||||
# addrtype and thus has different size:
|
||||
return self._swaptxsend(expect_pats=[r'Fee:.*0\.00186421', r'Fee:.*0\.00177765'])
|
||||
|
||||
def remote_get_refnum_color(self):
|
||||
t = self._rune_remote('get_refnum', (self.refreq_txid,))
|
||||
return t
|
||||
|
||||
def remote_validate_refnum(self):
|
||||
return self._remote_validate_refnum()
|
||||
|
||||
def remote_validate_refnum_short(self):
|
||||
return self._remote_validate_refnum(asset='ltc')
|
||||
|
||||
def _remote_validate_refnum(self, *, asset=None):
|
||||
data = json.loads(self.read_from_tmpfile('memo_info.json'))
|
||||
return self._rune_remote('validate_refnum', [(asset or data['asset']), data['reference']])
|
||||
|
||||
def remote_get_inbound_ltc(self):
|
||||
t = self._rune_remote('inbound_addrs', ['coin=ltc'], color=False)
|
||||
self.write_to_tmpfile('inbound_addr_info.json', t.read().strip())
|
||||
return t
|
||||
|
||||
def tool_get_memoless_amt(self):
|
||||
return self._tool_get_memoless_amt()
|
||||
|
||||
def tool_get_memoless_amt_short(self):
|
||||
return self._tool_get_memoless_amt(asset='ltc')
|
||||
|
||||
def _tool_get_memoless_amt(self, *, asset=None):
|
||||
data = json.loads(self.read_from_tmpfile('memo_info.json'))
|
||||
t = self.spawn(
|
||||
'mmgen-tool',
|
||||
['memoless_amt', (asset or data['asset']), '0.12345', data['reference']],
|
||||
env = cleanup_env(self.cfg))
|
||||
self.write_to_tmpfile('memoless_amt', t.read().strip())
|
||||
return t
|
||||
|
||||
def remote_validate_amt(self):
|
||||
return self._remote_validate_amt()
|
||||
|
||||
def remote_validate_amt_short(self):
|
||||
return self._remote_validate_amt(asset='ltc')
|
||||
|
||||
def _remote_validate_amt(self, *, asset=None):
|
||||
data = json.loads(self.read_from_tmpfile('memo_info.json'))
|
||||
amt = self.read_from_tmpfile('memoless_amt').strip()
|
||||
return self._rune_remote('validate_amt', [(asset or data['asset']), amt])
|
||||
|
||||
def txcreate_memoless_amt(self):
|
||||
self.get_file_with_ext('rawtx', delete_all=True)
|
||||
data = json.loads(self.read_from_tmpfile('inbound_addr_info.json'))
|
||||
amt = self.read_from_tmpfile('memoless_amt').strip()
|
||||
return self.txcreate_ui_common(
|
||||
self.spawn(
|
||||
'mmgen-txcreate',
|
||||
['--coin=ltc']
|
||||
+ self.rune_opts
|
||||
+ ['{},{}'.format(data['address'], amt), 'B']),
|
||||
inputs = '1',
|
||||
auto_chg_addr = 'B',
|
||||
interactive_fee = '10s')
|
||||
|
||||
def txsign_memoless_amt(self):
|
||||
self.get_file_with_ext('sigtx', delete_all=True)
|
||||
txfile = self.get_file_with_ext('rawtx')
|
||||
return self.txsign_ui_common(
|
||||
self.spawn('mmgen-txsign', self.rune_opts + [txfile]),
|
||||
do_passwd = True,
|
||||
passwd = rt_pw)
|
||||
|
||||
def txsend_memoless_amt(self):
|
||||
txfile = self.get_file_with_ext('sigtx')
|
||||
t = self.spawn('mmgen-txsend', self.rune_opts + [txfile])
|
||||
self.txsend_ui_common(t, bogus_send=False)
|
||||
return t
|
||||
|
||||
def bob_bal_memoless(self):
|
||||
return self._bob_bal(0, None, skip_check=True)
|
||||
|
||||
# admin methods:
|
||||
|
||||
def sleep(self):
|
||||
|
|
|
|||
|
|
@ -85,7 +85,39 @@ parse_vectors = [
|
|||
memo = '=:THOR.RUNE:thor1lukwlve7hayy66qrdkp4k7sh0emjqwergy7tl3:127025748855/0/1',
|
||||
pubkey = '03da157f891abfe7822efb91f59667aa6cc6c3768a7e280caeb9ae243c969eb3e7',
|
||||
signature = 'be8324f6a1535b971d63715532e2e42ee3c35c05b81ed9b6bccd9a1765688eca'
|
||||
'4ca00a66119b07c5168c6c78e22299becc82d5f311ee79ae9183776b0dff3269'))
|
||||
'4ca00a66119b07c5168c6c78e22299becc82d5f311ee79ae9183776b0dff3269')),
|
||||
_pv(
|
||||
'mainnet-tx-msgdeposit-refreq1.binpb',
|
||||
'957e120cb0c94fb26e2601a7c9190ae038c4b30cdddd37b55674650f2082b2c2',
|
||||
swap_tx_parms(
|
||||
'thor1uszfy2dyd2rcjxxpy6wjuv450muljac6ssr70k',
|
||||
'0',
|
||||
50000000,
|
||||
147684,
|
||||
25562,
|
||||
fee = 2000000,
|
||||
synth = False,
|
||||
trade = False,
|
||||
memo = 'REFERENCE:LTC.LTC:=:e:0x7182f0983a455113d925ea5fb92b99056bb953c7:310716643/0/16:sto/uws:0/1',
|
||||
pubkey = '025fa52beedbfb8d0140cc092281c12f5d3951c444fe9ea1f3249170c9bf29c283',
|
||||
signature = 'fe62a40de37e09109297fb1e969b3b26499b6a22abd58daf50a0fd4afbb94184'
|
||||
'61ac1c3613e0c58813b5436dc04e615bd92e6af9044880405639034be4888696')),
|
||||
_pv(
|
||||
'mainnet-tx-msgdeposit-refreq2.binpb',
|
||||
'f197ff8e235097e9a086ef3e0bc49c2d03e01dae36042a759cba683e99775c40',
|
||||
swap_tx_parms(
|
||||
'thor1uszfy2dyd2rcjxxpy6wjuv450muljac6ssr70k',
|
||||
'0',
|
||||
50000000,
|
||||
147684,
|
||||
2675,
|
||||
fee = 2000000,
|
||||
synth = False,
|
||||
trade = False,
|
||||
memo = 'REFERENCE:BTC.BTC:=:l:ltc1qxahvkaz58s27q38h9qg2aykp4efwqfph6ar6mu:1016595987/1/0:sto:0',
|
||||
pubkey = '025fa52beedbfb8d0140cc092281c12f5d3951c444fe9ea1f3249170c9bf29c283',
|
||||
signature = '246ede219411b4a1f7da334f2c1c51cbb1376d43d5bd63c04d40d4b7d4ac0f2d'
|
||||
'421ce07c8acbf7aa7e61e782266a83a5468c46c2e5a232314716391bbdc9e2c8'))
|
||||
]
|
||||
|
||||
_bv = namedtuple('build_vector', ['txid', 'txid2', 'parms', 'null_fee'], defaults=[None])
|
||||
|
|
|
|||
|
|
@ -188,3 +188,148 @@ class unit_tests:
|
|||
), pfx='')
|
||||
|
||||
return True
|
||||
|
||||
def memoless(self, name, ut, desc='memoless amounts'):
|
||||
from mmgen.swap.proto.thorchain.memoless import THORChainMemolessAmount
|
||||
from mmgen.swap.util import get_coin_amt
|
||||
vecs = [
|
||||
{
|
||||
'desc': 'BTC',
|
||||
'asset': 'btc',
|
||||
'data': (
|
||||
('1.00020002', '1.00020002', 100020002, 20002, False),
|
||||
('1', '1.00049999', 100049999, 49999, False),
|
||||
('1.00020002', '1.00049999', 100049999, 49999, False),
|
||||
('1', '1.0005', 100050000, 50000, False),
|
||||
('1', '0.99950001', 99950001, 50001, False),
|
||||
('1', '0.99999999', 99999999, 99999, False),
|
||||
('1.00049999', '1.00020002', 100020002, 20002, False),
|
||||
('1.00050001', '1.00000001', 100000001, 1, False),
|
||||
('1.99999999', '2.00000001', 200000001, 1, False))
|
||||
}, {
|
||||
'desc': 'BTC round down',
|
||||
'asset': 'btc',
|
||||
'data': (
|
||||
('1.00020002', '1.00020002', 100020002, 20002, True),
|
||||
('1', '0.99949999', 99949999, 49999, True),
|
||||
('1.00020002', '0.99949999', 99949999, 49999, True),
|
||||
('1', '0.9995', 99950000, 50000, True),
|
||||
('1', '0.99950001', 99950001, 50001, True),
|
||||
('1', '0.99999999', 99999999, 99999, True),
|
||||
('1.00049999', '1.00020002', 100020002, 20002, True),
|
||||
('1.00050001', '1.00000001', 100000001, 1, True),
|
||||
('1.99999999', '1.99900001', 199900001, 1, True))
|
||||
}, {
|
||||
'desc': 'BTC misc',
|
||||
'asset': 'btc',
|
||||
'data': (
|
||||
('1', '1.00020002', 100020002, 20002, False),
|
||||
('1', '0.99920002', 99920002, 20002, True),
|
||||
('1.234', '1.23420002', 123420002, 20002, False),
|
||||
('1.234', '1.23320002', 123320002, 20002, True),
|
||||
('1.234', '1.23449999', 123449999, 49999, False),
|
||||
('1.234', '1.2345', 123450000, 50000, False),
|
||||
('1.234', '1.2335', 123350000, 50000, True),
|
||||
('1.234', '1.23350001', 123350001, 50001, False),
|
||||
('1.234', '1.23350001', 123350001, 50001, True),
|
||||
('1.234', '1.23399999', 123399999, 99999, False),
|
||||
('1.2345', '1.23449999', 123449999, 49999, False),
|
||||
('1.23456789', '1.23420002', 123420002, 20002, False),
|
||||
('1.23456789', '1.23499999', 123499999, 99999, False),
|
||||
('1.23456789', '1.23399999', 123399999, 99999, True))
|
||||
}, {
|
||||
'desc': 'ETH',
|
||||
'asset': 'eth',
|
||||
'data': (
|
||||
('1.234567800000000001', '1.234567800000000001', 1234567800000000001, 1, False),
|
||||
('1.2345678', '1.234567800000000001', 1234567800000000001, 1, False),
|
||||
('1.234567800000000001', '1.234567800000050001', 1234567800000050001, 50001, False),
|
||||
('1.2345678', '1.234567799999999999', 1234567799999999999, 99999, False),
|
||||
('1.234567899999923456', '1.234567899999900001', 1234567899999900001, 1, True),
|
||||
('1.234567899999960001', '1.234567899999910001', 1234567899999910001, 10001, True),
|
||||
('1.234567899999960002', '1.234567899999910001', 1234567899999910001, 10001, True),
|
||||
('1.23456789', '1.234567890000020002', 1234567890000020002, 20002, False),
|
||||
('1.23456789', '1.234567889999920002', 1234567889999920002, 20002, True),
|
||||
('1.23456789', '1.234567889999999999', 1234567889999999999, 99999, False),
|
||||
('1.23456789', '1.234567889999999999', 1234567889999999999, 99999, True))
|
||||
}, {
|
||||
'desc': 'USDT',
|
||||
'asset': 'eth.usdt',
|
||||
'data': (
|
||||
('1.012345', '1.012345', 1012345, 12345, False),
|
||||
('1', '1.020002', 1020002, 20002, False),
|
||||
('1.025', '1.075', 1075000, 75000, False),
|
||||
('1.025', '0.975001', 975001, 75001, False),
|
||||
('1.075', '1.025001', 1025001, 25001, False),
|
||||
('1.075', '1.025', 1025000, 25000, False),
|
||||
('1.076', '1.125', 1125000, 25000, False),
|
||||
('1.012345', '1.012345', 1012345, 12345, True),
|
||||
('1', '0.920002', 920002, 20002, True),
|
||||
('1.025', '0.975', 975000, 75000, True),
|
||||
('1.025', '0.975001', 975001, 75001, True),
|
||||
('1.075', '1.025001', 1025001, 25001, True),
|
||||
('1.075', '1.025', 1025000, 25000, True),
|
||||
('1.076', '1.025', 1025000, 25000, True),
|
||||
('1.234567', '1.220002', 1220002, 20002, False),
|
||||
('1.234567', '1.25', 1250000, 50000, False),
|
||||
('1.234567', '1.250001', 1250001, 50001, False),
|
||||
('1.234567', '1.199999', 1199999, 99999, False),
|
||||
('1.234567', '1.199999', 1199999, 99999, True),
|
||||
('1.567', '1.520002', 1520002, 20002, False))}]
|
||||
|
||||
def get_type(diff):
|
||||
return 'A' if diff == 0 else (
|
||||
('B' if diff < halfmod else 'C' if diff == halfmod else 'D') if diff > 0 else
|
||||
('E' if diff > -halfmod else 'F' if diff == -halfmod else 'G'))
|
||||
|
||||
mod = THORChainMemolessAmount.mod
|
||||
halfmod = mod // 2
|
||||
|
||||
for vec in vecs:
|
||||
wid = get_coin_amt(cfg, vec['asset'], '0').max_prec + 3
|
||||
fs = ' {:{w}} {:{w}} {:<{w}} {:<6} {:<4} {}'
|
||||
vmsg(f'\n {vec["desc"]}')
|
||||
vmsg(fs.format('AMT_IN', 'AMT_OUT', 'INT_AMT', 'REFNUM', 'TYPE', 'ROUND_DOWN', w=wid))
|
||||
vmsg(fs.format('------', '-------', '-------', '------', '----', '----------', w=wid))
|
||||
for amt_in, amt_out, int_amt, refnum, round_down in vec['data']:
|
||||
amt = get_coin_amt(cfg, vec['asset'], amt_in)
|
||||
enc = THORChainMemolessAmount(amt, refnum=refnum, round_down=round_down)
|
||||
vmsg(fs.format(
|
||||
amt_in,
|
||||
amt_out,
|
||||
int_amt,
|
||||
refnum,
|
||||
get_type(refnum - amt.to_unit('atomic') % mod),
|
||||
round_down,
|
||||
w = wid))
|
||||
|
||||
assert enc.refnum == refnum, f'{enc.refnum} != {refnum}'
|
||||
assert str(enc.amt) == amt_out, f'{enc.amt!s} != {amt_out}'
|
||||
assert enc.int_amt == int_amt, f'{enc.int_amt} != {int_amt}'
|
||||
if round_down:
|
||||
assert enc.amt <= amt
|
||||
|
||||
dec = THORChainMemolessAmount(enc.amt)
|
||||
assert enc.amt == dec.amt, f'{enc.amt} != {dec.amt}'
|
||||
assert enc.int_amt == dec.int_amt, f'{enc.int_amt} != {dec.int_amt}'
|
||||
assert enc.refnum == dec.refnum, f'{enc.refnum} != {dec.refnum}'
|
||||
|
||||
return True
|
||||
|
||||
vmsg('\nTesting error handling')
|
||||
|
||||
proto = init_proto(cfg, 'btc', need_amt=True)
|
||||
|
||||
def bad1(): THORChainMemolessAmount(proto.coin_amt('1'), 100000)
|
||||
def bad2(): THORChainMemolessAmount(proto.coin_amt('1'), 'foo')
|
||||
def bad3(): THORChainMemolessAmount(proto.coin_amt('1'), -1)
|
||||
def bad4(): THORChainMemolessAmount(1, 1)
|
||||
|
||||
ut.process_bad_data((
|
||||
('refnum (100000)', 'AssertionError', 'must be an integer', bad1),
|
||||
('refnum ("foo")', 'AssertionError', 'must be an integer', bad2),
|
||||
('refnum (-1)', 'AssertionError', 'must be an integer', bad3),
|
||||
('amount (integer)', 'AssertionError', 'must be CoinAmt', bad4),
|
||||
))
|
||||
|
||||
return True
|
||||
|
|
|
|||
BIN
test/ref/thorchain/mainnet-tx-msgdeposit-refreq1.binpb
Normal file
BIN
test/ref/thorchain/mainnet-tx-msgdeposit-refreq1.binpb
Normal file
Binary file not shown.
BIN
test/ref/thorchain/mainnet-tx-msgdeposit-refreq2.binpb
Normal file
BIN
test/ref/thorchain/mainnet-tx-msgdeposit-refreq2.binpb
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue