THORChain ERC20 token swap support

Examples:

    # List available assets:
    $ mmgen-swaptxcreate -S

    # Create a Tether-to-LTC swap transaction for autosigning, connecting to the
    # swap quote server via Tor:
    $ mmgen-swaptxcreate --autosign --proxy=localhost:9050 ETH.USDT 1000 LTC

    # After signing, send the transaction via public Etherscan proxy over Tor:
    $ mmgen-txsend --autosign --quiet --tx-proxy=etherscan --proxy=localhost:9050

    # After sending, check the transaction status and receipt:
    $ mmgen-txsend --autosign --verbose --status

    # Create a Tether-to-DAI swap transaction, with explicit destination account:
    $ mmgen-swaptxcreate ETH.USDT 1000 ETH.DAI E:01234ABC:3

Testing:

    $ test/cmdtest.py -e ethswap
This commit is contained in:
The MMGen Project 2025-04-27 11:53:49 +00:00
commit ff28d39a3c
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
23 changed files with 371 additions and 35 deletions

View file

@ -16,6 +16,7 @@ include test/*/*.py
include test/*/*/*.py
include test/ref/*
include test/ref/*/*
include test/ref/*/*/*
include test/ref/*/*/*/*
include test/overlay/fakemods/mmgen/*.py
include test/overlay/fakemods/mmgen/*/*.py

View file

@ -1 +1 @@
15.1.dev30
15.1.dev31

View file

@ -73,4 +73,21 @@ EXAMPLES:
Check whether the funds have arrived in the BCH destination wallet:
$ mmgen-tool --coin=bch --bch-rpc-host=gemini twview minconf=0
Create a Tether-to-LTC swap transaction for autosigning, connecting to the
swap quote server via Tor:
$ {gc.prog_name} --autosign --proxy=localhost:9050 ETH.USDT 1000 LTC
After signing, send the transaction via public Etherscan proxy over Tor:
$ mmgen-txsend --autosign --quiet --tx-proxy=etherscan --proxy=localhost:9050
After sending, check the transaction status:
$ mmgen-txsend --autosign --verbose --status
Create a Tether-to-DAI swap transaction, with explicit destination account:
$ {gc.prog_name} ETH.USDT 1000 ETH.DAI E:01234ABC:3
"""

View file

@ -176,3 +176,23 @@ class ResolvedToken(Token, metaclass=AsyncInit):
if not self.decimals:
die('TokenNotInBlockchain', f'Token {addr!r} not in blockchain')
self.base_unit = Decimal('10') ** -self.decimals
# Tokens: First approve router to spend tokens from user: asset.approve(router,amount).
# Then call router.depositWithExpiry(inbound_address, asset, amount, memo, expiry).
# Asset is the token contract address. Amount should be in native asset decimals
# (eg 1e18 for most tokens). Do not swap to smart contract addresses.
class THORChainRouterContract(Token):
def create_deposit_with_expiry_data(self, inbound_addr, asset_addr, amt, memo, expiry):
assert isinstance(memo, bytes)
assert isinstance(expiry, int)
memo_chunks = len(memo) // 32 + bool(len(memo) % 32)
return ( # Method ID: 0x44bc937b
self.create_method_id('depositWithExpiry(address,address,uint256,string,uint256)')
+ inbound_addr.rjust(64, '0') # 32 bytes
+ asset_addr.rjust(64, '0') # 32 bytes
+ '{:064x}'.format(int(amt / self.base_unit)) # 32 bytes
+ '{:064x}'.format(32 * 5) # 32 bytes (memo offset)
+ '{:064x}'.format(expiry) # 32 bytes
+ '{:064x}'.format(len(memo)) # dynamic arg
+ memo.hex().ljust(64 * memo_chunks, '0'))

View file

@ -141,7 +141,7 @@ class EthereumTwCtl(TwCtl):
async def sym2addr(self, sym):
for addr in self.data['tokens']:
if self.data['tokens'][addr]['params']['symbol'] == sym.upper():
if self.data['tokens'][addr]['params']['symbol'].upper() == sym.upper():
return addr
def get_token_param(self, token, param):

View file

@ -111,7 +111,7 @@ class Base(TxBase):
f'{d[5]}: invalid swap memo in serialized data')
class TokenBase(Base):
dfl_gas = 52000
dfl_gas = 75000
contract_desc = 'token contract'
def check_serialized_integrity(self):
@ -126,8 +126,8 @@ class TokenBase(Base):
assert d[4] == b'', f'{d[4]}: non-empty amount field in token transaction in serialized data'
data = d[5].hex()
assert data[:8] == 'a9059cbb', (
f'{data[:8]}: invalid MethodID for op ‘transfer’ in serialized data')
assert data[:8] == ('095ea7b3' if self.is_swap else 'a9059cbb'), (
f'{data[:8]}: invalid MethodID for op ‘{self.token_op}’ in serialized data')
assert data[32:72] == o['token_to'], (
f'{data[32:72]}: invalid ‘token_to‘ address in serialized data')
assert TokenAmt(
@ -135,3 +135,20 @@ class TokenBase(Base):
decimals = o['decimals'],
from_unit = 'atomic') == o['amt'], (
f'{data[72:]}: invalid amt in serialized data')
if self.is_swap:
d = rlp.decode(bytes.fromhex(self.serialized2))
data = d[5].hex()
assert data[:8] == '44bc937b', (
f'{data[:8]}: invalid MethodID in router TX serialized data')
assert data[32:72] == self.token_vault_addr, (
f'{data[32:72]}: invalid vault address in router TX serialized data')
memo = bytes.fromhex(data[392:])[:len(self.swap_memo)]
assert memo == self.swap_memo.encode(), (
f'{memo}: invalid swap memo in router TX serialized data')
assert TokenAmt(
int(data[136:200], 16),
decimals = o['decimals'],
from_unit = 'atomic') == o['amt'], (
f'{data[136:200]}: invalid amt in router TX serialized data')

View file

@ -14,7 +14,7 @@ proto.eth.tx.info: Ethereum transaction info class
from ....tx.info import TxInfo
from ....util import fmt, pp_fmt
from ....color import pink, yellow, blue
from ....color import pink, yellow, blue, cyan
from ....addr import MMGenID
class TxInfo(TxInfo):
@ -34,7 +34,7 @@ class TxInfo(TxInfo):
return ' ' + (io.mmid.hl() if io.mmid else MMGenID.hlc(nonmm_str))
fs = """
From: {f}{f_mmid}
To: {t}{t_mmid}
{toaddr} {t}{t_mmid}{tvault}
Amount: {a} {c}
Gas price: {g} Gwei
Start gas: {G} Kwei
@ -44,10 +44,13 @@ class TxInfo(TxInfo):
t = tx.txobj
td = t['data']
to_addr = t[self.to_addr_key]
tokenswap = tx.is_swap and tx.is_token
return fs.format(
f = t['from'].hl(0),
t = to_addr.hl(0) if to_addr else blue('None'),
a = t['amt'].hl(),
toaddr = ('Router:' if tokenswap else 'To:').ljust(8),
tvault = (f'\nVault: {cyan(tx.token_vault_addr)}' if tokenswap else ''),
n = t['nonce'].hl(),
d = blue('None') if not td else '{}... ({} bytes)'.format(td[:40], len(td)//2),
m = pink(tx.swap_memo) if tx.is_swap else None,
@ -55,7 +58,7 @@ class TxInfo(TxInfo):
g = yellow(tx.pretty_fmt_fee(t['gasPrice'].to_unit('Gwei'))),
G = yellow(tx.pretty_fmt_fee(t['startGas'].to_unit('Kwei'))),
f_mmid = mmid_disp(tx.inputs[0]),
t_mmid = mmid_disp(tx.outputs[0]) if tx.outputs else '') + '\n\n'
t_mmid = mmid_disp(tx.outputs[0]) if tx.outputs and not tokenswap else '') + '\n\n'
def format_abs_fee(self, iwidth, /, *, color=None):
return self.tx.fee.fmt(iwidth, color=color) + (' (max)' if self.tx.txobj['data'] else '')

View file

@ -216,6 +216,8 @@ class TokenNew(TokenBase, New):
o['token_addr'] = t.addr
o['decimals'] = t.decimals
o['token_to'] = o['to']
if self.is_swap:
o['expiry'] = self.quote_data.data['expiry']
def update_change_output(self, funds_left):
if self.outputs[0].is_chg:

View file

@ -13,7 +13,7 @@ proto.eth.tx.new_swap: Ethereum new swap transaction class
"""
from ....tx.new_swap import NewSwap as TxNewSwap
from .new import New
from .new import New, TokenNew
class NewSwap(New, TxNewSwap):
desc = 'Ethereum swap transaction'
@ -34,3 +34,10 @@ class NewSwap(New, TxNewSwap):
@property
def vault_output(self):
return self.outputs[0]
class TokenNewSwap(TokenNew, NewSwap):
desc = 'Ethereum token swap transaction'
def update_vault_addr(self, c):
self.token_vault_addr = self.proto.coin_addr(c.inbound_address)
return super().update_vault_addr(c, addr='router')

View file

@ -18,7 +18,7 @@ from ....tx import unsigned as TxBase
from ....util import msg, msg_r, die
from ....obj import CoinTxID, ETHNonce, Int, HexStr
from ....addr import CoinAddr, ContractAddr
from ..contract import Token
from ..contract import Token, THORChainRouterContract
from .completed import Completed, TokenCompleted
class Unsigned(Completed, TxBase.Unsigned):
@ -110,14 +110,32 @@ class TokenUnsigned(TokenCompleted, Unsigned):
o['token_addr'] = ContractAddr(self.proto, d['token_addr'])
o['decimals'] = Int(d['decimals'])
o['token_to'] = o['to']
if self.is_swap:
o['expiry'] = Int(d['expiry'])
async def do_sign(self, o, wif):
t = Token(self.cfg, self.proto, o['token_addr'], decimals=o['decimals'])
tdata = t.create_transfer_data(o['to'], o['amt'], op='transfer')
tdata = t.create_transfer_data(o['to'], o['amt'], op=self.token_op)
tx_in = t.make_tx_in(gas=self.gas, gasPrice=o['gasPrice'], nonce=o['nonce'], data=tdata)
res = await t.txsign(tx_in, wif, o['from'], chain_id=o['chainId'])
self.serialized = res.txhex
self.coin_txid = res.txid
if self.is_swap:
c = THORChainRouterContract(self.cfg, self.proto, o['to'], decimals=o['decimals'])
cdata = c.create_deposit_with_expiry_data(
self.token_vault_addr,
o['token_addr'],
o['amt'],
self.swap_memo.encode(),
o['expiry'])
tx_in = c.make_tx_in(
gas = self.gas * (7.8 if self.cfg.test_suite else 2),
gasPrice = o['gasPrice'],
nonce = o['nonce'] + 1,
data = cdata)
res = await t.txsign(tx_in, wif, o['from'], chain_id=o['chainId'])
self.serialized2 = res.txhex
self.coin_txid2 = res.txid
class AutomountUnsigned(TxBase.AutomountUnsigned, Unsigned):
pass

View file

@ -24,9 +24,44 @@ class THORChainSwapAsset(SwapAsset):
'ETH': _ad('Ethereum', 'ETH', None, 'e', True),
'DOGE': _ad('Dogecoin', 'DOGE', None, 'd', False),
'RUNE': _ad('Rune (THORChain)', 'RUNE', 'THOR.RUNE', 'r', False),
'ETH.AAVE': _ad('Aave (ETH)', None, 'ETH.AAVE', None, True),
'ETH.DAI': _ad('Sky Dollar (USDS) (ETH)', None, 'ETH.DAI', None, True),
'ETH.DPI': _ad('DeFi Pulse Index (ETH)', None, 'ETH.DPI', None, True),
'ETH.FOX': _ad('ShapeShift FOX (ETH)', None, 'ETH.FOX', None, True),
'ETH.GUSD': _ad('Gemini Dollar (ETH)', None, 'ETH.GUSD', None, True),
'ETH.LINK': _ad('Chainlink (ETH)', None, 'ETH.LINK', None, True),
'ETH.LUSD': _ad('Liquity USD (ETH)', None, 'ETH.LUSD', None, True),
'ETH.SNX': _ad('Synthetix (ETH)', None, 'ETH.SNX', None, True),
'ETH.TGT': _ad('THORWallet (ETH)', None, 'ETH.TGT', None, True),
'ETH.THOR': _ad('THORSwap (ETH)', None, 'ETH.THOR', None, True),
'ETH.USDC': _ad('USDC (ETH)', None, 'ETH.USDC', None, True),
'ETH.USDP': _ad('Pax Dollar (ETH)', None, 'ETH.USDP', None, True),
'ETH.USDT': _ad('Tether (ETH)', None, 'ETH.USDT', None, True),
'ETH.vTHOR': _ad('THORSwap Staking (ETH)', None, 'ETH.vTHOR', None, True),
'ETH.WBTC': _ad('Wrapped BTC (ETH)', None, 'ETH.WBTC', None, True),
'ETH.XRUNE': _ad('Thorstarter (ETH)', None, 'ETH.XRUNE', None, True),
'ETH.YFI': _ad('yearn.finance (ETH)', None, 'ETH.YFI', None, True),
}
evm_contracts = {}
evm_contracts = {
'ETH.AAVE': '7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9',
'ETH.DAI': '6b175474e89094c44da98b954eedeac495271d0f',
'ETH.DPI': '1494ca1f11d487c2bbe4543e90080aeba4ba3c2b',
'ETH.FOX': 'c770eefad204b5180df6a14ee197d99d808ee52d',
'ETH.GUSD': '056fd409e1d7a124bd7017459dfea2f387b6d5cd',
'ETH.LINK': '514910771af9ca656af840dff83e8264ecf986ca',
'ETH.LUSD': '5f98805a4e8be255a32880fdec7f6728c6568ba0',
'ETH.SNX': 'c011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
'ETH.TGT': '108a850856db3f85d0269a2693d896b394c80325',
'ETH.THOR': 'a5f2211b9b8170f694421f2046281775e8468044',
'ETH.USDC': 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'ETH.USDP': '8e870d67f660d95d5be530380d0ec0bd388289e1',
'ETH.USDT': 'dac17f958d2ee523a2206206994597c13d831ec7',
'ETH.vTHOR': '815c23eca83261b6ec689b60cc4a58b54bc24d8d',
'ETH.WBTC': '2260fac5e5542a773aa44fbcfedf7c193bc2c599',
'ETH.XRUNE': '69fa0fee221ad11012bab0fdb45d444d3d2ce71c',
'ETH.YFI': '0bc529c00c6401aef6d220be8c6ea1667f6ad93e',
}
unsupported = ('DOGE', 'RUNE')

View file

@ -172,6 +172,10 @@ class Thornode:
addr = self.data['inbound_address']
return addr.removeprefix('0x') if self.tx.proto.is_evm else addr
@property
def router(self):
return self.data['router'].lower().removeprefix('0x')
@property
def rel_fee_hint(self):
gas_unit = self.data['gas_rate_units']

View file

@ -78,6 +78,7 @@ async def _get_obj_async(_clsname, _modname, **kwargs):
# signing.
if proto and proto.tokensym and clsname in (
'New',
'NewSwap',
'OnlineSigned',
'AutomountOnlineSigned',
'Sent',

View file

@ -86,7 +86,10 @@ class Base(MMGenObject):
'swap_quote_expiry': None,
'swap_recv_addr_mmid': None,
'swap_recv_asset_spec': None,
'swap_memo': None}
'swap_memo': None,
'token_vault_addr': None,
'serialized2': None,
'coin_txid2': CoinTxID}
file_format = 'json'
non_mmgen_inputs_msg = f"""
This transaction includes inputs with non-{gc.proj_name} addresses. When
@ -243,3 +246,8 @@ class Base(MMGenObject):
from ..swap.asset import SwapAsset
x = '[unknown]'
return SwapAsset._ad(x, x, x, x, x)
# token methods:
@property
def token_op(self):
return 'approve' if self.is_swap else 'transfer'

View file

@ -173,11 +173,11 @@ class NewSwap(New):
else:
self.usr_trade_limit = None
def update_vault_addr(self, addr):
def update_vault_addr(self, c, *, addr='inbound_address'):
vault_idx = self.vault_idx
assert vault_idx == 0, f'{vault_idx}: vault index is not zero!'
o = self.outputs[vault_idx]._asdict()
o['addr'] = addr
o['addr'] = getattr(c, addr)
self.outputs[vault_idx] = self.Output(self.proto, **o)
async def update_vault_output(self, amt, *, deduct_est_fee=False):
@ -206,6 +206,7 @@ class NewSwap(New):
break
self.swap_quote_expiry = c.data['expiry']
self.update_vault_addr(c.inbound_address)
self.update_vault_addr(c)
self.update_data_output(trade_limit)
self.quote_data = c
return c.rel_fee_hint

View file

@ -57,7 +57,8 @@ from .include.common import (
get_file_with_ext,
ok_msg,
Ctrl_U,
cleanup_env)
cleanup_env,
thorchain_router_addr_file)
from .base import CmdTestBase
from .shared import CmdTestShared
@ -240,10 +241,11 @@ class CmdTestEthdevMethods:
gas,
mmgen_cmd = 'txdo',
gas_price = '8G',
fn = None,
num = None):
keyfile = joinpath(self.tmpdir, dfl_devkey_fn)
fn = joinpath(self.tmpdir, 'mm'+str(num), key+'.bin')
fn = fn or joinpath(self.tmpdir, 'mm'+str(num), key+'.bin')
args = [
'-B',
f'--fee={gas_price}',
@ -256,6 +258,14 @@ class CmdTestEthdevMethods:
contract_addr = self._get_contract_address(dfl_devaddr)
if key == 'Token':
self.write_to_tmpfile(f'token_addr{num}', contract_addr+'\n')
elif key == 'thorchain_router':
from mmgen.fileutil import write_data_to_file
write_data_to_file(
self.cfg,
thorchain_router_addr_file,
contract_addr + '\n',
ask_overwrite = False,
quiet = True)
if mmgen_cmd == 'txdo':
args += ['-k', keyfile]
@ -1587,7 +1597,7 @@ class CmdTestEthdev(CmdTestEthdevMethods, CmdTestBase, CmdTestShared):
def token_txdo_cached_balances(self):
return self.txdo_cached_balances(
acct = '1',
fee_info_data = ('0.0026', '50'),
fee_info_data = ('0.00375', '50'),
add_args = ['--token=mm1', '98831F3A:E:12,43.21'])
def token_txcreate_refresh_balances(self):

View file

@ -12,11 +12,17 @@
test.cmdtest_d.ethswap: Ethereum swap tests for the cmdtest.py test suite
"""
from subprocess import run, PIPE, DEVNULL
from mmgen.cfg import Config
from mmgen.util import rmsg, die
from mmgen.protocol import init_proto
from mmgen.fileutil import get_data_from_file
from ..include.common import imsg, chk_equal
from .include.runner import CmdTestRunner
from .include.common import dfl_sid
from .include.common import dfl_sid, eth_inbound_addr, thorchain_router_addr_file
from .httpd.thornode import ThornodeServer
from .regtest import CmdTestRegtest
@ -42,6 +48,38 @@ class CmdTestEthSwapMethods:
async def token_deploy_c(self):
return await self._token_deploy_token(num=1)
def token_compile_router(self):
if not self.using_solc:
bin_fn = 'test/ref/ethereum/bin/THORChain_Router.bin'
imsg(f'Using precompiled contract data ‘{bin_fn}')
import shutil
shutil.copy(bin_fn, self.tmpdir)
return 'skip'
imsg("Compiling THORChain router contract")
self.spawn(msg_only=True)
cmd = [
'solc',
'--evm-version=constantinople',
'--overwrite',
f'--output-dir={self.tmpdir}',
'--bin',
'test/ref/ethereum/THORChain_Router.sol']
imsg('Executing: {}'.format(' '.join(cmd)))
cp = run(cmd, stdout=DEVNULL, stderr=PIPE)
if cp.returncode != 0:
rmsg('solc failed with the following output:')
die(2, cp.stderr.decode())
imsg('THORChain router contract compiled')
return 'ok'
async def token_deploy_router(self):
return await self._token_deploy(
key = 'thorchain_router',
gas = 1_000_000,
fn = f'{self.tmpdir}/THORChain_Router.bin')
def token_fund_user(self):
return self._token_transfer_ops(
op = 'fund_user',
@ -54,9 +92,40 @@ class CmdTestEthSwapMethods:
def token_addrimport(self):
return self._token_addrimport('token_addr1', '1-5', expect='5/5')
def token_addrimport_inbound(self):
token_addr = self.read_from_tmpfile('token_addr1').strip()
return self.spawn(
'mmgen-addrimport',
['--quiet', '--regtest=1', f'--token-addr={token_addr}', f'--address={eth_inbound_addr}'])
def token_bal1(self):
return self._token_bal_check(pat=rf'{dfl_sid}:E:1\s+{self.token_fund_amt}\s')
def token_bal2(self):
return self._token_bal_check(pat=rf'{eth_inbound_addr}\s+\S+\s+87.654321\s')
async def _swaptxmemo(self, chk):
from mmgen.proto.eth.contract import Contract
self.spawn(msg_only=True)
addr = get_data_from_file(self.cfg, thorchain_router_addr_file, quiet=True).strip()
c = Contract(self.cfg, self.proto, addr, rpc=await self.rpc)
res = (await c.do_call('saved_memo()'))[2:]
memo_len = int(res[64:128], 16)
chk_equal(bytes.fromhex(res[128:128+(2*memo_len)]).decode(), chk)
imsg(f'saved_memo: {chk}')
return 'ok'
def _swaptxsend_eth_proxy(self, *, add_opts=[], test=False):
t = self._swaptxsend(
add_opts = ['--tx-proxy=eth'] + (['--test'] if test else []) + add_opts,
spawn_only = True)
t.expect('view: ', 'y')
t.expect('continue: ', '\n') # exit swap quote
t.expect('(y/N): ', '\n') # add comment
if not test:
t.expect('to confirm: ', 'YES\n')
return t
class CmdTestEthSwap(CmdTestSwapMethods, CmdTestRegtest):
'Ethereum swap operations'
@ -117,9 +186,12 @@ class CmdTestEthSwap(CmdTestSwapMethods, CmdTestRegtest):
('eth_token_deploy_a', ''),
('eth_token_deploy_b', ''),
('eth_token_deploy_c', ''),
('eth_token_compile_router', ''),
('eth_token_deploy_router', ''),
('eth_token_fund_user', ''),
('eth_token_addrgen', ''),
('eth_token_addrimport', ''),
('eth_token_addrimport_inbound', ''),
('eth_token_bal1', ''),
),
'token_swap': (
@ -153,10 +225,28 @@ class CmdTestEthSwap(CmdTestSwapMethods, CmdTestRegtest):
('eth_bal2', ''),
),
'eth_token_swap': (
'swap operations (ETH <-> MM1)',
'swap operations (ETH -> ERC20, ERC20 -> BTC, ERC20 -> ETH)',
# ETH -> MM1
('eth_swaptxcreate3', ''),
('eth_swaptxsign3', ''),
('eth_swaptxsend3', ''),
# MM1 -> BTC
('eth_swaptxcreate4', ''),
('eth_swaptxsign4', ''),
('eth_swaptxsend4', ''),
('eth_swaptxmemo4', ''),
('eth_swaptxstatus4', ''),
('eth_swaptxreceipt4', ''),
('eth_token_bal2', ''),
# MM1 -> ETH
('eth_swaptxcreate5', ''),
('eth_swaptxsign5', ''),
('eth_etherscan_server_start', ''),
('eth_swaptxsend5_test', ''),
('eth_swaptxsend5a', ''),
('eth_swaptxsend5b', ''),
('eth_swaptxsend5', ''),
('eth_etherscan_server_stop', ''),
),
}
@ -266,9 +356,12 @@ class CmdTestEthSwapEth(CmdTestEthSwapMethods, CmdTestSwapMethods, CmdTestEthdev
('token_deploy_a', 'deploying ERC20 token MM1 (SafeMath)'),
('token_deploy_b', 'deploying ERC20 token MM1 (Owned)'),
('token_deploy_c', 'deploying ERC20 token MM1 (Token)'),
('token_compile_router', 'compiling THORChain router contract'),
('token_deploy_router', 'deploying THORChain router contract'),
('token_fund_user', 'transferring token funds from dev to user'),
('token_addrgen', 'generating token addresses'),
('token_addrimport', 'importing token addresses using token address (MM1)'),
('token_addrimport_inbound', 'importing THORNode inbound token address'),
('token_bal1', 'the token balance'),
# eth_token_swap:
@ -276,6 +369,25 @@ class CmdTestEthSwapEth(CmdTestEthSwapMethods, CmdTestSwapMethods, CmdTestEthdev
('swaptxcreate3', 'creating an ETH->MM1 swap transaction'),
('swaptxsign3', 'signing the transaction'),
('swaptxsend3', 'sending the transaction'),
# MM1 -> BTC
('swaptxcreate4', 'creating an MM1->BTC swap transaction'),
('swaptxsign4', 'signing the transaction'),
('swaptxsend4', 'sending the transaction'),
('swaptxmemo4', 'checking the memo'),
('swaptxstatus4', 'getting the transaction status'),
('swaptxreceipt4', 'getting the transaction receipt'),
('token_bal2', 'the token balance'),
# MM1 -> ETH
('swaptxcreate5', 'creating an MM1->ETH swap transaction'),
('swaptxsign5', 'signing the transaction'),
('etherscan_server_start', 'starting the Etherscan server'),
('swaptxsend5_test', 'testing the transaction via Etherscan'),
('swaptxsend5a', 'sending the transaction via Etherscan (p1)'),
('swaptxsend5b', 'sending the transaction via Etherscan (p2)'),
('swaptxsend5', 'sending the transaction via Etherscan (complete)'),
('etherscan_server_stop', 'stopping the Etherscan server'),
)
def swaptxcreate1(self):
@ -294,6 +406,14 @@ class CmdTestEthSwapEth(CmdTestEthSwapMethods, CmdTestSwapMethods, CmdTestEthdev
t = self._swaptxcreate(['ETH', '8.765', 'ETH.MM1', f'{dfl_sid}:E:5'])
return self._swaptxcreate_ui_common(t)
def swaptxcreate4(self):
t = self._swaptxcreate(['ETH.MM1', '87.654321', 'BTC', f'{dfl_sid}:C:2'])
return self._swaptxcreate_ui_common(t)
def swaptxcreate5(self):
t = self._swaptxcreate(['ETH.MM1', '98.7654321', 'ETH', f'{dfl_sid}:E:12'])
return self._swaptxcreate_ui_common(t)
def swaptxsign1(self):
return self._swaptxsign()
@ -304,8 +424,28 @@ class CmdTestEthSwapEth(CmdTestEthSwapMethods, CmdTestSwapMethods, CmdTestEthdev
self.mining_delay()
return self._swaptxsend(add_opts=['--verbose', '--status'], status=True)
swaptxsign3 = swaptxsign1
swaptxsend3 = swaptxsend1
def swaptxmemo4(self):
return self._swaptxmemo('=:b:mkQsXA7mqDtnUpkaXMbDtAL1KMeof4GPw3:0/1/0')
def swaptxreceipt4(self):
self.mining_delay()
return self._swaptxsend(add_opts=['--receipt'], spawn_only=True)
def swaptxsend5_test(self):
return self._swaptxsend_eth_proxy(test=True)
def swaptxsend5a(self):
return self._swaptxsend_eth_proxy(add_opts=['--txhex-idx=1'])
def swaptxsend5b(self):
return self._swaptxsend_eth_proxy(add_opts=['--txhex-idx=2'])
def swaptxsend5(self):
return self._swaptxsend_eth_proxy()
swaptxsign5 = swaptxsign4 = swaptxsign3 = swaptxsign1
swaptxsend4 = swaptxsend3 = swaptxsend1
swaptxstatus4 = swaptxstatus1
def bal1(self):
return self.bal('swap1')

View file

@ -18,6 +18,8 @@ from mmgen.cfg import Config
from mmgen.amt import UniAmt
from mmgen.protocol import init_proto
from ..include.common import eth_inbound_addr, thorchain_router_addr_file
from . import HTTPD
cfg = Config()
@ -117,14 +119,16 @@ data_template_eth = {
}
def make_inbound_addr(proto, mmtype):
if proto.is_evm:
return '0x' + eth_inbound_addr # non-checksummed as per ninerealms thornode
else:
from mmgen.tool.coin import tool_cmd
n = int(time.time()) // (60 * 60 * 24) # increments once every 24 hrs
ret = tool_cmd(
return 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'
@ -160,4 +164,10 @@ class ThornodeServer(HTTPD):
'recommended_gas_rate': recommended_gas_rate[send_proto.base_proto_coin]
})
if send_asset == 'MM1':
eth_proto = init_proto(cfg, 'eth', network='regtest')
with open(thorchain_router_addr_file) as fh:
raw_addr = fh.read().strip()
data['router'] = '0x' + eth_proto.checksummed_addr(raw_addr)
return json.dumps(data).encode()

View file

@ -71,6 +71,10 @@ chksum_pat = r'\b[A-F0-9]{4} [A-F0-9]{4} [A-F0-9]{4} [A-F0-9]{4}\b'
Ctrl_U = '\x15'
eth_inbound_addr = (28 * '0') + 'feedbeefcafe'
thorchain_router_addr_file = 'test/data_dir/thorchain_router_addr'
def ok_msg():
if cfg.profile:
return

View file

@ -236,6 +236,9 @@ def cmp_or_die(s, t, desc=None):
f'ERROR: recoded data:\n{t!r}\ndiffers from original data:\n{s!r}'
)
def chk_equal(a, b):
assert a == b, f'equality test failed: {a} != {b}'
def init_coverage():
coverdir = os.path.join('test', 'trace')
acc_file = os.path.join('test', 'trace.acc')

View file

@ -3,7 +3,6 @@ from .asset_orig import *
class overlay_fake_THORChainSwapAsset:
assets_data = {
'ETH.USDT': THORChainSwapAsset._ad('Tether (ETH)', None, 'ETH.USDT', None, True),
'ETH.MM1': THORChainSwapAsset._ad('MM1 Token (ETH)', None, 'ETH.MM1', None, True),
'ETH.JUNK': THORChainSwapAsset._ad('Junk Token (ETH)', None, 'ETH.JUNK', None, True),
'ETH.NONE': THORChainSwapAsset._ad('Unavailable Token (ETH)', None, 'ETH.NONE', None, True)

View file

@ -0,0 +1,35 @@
// 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
//
// Minimal THORChain router for testing
//
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.25;
interface iERC20 {
function transferFrom(
address from,
address to,
uint tokens) external payable returns (bool success);
}
contract THORChain_Router {
string public saved_memo;
function depositWithExpiry(
address payable vault,
address asset,
uint amount,
string memory memo,
uint expiration
) external payable returns (bool success) {
require(block.timestamp < expiration, "THORChain_Router: expired");
saved_memo = memo;
return iERC20(asset).transferFrom(msg.sender, vault, amount);
}
}

File diff suppressed because one or more lines are too long