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:
parent
287107ce73
commit
f0563031de
27 changed files with 376 additions and 35 deletions
|
|
@ -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)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
15.1.dev22
|
||||
15.1.dev23
|
||||
|
|
|
|||
|
|
@ -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-
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
38
mmgen/proto/eth/tx/new_swap.py
Executable 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]
|
||||
|
|
@ -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!')
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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',)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}'])
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
219
test/cmdtest_d/ct_ethswap.py
Executable 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')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue