Browse Source

tx.new_swap: swap to ERC20 token support

The MMGen Project 7 months ago
parent
commit
38ea93a51f

+ 32 - 10
mmgen/swap/proto/thorchain/thornode.py

@@ -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

+ 6 - 2
mmgen/tx/completed.py

@@ -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:

+ 62 - 13
test/cmdtest_d/ethswap.py

@@ -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)

+ 55 - 6
test/cmdtest_d/httpd/thornode.py

@@ -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()

+ 2 - 0
test/modtest_d/swap.py

@@ -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)

+ 14 - 0
test/overlay/fakemods/mmgen/swap/proto/thorchain/asset.py

@@ -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