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:
parent
c449e4d4d9
commit
ff28d39a3c
23 changed files with 371 additions and 35 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
15.1.dev30
|
||||
15.1.dev31
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 '')
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
35
test/ref/ethereum/THORChain_Router.sol
Normal file
35
test/ref/ethereum/THORChain_Router.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
1
test/ref/ethereum/bin/THORChain_Router.bin
Normal file
1
test/ref/ethereum/bin/THORChain_Router.bin
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue