Ethereum THORChain swaps

Only native ETH supported for now.  Work on ERC20 token swaps is underway.

Sample create-sign-send workflow for a BTC->ETH swap (assumes offline
autosigning is set up):

    $ mmgen-swaptxcreate --autosign BTC 0.12345 ETH

    remove device - insert - wait for signing - remove - insert

    $ mmgen-txsend --autosign

Create step for ETH->BTC swap:

    $ mmgen-swaptxcreate --autosign ETH 5.4321 BTC

For more information, see:

    $ mmgen-swaptxcreate --help

Testing:

    $ test/cmdtest.py ethswap
This commit is contained in:
The MMGen Project 2025-03-23 09:26:45 +03:00
commit f0563031de
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
27 changed files with 376 additions and 35 deletions

View file

@ -64,8 +64,11 @@
By default, the change and destination addresses are chosen automatically by
finding the lowest-indexed unused addresses of the preferred address types in
the send and receive tracking wallets. Types ‘B’, ‘S’ and ‘C’ (see ADDRESS
TYPES below) are searched in that order for unused addresses.
the send and receive tracking wallets. For Bitcoin and forks, types ‘B’,
‘S’ and ‘C’ (see ADDRESS TYPES below) are searched in that 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.
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
@ -74,7 +77,8 @@
Change and destination addresses may also be specified manually with the
CHG_ADDR and ADDR arguments. These may be given as full MMGen IDs or in the
form ADDRTYPE_CODE or SEED_ID:ADDRTYPE_CODE (see EXAMPLES below and the
‘mmgen-txcreate’ help screen for details).
‘mmgen-txcreate’ help screen for details). For Ethereum, the CHG_ADDR
argument is not supported.
While discouraged, sending change or swapping to non-wallet addresses is also
supported, in which case the signing script (‘mmgen-txsign’ or ‘mmgen-
@ -196,5 +200,5 @@
$ mmgen-tool --coin=bch --bch-rpc-host=gemini twview minconf=0
MMGEN v15.1.dev18 March 2025 MMGEN-SWAPTXCREATE(1)
MMGEN v15.1.dev23 March 2025 MMGEN-SWAPTXCREATE(1)
```

View file

@ -85,8 +85,11 @@
By default, the change and destination addresses are chosen automatically by
finding the lowest-indexed unused addresses of the preferred address types in
the send and receive tracking wallets. Types ‘B’, ‘S’ and ‘C’ (see ADDRESS
TYPES below) are searched in that order for unused addresses.
the send and receive tracking wallets. For Bitcoin and forks, types ‘B’,
‘S’ and ‘C’ (see ADDRESS TYPES below) are searched in that 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.
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
@ -95,7 +98,8 @@
Change and destination addresses may also be specified manually with the
CHG_ADDR and ADDR arguments. These may be given as full MMGen IDs or in the
form ADDRTYPE_CODE or SEED_ID:ADDRTYPE_CODE (see EXAMPLES below and the
‘mmgen-txcreate’ help screen for details).
‘mmgen-txcreate’ help screen for details). For Ethereum, the CHG_ADDR
argument is not supported.
While discouraged, sending change or swapping to non-wallet addresses is also
supported, in which case the signing script (‘mmgen-txsign’ or ‘mmgen-
@ -260,5 +264,5 @@
$ mmgen-tool --coin=bch --bch-rpc-host=gemini twview minconf=0
MMGEN v15.1.dev18 March 2025 MMGEN-SWAPTXDO(1)
MMGEN v15.1.dev23 March 2025 MMGEN-SWAPTXDO(1)
```

View file

@ -1 +1 @@
15.1.dev22
15.1.dev23

View file

@ -41,8 +41,11 @@ inputs will be swapped.
By default, the change and destination addresses are chosen automatically by
finding the lowest-indexed unused addresses of the preferred address types in
the send and receive tracking wallets. Types B, S and C (see ADDRESS
TYPES below) are searched in that order for unused addresses.
the send and receive tracking wallets. For Bitcoin and forks, types B,
S and C (see ADDRESS TYPES below) are searched in that 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.
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
@ -51,7 +54,8 @@ 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 ADDR arguments. These may be given as full MMGen IDs or in the
form ADDRTYPE_CODE or SEED_ID:ADDRTYPE_CODE (see EXAMPLES below and the
mmgen-txcreate help screen for details).
mmgen-txcreate help screen for details). For Ethereum, the CHG_ADDR
argument is not supported.
While discouraged, sending change or swapping to non-wallet addresses is also
supported, in which case the signing script (mmgen-txsign or mmgen-

View file

@ -53,6 +53,7 @@ class mainnet(CoinProtocol.Secp256k1): # chainparams.cpp
start_subsidy = 50
max_int = 0xffffffff
max_op_return_data_len = 80
address_reuse_ok = False
coin_cfg_opts = (
'daemon_id',

View file

@ -37,6 +37,8 @@ class mainnet(CoinProtocol.DummyWIF, CoinProtocol.Secp256k1):
base_coin = 'ETH'
avg_bdi = 15
decimal_prec = 36
address_reuse_ok = True
is_evm = True
# https://www.chainid.dev
chain_ids = {

View file

@ -65,8 +65,7 @@ class EthereumTwAddresses(TwAddresses, EthereumTwView, EthereumTwRPC):
async def get_rpc_data(self):
amt0 = self.proto.coin_amt('0')
self.total = amt0
self.total = self.proto.coin_amt('0')
self.minconf = None
addrs = {}
@ -75,7 +74,8 @@ class EthereumTwAddresses(TwAddresses, EthereumTwView, EthereumTwRPC):
addrs[e.label.mmid] = {
'addr': e.coinaddr,
'amt': bal,
'recvd': amt0,
'recvd': bal, # since it’s nearly impossible to empty an Ethereum account,
# we consider a used account to be any account with a balance
'confs': 0,
'lbl': e.label}
self.total += bal

View file

@ -18,6 +18,11 @@ from .base import Base, TokenBase
class Completed(Base, TxBase.Completed):
fn_fee_unit = 'Mwei'
def get_tx_usr_data(self):
o = self.txobj
if o['to'] and o['data']:
return bytes.fromhex(o['data'])
@property
def send_amt(self):
return self.outputs[0].amt if self.outputs else self.proto.coin_amt('0')

View file

@ -49,7 +49,10 @@ class TxInfo(TxInfo):
t = to_addr.hl(0) if to_addr else blue('None'),
a = t['amt'].hl(),
n = t['nonce'].hl(),
d = '{}... ({} bytes)'.format(td[:40], len(td)//2) if len(td) else blue('None'),
d = (
blue('None') if not td
else pink(bytes.fromhex(td).decode()) if tx.is_swap
else '{}... ({} bytes)'.format(td[:40], len(td)//2)),
c = tx.proto.dcoin if len(tx.outputs) else '',
g = yellow(tx.pretty_fmt_fee(t['gasPrice'].to_unit('Gwei'))),
G = yellow(tx.pretty_fmt_fee(t['startGas'].to_unit('Kwei'))),
@ -65,7 +68,7 @@ class TxInfo(TxInfo):
)
def format_verbose_footer(self):
if self.tx.txobj['data']:
if self.tx.txobj['data'] and not self.tx.is_swap:
from ..contract import parse_abi
return '\nParsed contract data: ' + pp_fmt(parse_abi(self.tx.txobj['data']))
else:

View file

@ -28,6 +28,8 @@ class New(Base, TxBase.New):
no_chg_msg = 'Warning: Transaction leaves account with zero balance'
usr_fee_prompt = 'Enter transaction fee or gas price: '
msg_insufficient_funds = 'Account balance insufficient to fund this transaction ({} {} needed)'
byte_cost = 68 # https://ethereum.stackexchange.com/questions/39401/
# how-do-you-calculate-gas-limit-for-transaction-with-data-in-ethereum
def __init__(self, *args, **kwargs):
@ -66,7 +68,7 @@ class New(Base, TxBase.New):
async def create_serialized(self, *, locktime=None):
assert len(self.inputs) == 1, 'Transaction has more than one input!'
o_num = len(self.outputs)
o_ok = 0 if self.usr_contract_data else 1
o_ok = 0 if self.usr_contract_data and not self.is_swap else 1
assert o_num == o_ok, f'Transaction has {o_num} output{suf(o_num)} (should have {o_ok})'
await self.make_txobj()
odict = {k:v if v is None else str(v) for k, v in self.txobj.items() if k != 'token_to'}
@ -78,10 +80,26 @@ class New(Base, TxBase.New):
'update_txid() must be called only when self.serialized is not hex data')
self.txid = MMGenTxID(make_chksum_6(self.serialized).upper())
def set_gas_with_data(self, data):
self.gas = self.proto.coin_amt(self.dfl_gas + self.byte_cost * len(data), from_unit='wei')
# one-shot method
def adj_gas_with_extra_data_len(self, extra_data_len):
if not hasattr(self, '_gas_adjusted'):
self.gas += self.proto.coin_amt(self.byte_cost * extra_data_len, from_unit='wei')
self._gas_adjusted = True
async def process_cmdline_args(self, cmd_args, ad_f, ad_w):
lc = len(cmd_args)
if lc == 2 and self.is_swap:
data_arg = cmd_args.pop()
lc = 1
assert data_arg.startswith('data:'), f'{data_arg}: invalid data arg (must start with "data:")'
self.usr_contract_data = data_arg.removeprefix('data:').encode()
self.set_gas_with_data(self.usr_contract_data)
if lc == 0 and self.usr_contract_data and 'Token' not in self.name:
return
@ -91,9 +109,10 @@ class New(Base, TxBase.New):
a = self.parse_cmdline_arg(self.proto, cmd_args[0], ad_f, ad_w)
self.add_output(
coinaddr = a.addr,
coinaddr = None if a.is_vault else a.addr,
amt = self.proto.coin_amt(a.amt or '0'),
is_chg = not a.amt)
is_chg = not a.amt,
is_vault = a.is_vault)
self.add_mmaddrs_to_outputs(ad_f, ad_w)

38
mmgen/proto/eth/tx/new_swap.py Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env python3
#
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
proto.eth.tx.new_swap: Ethereum new swap transaction class
"""
from ....tx.new_swap import NewSwap as TxNewSwap
from ....tx.new_swap import get_swap_proto_mod
from .new import New
class NewSwap(New, TxNewSwap):
desc = 'Ethereum swap transaction'
def update_data_output(self, trade_limit):
sp = get_swap_proto_mod(self.swap_proto)
parsed_memo = sp.data.parse(self.usr_contract_data.decode())
memo = sp.data(
self.recv_proto,
self.recv_proto.coin_addr(parsed_memo.address),
trade_limit = trade_limit)
self.usr_contract_data = str(memo).encode()
self.set_gas_with_data(self.usr_contract_data)
@property
def vault_idx(self):
return 0
@property
def vault_output(self):
return self.outputs[0]

View file

@ -60,7 +60,7 @@ class Status(TxBase.Status):
from ....color import cyan
msg('{}\n{}'.format(cyan('TRANSACTION RECEIPT'), pp_fmt(ret.rx)))
if ret:
if tx.txobj['data']:
if tx.txobj['data'] and not tx.is_swap:
cd = capfirst(tx.contract_desc)
if ret.exec_status == 0:
msg(f'{cd} failed to execute!')

View file

@ -60,7 +60,8 @@ class Unsigned(Completed, TxBase.Unsigned):
if o['data']: # contract-creating transaction
if o['to']:
raise ValueError('contract-creating transaction cannot have to-address')
if not self.is_swap:
raise ValueError('contract-creating transaction cannot have to-address')
else:
self.txobj['token_addr'] = TokenAddr(self.proto, etx.creates.hex())

View file

@ -55,6 +55,7 @@ class CoinProtocol(MMGenObject):
base_coin = None
is_fork_of = None
chain_names = None
is_evm = False
networks = ('mainnet', 'testnet', 'regtest')
decimal_prec = 28
_set_ok = ('tokensym',)

View file

@ -23,11 +23,13 @@ class params:
'BTC': 'Bitcoin',
'LTC': 'Litecoin',
'BCH': 'Bitcoin Cash',
'ETH': 'Ethereum',
},
'receive': {
'BTC': 'Bitcoin',
'LTC': 'Litecoin',
'BCH': 'Bitcoin Cash',
'ETH': 'Ethereum',
}
}

View file

@ -12,7 +12,7 @@
swap.proto.thorchain.memo: THORChain swap protocol memo class
"""
from ....util import die
from ....util import die, is_hex_str
from ....amt import UniAmt
from . import name as proto_name
@ -42,6 +42,8 @@ class Memo:
'THOR.RUNE': 'r',
}
evm_chains = ('ETH', 'AVAX', 'BSC', 'BASE')
function_abbrevs = {
'SWAP': '=',
}
@ -95,6 +97,11 @@ class Memo:
address = get_item('address')
if chain in cls.evm_chains:
assert address.startswith('0x'), f'{address}: address does not start with ‘0x’'
assert len(address) == 42, f'{address}: address has incorrect length ({len(address)} != 42)'
address = address.removeprefix('0x')
desc = 'trade_limit/stream_interval/stream_quantity'
lsq = get_item(desc)
@ -135,6 +142,11 @@ class Memo:
self.addr = addr.views[addr.view_pref]
assert not ':' in self.addr # colon is record separator, so address mustn’t contain one
if self.chain in self.evm_chains:
assert len(self.addr) == 40, f'{self.addr}: address has incorrect length ({len(self.addr)} != 40)'
assert is_hex_str(self.addr), f'{self.addr}: address is not a hexadecimal string'
self.addr = '0x' + self.addr
def __str__(self):
from . import ExpInt4
try:

View file

@ -98,6 +98,8 @@ class Thornode:
{lblcolor('Trade limit:')} {tl_rounded.hl()} {out_coin} """ + mcolor(
f'({abs(1 - ratio) * 100:0.2f}% {direction} expected amount)')
tx_size_adj = len(e.enc) - 1
if tx.proto.is_evm:
tx.adj_gas_with_extra_data_len(len(e.enc) - 1) # one-shot method, no-op if repeated
else:
trade_limit_disp = ''
tx_size_adj = 0
@ -105,7 +107,7 @@ class Thornode:
def get_estimated_fee():
return tx.feespec2abs(
fee_arg = d['recommended_gas_rate'] + gas_unit_data[gas_unit].code,
tx_size = tx.estimate_size() + tx_size_adj)
tx_size = None if tx.proto.is_evm else tx.estimate_size() + tx_size_adj)
_amount_in_label = 'Amount in:'
if deduct_est_fee:
@ -145,7 +147,8 @@ class Thornode:
@property
def inbound_address(self):
return self.data['inbound_address']
addr = self.data['inbound_address']
return addr.removeprefix('0x') if self.tx.proto.is_evm else addr
@property
def rel_fee_hint(self):

View file

@ -74,7 +74,9 @@ class TxInfo:
if tx.is_swap:
from ..swap.proto.thorchain.memo import Memo, proto_name
data = tx.data_output.data
data = (
(tx.usr_contract_data or bytes.fromhex(tx.txobj['data'])) if tx.proto.is_evm
else tx.data_output.data)
if Memo.is_partial_memo(data):
p = Memo.parse(data.decode('ascii'))
yield ' {} {}\n'.format(magenta('DEX Protocol:'), blue(proto_name))

View file

@ -306,6 +306,8 @@ class New(Base):
die(1, 'Exiting at user request')
async def warn_addr_used(self, proto, chg, desc):
if proto.address_reuse_ok:
return
from ..tw.addresses import TwAddresses
if (await TwAddresses(self.cfg, proto, get_data=True)).is_used(chg.addr):
from ..ui import keypress_confirm

View file

@ -97,7 +97,7 @@ class NewSwap(New):
arg = get_arg()
# arg 3: chg_spec (change address spec)
if args.send_amt:
if args.send_amt and not self.proto.is_evm:
if not arg in sp.params.coins['receive']: # is change arg
args.chg_spec = arg
arg = get_arg()
@ -119,7 +119,7 @@ class NewSwap(New):
chg_output = (
await self.get_swap_output(self.proto, args.chg_spec, addrfiles, 'change address')
if args.send_amt else None)
if args.send_amt and not self.proto.is_evm else None)
if chg_output:
self.check_addr_is_wallet_addr(
@ -145,6 +145,7 @@ class NewSwap(New):
self.swap_recv_addr_mmid = recv_output.mmid
return (
[f'vault,{args.send_amt}', f'data:{memo}'] if args.send_amt and self.proto.is_evm else
[f'vault,{args.send_amt}', chg_output.mmid, f'data:{memo}'] if args.send_amt else
['vault', f'data:{memo}'])

View file

@ -39,6 +39,7 @@ cmd_groups_dfl = {
'autosign_eth': ('CmdTestAutosignETH', {'modname': 'automount_eth'}),
'regtest': ('CmdTestRegtest', {}),
'swap': ('CmdTestSwap', {}),
'ethswap': ('CmdTestEthSwap', {}),
# 'chainsplit': ('CmdTestChainsplit', {}),
'ethdev': ('CmdTestEthdev', {}),
'xmrwallet': ('CmdTestXMRWallet', {}),
@ -46,6 +47,7 @@ cmd_groups_dfl = {
}
cmd_groups_extra = {
'ethswap_eth': ('CmdTestEthSwapEth', {'modname': 'ethswap'}),
'dev': ('CmdTestDev', {'modname': 'misc'}),
'regtest_legacy': ('CmdTestRegtestBDBWallet', {'modname': 'regtest'}),
'autosign_btc': ('CmdTestAutosignBTC', {'modname': 'autosign'}),
@ -241,6 +243,8 @@ cfgs = { # addr_idx_lists (except 31, 32, 33, 34) must contain exactly 8 address
'39': {}, # xmr_autosign
'40': {}, # cfgfile
'41': {}, # opts
'47': {}, # ethswap
'48': {}, # ethswap_eth
'49': {}, # autosign_automount
'59': {}, # autosign_eth
'99': {}, # dummy

View file

@ -123,7 +123,7 @@ coin = cfg.coin
class CmdTestEthdev(CmdTestBase, CmdTestShared):
'Ethereum transacting, token deployment and tracking wallet operations'
networks = ('eth', 'etc')
passthru_opts = ('coin', 'daemon_id', 'http_timeout', 'rpc_backend')
passthru_opts = ('coin', 'daemon_id', 'eth_daemon_id', 'http_timeout', 'rpc_backend')
tmpdir_nums = [22]
color = True
menu_prompt = 'efresh balance:\b'

219
test/cmdtest_d/ct_ethswap.py Executable file
View file

@ -0,0 +1,219 @@
#!/usr/bin/env python3
#
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
# Licensed under the GNU General Public License, Version 3:
# https://www.gnu.org/licenses
# Public project repositories:
# https://github.com/mmgen/mmgen-wallet
# https://gitlab.com/mmgen/mmgen-wallet
"""
test.cmdtest_d.ct_ethswap: Ethereum swap tests for the cmdtest.py test suite
"""
from mmgen.wallet.mmgen import wallet as MMGenWallet
from mmgen.cfg import Config
from mmgen.protocol import init_proto
from .runner import CmdTestRunner
from .common import dfl_words_file, dfl_seed_id, rt_pw
from .httpd.thornode import ThornodeServer
from .ct_regtest import CmdTestRegtest
from .ct_swap import CmdTestSwapMethods
from .ct_ethdev import CmdTestEthdev
thornode_server = ThornodeServer()
method_template = """
def {name}(self):
self.spawn(log_only=True)
return ethswap_eth.run_test("{eth_name}", sub=True)
"""
class CmdTestEthSwap(CmdTestRegtest, CmdTestSwapMethods):
'Ethereum swap operations'
bdb_wallet = True
tmpdir_nums = [47]
networks = ('btc',)
passthru_opts = ('coin', 'rpc_backend', 'eth_daemon_id')
cmd_group_in = (
('setup', 'regtest (Bob and Alice) mode setup'),
('eth_setup', 'Ethereum devnet setup'),
('subgroup.init', []),
('subgroup.fund', ['init']),
('subgroup.eth_init', []),
('subgroup.eth_fund', ['eth_init']),
('subgroup.swap', ['fund', 'eth_fund']),
('subgroup.eth_swap', ['fund', 'eth_fund']),
('stop', 'stopping regtest daemon'),
('eth_stop', 'stopping Ethereum daemon'),
('thornode_server_stop', 'stopping the Thornode server'),
)
cmd_subgroups = {
'init': (
'creating Bob’s MMGen wallet and tracking wallet',
('walletconv_bob', 'wallet creation (Bob)'),
('addrgen_bob', 'address generation (Bob)'),
('addrimport_bob', 'importing Bob’s addresses'),
),
'fund': (
'funding Bob’s wallet',
('bob_import_miner_addr', 'importing miner’s coinbase addr into Bob’s wallet'),
('fund_bob', 'funding Bob’s wallet'),
('generate', 'mining a block'),
('bob_bal1', 'Bob’s balance'),
),
'eth_init': (
'initializing the ETH tracking wallet',
('eth_addrgen', ''),
('eth_addrimport', ''),
('eth_addrimport_dev_addr', ''),
('eth_fund_dev_address', ''),
),
'eth_fund': (
'funding the ETH tracking wallet',
('eth_txcreate1', ''),
('eth_txsign1', ''),
('eth_txsend1', ''),
('eth_bal1', ''),
),
'swap': (
'swap operations (BTC -> ETH)',
('swaptxcreate1', 'creating a BTC->ETH swap transaction'),
('swaptxcreate2', 'creating a BTC->ETH swap transaction (used account)'),
('swaptxsign1', 'signing the swap transaction'),
('swaptxsend1', 'sending the swap transaction'),
('generate', 'generating a block'),
('bob_bal2', 'Bob’s balance'),
),
'eth_swap': (
'swap operations (ETH -> BTC)',
('eth_swaptxcreate1', ''),
('eth_swaptxcreate2', ''),
('eth_swaptxsign1', ''),
('eth_swaptxsend1', ''),
('eth_swaptxstatus1', ''),
('eth_bal2', ''),
),
}
eth_tests = [c[0] for v in tuple(cmd_subgroups.values()) + (cmd_group_in,)
for c in v if isinstance(c, tuple) and c[0].startswith('eth_')]
exec(''.join(method_template.format(name=k, eth_name=k.removeprefix('eth_')) for k in eth_tests))
def __init__(self, cfg, trunner, cfgs, spawn):
super().__init__(cfg, trunner, cfgs, spawn)
if not trunner:
return
global ethswap_eth
cfg = Config({
'_clone': trunner.cfg,
'proto': init_proto(cfg, network_id='eth'),
'resume': None,
'resume_after': None,
'exit_after': None,
'eth_daemon_id': trunner.cfg.eth_daemon_id,
'log': None,
'coin': 'eth'})
t = trunner
ethswap_eth = CmdTestRunner(cfg, t.repo_root, t.data_dir, t.trash_dir, t.trash_dir2)
ethswap_eth.init_group('ethswap_eth')
thornode_server.start()
def walletconv_bob(self):
t = self.spawn(
'mmgen-walletconv',
['--bob', '--quiet', '-r0', f'-d{self.cfg.data_dir}/regtest/bob', dfl_words_file],
no_passthru_opts = ['coin', 'eth_daemon_id'])
t.hash_preset(MMGenWallet.desc, '1')
t.passphrase_new('new '+MMGenWallet.desc, rt_pw)
t.label()
return t
def swaptxcreate1(self):
self.get_file_with_ext('rawtx', delete_all=True)
t = self._swaptxcreate(['BTC', '8.765', 'ETH'])
t.expect('OK? (Y/n): ', 'y')
t.expect(':E:2')
t.expect('OK? (Y/n): ', 'y')
return self._swaptxcreate_ui_common(t)
def swaptxcreate2(self):
self.get_file_with_ext('rawtx', delete_all=True)
t = self._swaptxcreate(['BTC', '8.765', 'ETH', f'{dfl_seed_id}:E:1'])
t.expect('OK? (Y/n): ', 'y')
return self._swaptxcreate_ui_common(t)
def swaptxsign1(self):
return self._swaptxsign()
def swaptxsend1(self):
return self._swaptxsend()
def bob_bal2(self):
return self._user_bal_cli('bob', chk='491.23498314')
def thornode_server_stop(self):
self.spawn(msg_only=True)
thornode_server.stop()
return 'ok'
class CmdTestEthSwapEth(CmdTestEthdev, CmdTestSwapMethods):
'Ethereum swap operations - Ethereum wallet'
networks = ('eth',)
tmpdir_nums = [48]
bals = lambda self, k: {
'swap1': [('98831F3A:E:1', '123.456')],
'swap2': [('98831F3A:E:1', '114.690978056')],
}[k]
cmd_group_in = CmdTestEthdev.cmd_group_in + (
('swaptxcreate1', 'creating an ETH->BTC swap transaction'),
('swaptxcreate2', 'creating an ETH->BTC swap transaction (specific address, trade limit)'),
('swaptxsign1', 'signing the transaction'),
('swaptxsend1', 'sending the transaction'),
('swaptxstatus1', 'getting the transaction status (with --verbose)'),
('bal1', 'the ETH balance'),
('bal2', 'the ETH balance'),
)
def swaptxcreate1(self):
self.get_file_with_ext('rawtx', delete_all=True)
t = self._swaptxcreate(['ETH', '8.765', 'BTC'])
t.expect('OK? (Y/n): ', 'y')
return self._swaptxcreate_ui_common(t)
def swaptxcreate2(self):
self.get_file_with_ext('rawtx', delete_all=True)
return self._swaptxcreate_ui_common(
self._swaptxcreate(
['ETH', '8.765', 'BTC', f'{dfl_seed_id}:B:3'],
add_opts = ['--trade-limit=3%']),
expect = ':2019e4/1/0')
def swaptxsign1(self):
return self._swaptxsign()
def swaptxsend1(self):
return self._swaptxsend()
def swaptxstatus1(self):
return self._swaptxsend(add_opts=['--verbose', '--status'], status=True)
def bal1(self):
return self.bal('swap1')
def bal2(self):
return self.bal('swap2')

View file

@ -160,7 +160,7 @@ class CmdTestSwapMethods:
],
spawn_only = spawn_only)
def _swaptxsend(self, *, add_opts=[], spawn_only=False):
def _swaptxsend(self, *, add_opts=[], spawn_only=False, status=False):
fn = self.get_file_with_ext('sigtx')
t = self.spawn(
'mmgen-txsend',
@ -169,6 +169,8 @@ class CmdTestSwapMethods:
if spawn_only:
return t
t.expect('view: ', 'v')
if status:
return t
t.expect('(y/N): ', 'n')
t.expect('to confirm: ', 'YES\n')
return t

View file

@ -24,9 +24,9 @@ cfg = Config()
# https://thornode.ninerealms.com/thorchain/quote/swap?from_asset=BCH.BCH&to_asset=LTC.LTC&amount=1000000
sample_request = 'GET /thorchain/quote/swap?from_asset=BCH.BCH&to_asset=LTC.LTC&amount=1000000000'
request_pat = r'/thorchain/quote/swap\?from_asset=(\S+)\.(\S+)&to_asset=(\S+)\.(\S+)&amount=(\d+)'
prices = {'BTC': 97000, 'LTC': 115, 'BCH': 330}
gas_rate_units = {'BTC': 'satsperbyte'}
recommended_gas_rate = {'BTC': '6'}
prices = {'BTC': 97000, 'LTC': 115, 'BCH': 330, 'ETH': 2304}
gas_rate_units = {'ETH': 'gwei', 'BTC': 'satsperbyte'}
recommended_gas_rate = {'ETH': '1', 'BTC': '6'}
data_template = {
'inbound_address': None,
@ -59,11 +59,12 @@ data_template = {
def make_inbound_addr(proto, mmtype):
from mmgen.tool.coin import tool_cmd
n = int(time.time()) // (60 * 60 * 24) # increments once every 24 hrs
return tool_cmd(
ret = tool_cmd(
cfg = cfg,
cmdname = 'pubhash2addr',
proto = proto,
mmtype = mmtype).pubhash2addr(f'{n:040x}')
return '0x' + ret if proto.is_evm else ret
class ThornodeServer(HTTPD):
name = 'thornode server'

View file

@ -124,6 +124,7 @@ class CmdTestRunner:
extra_desc = '',
no_output = False,
msg_only = False,
log_only = False,
no_msg = False,
cmd_dir = 'cmds',
no_exec_wrapper = False,
@ -167,6 +168,9 @@ class CmdTestRunner:
self.tg.test_name,
cmd_disp))
if log_only:
return
for i in args: # die only after writing log entry
if not isinstance(i, str):
die(2, 'Error: missing input files in cmd line?:\nName: {}\nCmdline: {!r}'.format(
@ -426,7 +430,7 @@ class CmdTestRunner:
return rerun
def run_test(self, cmd):
def run_test(self, cmd, sub=False):
if self.deps_only and cmd == self.deps_only:
sys.exit(0)
@ -463,6 +467,9 @@ class CmdTestRunner:
setattr(self.tg, k, test_cfg[k])
ret = getattr(self.tg, cmd)(*arg_list) # run the test
if sub:
return ret
if type(ret).__name__ == 'coroutine':
ret = asyncio.run(ret)

View file

@ -9,6 +9,7 @@ import os
from mmgen.tx import CompletedTX, UnsignedTX
from mmgen.tx.file import MMGenTxFile
from mmgen.cfg import Config
from mmgen.color import cyan
from ..include.common import cfg, qmsg, vmsg, gr_uc, make_burn_addr
@ -175,10 +176,13 @@ class unit_tests:
for coin, addrtype in (
('ltc', 'bech32'),
('bch', 'compressed'),
('eth', None),
):
proto = init_proto(cfg, coin, need_amt=True)
addr = make_burn_addr(proto, addrtype)
vmsg(f'\nTesting coin {cyan(coin.upper())}:')
for limit, limit_chk in (
('123.4567', 12340000000),
('1.234567', 123400000),