tx.new_swap: swap to ERC20 token support

This commit is contained in:
The MMGen Project 2025-04-21 14:01:16 +00:00
commit 38ea93a51f
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
6 changed files with 171 additions and 31 deletions

View file

@ -12,8 +12,9 @@
swap.proto.thorchain.thornode: THORChain swap protocol network query ops
"""
import json
import time, json
from collections import namedtuple
from ....amt import UniAmt
_gd = namedtuple('gas_unit_data', ['code', 'disp'])
@ -60,15 +61,36 @@ class Thornode:
self.rpc = ThornodeRPCClient(tx)
def get_quote(self):
self.get_str = '/thorchain/quote/swap?from_asset={a}&to_asset={b}&amount={c}'.format(
a = self.tx.send_asset.full_name,
b = self.tx.recv_asset.full_name,
c = self.in_amt.to_unit('satoshi'))
self.result = self.rpc.get(self.get_str)
self.data = json.loads(self.result.content)
if not 'expiry' in self.data:
from ....util import pp_fmt, die
die(2, pp_fmt(self.data))
def get_data(send, recv, amt):
get_str = f'/thorchain/quote/swap?from_asset={send}&to_asset={recv}&amount={amt}'
data = json.loads(self.rpc.get(get_str).content)
if not 'expiry' in data:
from ....util import pp_fmt, die
die(2, pp_fmt(data))
return data
if self.tx.proto.tokensym or self.tx.recv_asset.asset: # token swap
in_data = get_data(
self.tx.send_asset.full_name,
'THOR.RUNE',
self.in_amt.to_unit('satoshi'))
if self.tx.proto.network != 'regtest':
time.sleep(1.1) # ninerealms max request rate 1/sec
out_data = get_data(
'THOR.RUNE',
self.tx.recv_asset.full_name,
in_data['expected_amount_out'])
self.data = in_data | {
'expected_amount_out': out_data['expected_amount_out'],
'fees': out_data['fees'],
'expiry': min(in_data['expiry'], out_data['expiry'])
}
else:
self.data = get_data(
self.tx.send_asset.full_name,
self.tx.recv_asset.full_name,
self.in_amt.to_unit('satoshi'))
async def format_quote(self, trade_limit, usr_trade_limit, *, deduct_est_fee=False):
from ....util import make_timestr, ymsg

View file

@ -79,8 +79,12 @@ class Completed(Base):
assert p.function == 'SWAP', f'{p.function}’: unsupported function in swap memo ‘{text}'
aname = p.chain + (f'.{p.asset}' if p.asset != p.chain else '')
assert aname == self.recv_asset.name, f'invalid memo: {aname} != {self.recv_asset.name}'
assert p.chain == p.asset, f'{p.chain} != {p.asset}: chain/asset mismatch in swap memo ‘{text}'
proto = init_proto(self.cfg, p.asset, network=self.cfg.network, need_amt=True)
proto = init_proto(
self.cfg,
p.chain,
network = self.cfg.network,
tokensym = None if p.chain == p.asset else p.asset,
need_amt = True)
if self.swap_recv_addr_mmid:
mmid = self.swap_recv_addr_mmid
elif self.cfg.allow_non_wallet_swap:

View file

@ -68,17 +68,20 @@ class CmdTestEthSwap(CmdTestSwapMethods, CmdTestRegtest):
eth_group = 'ethswap_eth'
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'),
('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']),
('subgroup.token_init', ['eth_fund']),
('subgroup.token_swap', ['fund', 'token_init']),
('subgroup.eth_token_swap', ['fund', 'token_init']),
('stop', 'stopping regtest daemon'),
('eth_stop', 'stopping Ethereum daemon'),
('thornode_server_stop', 'stopping the Thornode server'),
)
cmd_subgroups = {
'init': (
@ -107,6 +110,23 @@ class CmdTestEthSwap(CmdTestSwapMethods, CmdTestRegtest):
('eth_fund_mmgen_addr2', ''),
('eth_bal1', ''),
),
'token_init': (
'deploying tokens and initializing the ETH token tracking wallet',
('eth_token_compile1', ''),
('eth_token_deploy_a', ''),
('eth_token_deploy_b', ''),
('eth_token_deploy_c', ''),
('eth_token_fund_user', ''),
('eth_token_addrgen', ''),
('eth_token_addrimport', ''),
('eth_token_bal1', ''),
),
'token_swap': (
'token swap operations (BTC -> MM1)',
('swaptxcreate3', 'creating a BTC->MM1 swap transaction'),
('swaptxsign3', 'signing the swap transaction'),
('swaptxsend3', 'sending the swap transaction'),
),
'swap': (
'swap operations (BTC -> ETH)',
('swaptxcreate1', 'creating a BTC->ETH swap transaction'),
@ -131,6 +151,12 @@ class CmdTestEthSwap(CmdTestSwapMethods, CmdTestRegtest):
('eth_swaptxstatus1', ''),
('eth_bal2', ''),
),
'eth_token_swap': (
'swap operations (ETH <-> MM1)',
('eth_swaptxcreate3', ''),
('eth_swaptxsign3', ''),
('eth_swaptxsend3', ''),
),
}
eth_tests = [c[0] for v in tuple(cmd_subgroups.values()) + (cmd_group_in,)
@ -178,8 +204,8 @@ class CmdTestEthSwap(CmdTestSwapMethods, CmdTestRegtest):
def swaptxsend1(self):
return self._swaptxsend()
swaptxsign2 = swaptxsign1
swaptxsend2 = swaptxsend1
swaptxsign2 = swaptxsign3 = swaptxsign1
swaptxsend2 = swaptxsend3 = swaptxsend1
def swaptxbump1(self): # create one-output TX back to self to rescue funds
return self._swaptxbump('40s', output_args=[f'{dfl_sid}:B:1'])
@ -198,6 +224,11 @@ class CmdTestEthSwap(CmdTestSwapMethods, CmdTestRegtest):
def bob_bal3(self):
return self._user_bal_cli('bob', chk='499.77656902')
def swaptxcreate3(self):
t = self._swaptxcreate(['BTC', '0.87654321', 'ETH.MM1', f'{dfl_sid}:E:5'])
t.expect('OK? (Y/n): ', 'y')
return self._swaptxcreate_ui_common(t)
def thornode_server_stop(self):
self.spawn(msg_only=True)
thornode_server.stop()
@ -224,8 +255,19 @@ class CmdTestEthSwapEth(CmdTestEthSwapMethods, CmdTestSwapMethods, CmdTestEthdev
('swaptxsign1', 'signing the transaction'),
('swaptxsend1', 'sending the transaction'),
('swaptxstatus1', 'getting the transaction status (with --verbose)'),
('swaptxcreate3', 'creating an ETH->MM1 swap transaction'),
('swaptxsign3', 'signing the transaction'),
('swaptxsend3', 'sending the transaction'),
('bal1', 'the ETH balance'),
('bal2', 'the ETH balance'),
('token_compile1', 'compiling ERC20 token #1'),
('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_fund_user', 'transferring token funds from dev to user'),
('token_addrgen', 'generating token addresses'),
('token_addrimport', 'importing token addresses using token address (MM1)'),
('token_bal1', 'the token balance'),
)
def swaptxcreate1(self):
@ -240,12 +282,19 @@ class CmdTestEthSwapEth(CmdTestEthSwapMethods, CmdTestSwapMethods, CmdTestEthdev
add_opts = ['--trade-limit=3%']),
expect = ':2019e4/1/0')
def swaptxcreate3(self):
t = self._swaptxcreate(['ETH', '8.765', 'ETH.MM1', f'{dfl_sid}:E:5'])
return self._swaptxcreate_ui_common(t)
def swaptxsign1(self):
return self._swaptxsign()
def swaptxsend1(self):
return self._swaptxsend()
swaptxsign3 = swaptxsign1
swaptxsend3 = swaptxsend1
def swaptxstatus1(self):
self.mining_delay()
return self._swaptxsend(add_opts=['--verbose', '--status'], status=True)

View file

@ -24,10 +24,52 @@ 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, 'ETH': 2304}
prices = {'BTC': 97000, 'LTC': 115, 'BCH': 330, 'ETH': 2304, 'MM1': 0.998, 'RUNE': 1.4}
gas_rate_units = {'ETH': 'gwei', 'BTC': 'satsperbyte'}
recommended_gas_rate = {'ETH': '1', 'BTC': '6'}
data_template_from_rune = {
'outbound_delay_blocks': 0,
'outbound_delay_seconds': 0,
'fees': {
'asset': 'BTC.BTC',
'affiliate': '0',
'outbound': '1182',
'liquidity': '110',
'total': '1292',
'slippage_bps': 7,
'total_bps': 92
},
'warning': 'Do not cache this response. Do not send funds after the expiry.',
'notes': 'Broadcast a MsgDeposit to the THORChain network with the appropriate memo. Do not use multi-in, multi-out transactions.',
'max_streaming_quantity': 0,
'streaming_swap_blocks': 0
}
data_template_to_rune = {
'inbound_confirmation_blocks': 2,
'inbound_confirmation_seconds': 24,
'outbound_delay_blocks': 0,
'outbound_delay_seconds': 0,
'fees': {
'asset': 'THOR.RUNE',
'affiliate': '0',
'outbound': '2000000',
'liquidity': '684966',
'total': '2684966',
'slippage_bps': 8,
'total_bps': 31
},
'router': '0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146',
'warning': 'Do not cache this response. Do not send funds after the expiry.',
'notes': 'Base Asset: Send the inbound_address the asset with the memo encoded in hex in the data field. 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.',
'dust_threshold': '1',
'recommended_gas_rate': '1',
'max_streaming_quantity': 0,
'streaming_swap_blocks': 0,
'total_swap_seconds': 24
}
data_template_btc = {
'inbound_confirmation_blocks': 4,
'inbound_confirmation_seconds': 2400,
@ -98,17 +140,24 @@ class ThornodeServer(HTTPD):
out_amt = in_amt * (prices[send_asset] / prices[recv_asset])
data_template = (
data_template_from_rune if send_asset == 'RUNE' else
data_template_to_rune if recv_asset == 'RUNE' else
data_template_eth if send_asset == 'ETH' else
data_template_btc)
from mmgen.protocol import init_proto
send_proto = init_proto(cfg, send_chain, network='regtest', need_amt=True)
data = data_template | {
'recommended_min_amount_in': str(int(70 * 10**8 / prices[send_asset])), # $70
'expected_amount_out': str(out_amt.to_unit('satoshi')),
'expiry': int(time.time()) + (10 * 60),
'inbound_address': make_inbound_addr(send_proto, send_proto.preferred_mmtypes[0]),
'gas_rate_units': gas_rate_units[send_proto.base_proto_coin],
'recommended_gas_rate': recommended_gas_rate[send_proto.base_proto_coin],
}
if send_asset != 'RUNE':
from mmgen.protocol import init_proto
send_proto = init_proto(cfg, send_chain, network='regtest', need_amt=True)
data.update({
'inbound_address': make_inbound_addr(send_proto, send_proto.preferred_mmtypes[0]),
'gas_rate_units': gas_rate_units[send_proto.base_proto_coin],
'recommended_gas_rate': recommended_gas_rate[send_proto.base_proto_coin]
})
return json.dumps(data).encode()

View file

@ -17,6 +17,7 @@ class unit_tests:
('BTC', 'BTC.BTC', 'b', 'BTC', None, 'recv'),
('LTC', 'LTC.LTC', 'l', 'LTC', None, 'recv'),
('BCH', 'BCH.BCH', 'c', 'BCH', None, 'recv'),
('ETH.USDT', 'ETH.USDT', 'ETH.USDT', 'ETH', 'USDT', 'recv'),
):
a = SwapAsset(name, direction)
vmsg(f' {a.name}')
@ -36,6 +37,7 @@ class unit_tests:
('ltc', 'bech32', 'LTC', None),
('bch', 'compressed', 'BCH', None),
('eth', None, 'ETH', None),
('eth', None, 'ETH.USDT', 'USDT'),
):
proto = init_proto(cfg, coin, tokensym=token, need_amt=True)
addr = make_burn_addr(proto, addrtype)

View file

@ -0,0 +1,14 @@
from .asset_orig import *
class overlay_fake_THORChainSwapAsset:
assets_data = {
'ETH.MM1': THORChainSwapAsset._ad('MM1 Token (ETH)', None, 'ETH.MM1', None),
'ETH.USDT': THORChainSwapAsset._ad('Tether (ETH)', None, 'ETH.USDT', None)
}
send = ('ETH.MM1',)
recv = ('ETH.MM1', 'ETH.USDT')
THORChainSwapAsset.assets_data |= overlay_fake_THORChainSwapAsset.assets_data
THORChainSwapAsset.send += overlay_fake_THORChainSwapAsset.send
THORChainSwapAsset.recv += overlay_fake_THORChainSwapAsset.recv