ethswap.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. test.cmdtest_d.ethswap: Ethereum swap tests for the cmdtest.py test suite
  12. """
  13. from subprocess import run, PIPE, DEVNULL
  14. from mmgen.cfg import Config
  15. from mmgen.util import msg_r, rmsg, die
  16. from mmgen.protocol import init_proto
  17. from mmgen.fileutil import get_data_from_file
  18. from ..include.common import imsg, chk_equal
  19. from .include.runner import CmdTestRunner
  20. from .include.common import dfl_sid, eth_inbound_addr, thorchain_router_addr_file
  21. from .httpd.thornode import ThornodeServer
  22. from .regtest import CmdTestRegtest
  23. from .swap import CmdTestSwapMethods
  24. from .ethdev import CmdTestEthdev
  25. thornode_server = ThornodeServer()
  26. method_template = """
  27. def {name}(self):
  28. self.spawn(log_only=True)
  29. return ethswap_eth.run_test("{eth_name}", sub=True)
  30. """
  31. class CmdTestEthSwapMethods:
  32. async def token_deploy_a(self):
  33. return await self._token_deploy_math(num=1)
  34. async def token_deploy_b(self):
  35. return await self._token_deploy_owned(num=1)
  36. async def token_deploy_c(self):
  37. return await self._token_deploy_token(num=1)
  38. def token_compile_router(self):
  39. if not self.using_solc:
  40. bin_fn = 'test/ref/ethereum/bin/THORChain_Router.bin'
  41. imsg(f'Using precompiled contract data ‘{bin_fn}’')
  42. import shutil
  43. shutil.copy(bin_fn, self.tmpdir)
  44. return 'skip'
  45. imsg("Compiling THORChain router contract")
  46. self.spawn(msg_only=True)
  47. cmd = [
  48. 'solc',
  49. '--evm-version=constantinople',
  50. '--overwrite',
  51. f'--output-dir={self.tmpdir}',
  52. '--bin',
  53. 'test/ref/ethereum/THORChain_Router.sol']
  54. imsg('Executing: {}'.format(' '.join(cmd)))
  55. cp = run(cmd, stdout=DEVNULL, stderr=PIPE)
  56. if cp.returncode != 0:
  57. rmsg('solc failed with the following output:')
  58. die(2, cp.stderr.decode())
  59. imsg('THORChain router contract compiled')
  60. return 'ok'
  61. async def token_deploy_router(self):
  62. return await self._token_deploy(
  63. key = 'thorchain_router',
  64. gas = 1_000_000,
  65. fn = f'{self.tmpdir}/THORChain_Router.bin')
  66. def token_fund_user(self):
  67. return self._token_transfer_ops(
  68. op = 'fund_user',
  69. mm_idxs = [1, 2, 12],
  70. token_addr = 'token_addr1',
  71. amt = self.token_fund_amt)
  72. def token_addrgen(self):
  73. return self._token_addrgen(mm_idxs=[1], naddrs=12)
  74. def token_addrimport(self):
  75. return self._token_addrimport('token_addr1', '1-12', expect='12/12')
  76. def token_addrimport_inbound(self):
  77. token_addr = self.read_from_tmpfile('token_addr1').strip()
  78. return self.spawn(
  79. 'mmgen-addrimport',
  80. ['--quiet', '--regtest=1', f'--token-addr={token_addr}', f'--address={eth_inbound_addr}'])
  81. def token_bal1(self):
  82. return self._token_bal_check(pat=rf'{dfl_sid}:E:1\s+{self.token_fund_amt}\s')
  83. def token_bal2(self):
  84. return self._token_bal_check(pat=rf'{eth_inbound_addr}\s+\S+\s+87.654321\s')
  85. async def _check_token_swaptx_memo(self, chk):
  86. from mmgen.proto.eth.contract import Contract
  87. self.spawn(msg_only=True)
  88. addr = get_data_from_file(self.cfg, thorchain_router_addr_file, quiet=True).strip()
  89. c = Contract(self.cfg, self.proto, addr, rpc=await self.rpc)
  90. res = (await c.do_call('saved_memo()'))[2:]
  91. memo_len = int(res[64:128], 16)
  92. chk_equal(bytes.fromhex(res[128:128+(2*memo_len)]).decode(), chk)
  93. imsg(f'saved_memo: {chk}')
  94. return 'ok'
  95. def _swaptxsend_eth_proxy(self, *, add_opts=[], test=False):
  96. t = self._swaptxsend(
  97. add_opts = ['--tx-proxy=eth'] + (['--test'] if test else []) + add_opts,
  98. spawn_only = True)
  99. t.expect('view: ', 'y')
  100. t.expect('continue: ', '\n') # exit swap quote
  101. t.expect('(y/N): ', '\n') # add comment
  102. if not test:
  103. t.expect('to confirm: ', 'YES\n')
  104. return t
  105. class CmdTestEthSwap(CmdTestSwapMethods, CmdTestRegtest):
  106. 'Ethereum swap operations'
  107. bdb_wallet = True
  108. tmpdir_nums = [47]
  109. networks = ('btc',)
  110. passthru_opts = ('coin', 'rpc_backend', 'eth_daemon_id')
  111. eth_group = 'ethswap_eth'
  112. cmd_group_in = (
  113. ('setup', 'regtest (Bob and Alice) mode setup'),
  114. ('eth_setup', 'Ethereum devnet setup'),
  115. ('subgroup.init', []),
  116. ('subgroup.fund', ['init']),
  117. ('subgroup.eth_init', []),
  118. ('subgroup.eth_fund', ['eth_init']),
  119. ('subgroup.swap', ['fund', 'eth_fund']),
  120. ('subgroup.eth_swap', ['fund', 'eth_fund']),
  121. ('subgroup.token_init', ['eth_fund']),
  122. ('subgroup.token_swap', ['fund', 'token_init']),
  123. ('subgroup.eth_token_swap', ['fund', 'token_init']),
  124. ('stop', 'stopping regtest daemon'),
  125. ('eth_stop', 'stopping Ethereum daemon'),
  126. ('thornode_server_stop', 'stopping the Thornode server'),
  127. )
  128. cmd_subgroups = {
  129. 'init': (
  130. 'creating Bob’s MMGen wallet and tracking wallet',
  131. ('walletconv_bob', 'wallet creation (Bob)'),
  132. ('addrgen_bob', 'address generation (Bob)'),
  133. ('addrimport_bob', 'importing Bob’s addresses'),
  134. ),
  135. 'fund': (
  136. 'funding Bob’s wallet',
  137. ('bob_import_miner_addr', 'importing miner’s coinbase addr into Bob’s wallet'),
  138. ('fund_bob', 'funding Bob’s wallet'),
  139. ('generate', 'mining a block'),
  140. ('bob_bal1', 'Bob’s balance'),
  141. ),
  142. 'eth_init': (
  143. 'initializing the ETH tracking wallet',
  144. ('eth_addrgen', ''),
  145. ('eth_addrimport', ''),
  146. ('eth_addrimport_devaddr', ''),
  147. ('eth_addrimport_reth_devaddr', ''),
  148. ('eth_fund_devaddr', ''),
  149. ('eth_del_reth_devaddr', ''),
  150. ),
  151. 'eth_fund': (
  152. 'funding the ETH tracking wallet',
  153. ('eth_fund_mmgen_addr1', ''),
  154. ('eth_fund_mmgen_addr1b', ''),
  155. ('eth_fund_mmgen_addr2', ''),
  156. ('eth_bal1', ''),
  157. ),
  158. 'token_init': (
  159. 'deploying tokens and initializing the ETH token tracking wallet',
  160. ('eth_token_compile1', ''),
  161. ('eth_token_deploy_a', ''),
  162. ('eth_token_deploy_b', ''),
  163. ('eth_token_deploy_c', ''),
  164. ('eth_token_compile_router', ''),
  165. ('eth_token_deploy_router', ''),
  166. ('eth_token_fund_user', ''),
  167. ('eth_token_addrgen', ''),
  168. ('eth_token_addrimport', ''),
  169. ('eth_token_addrimport_inbound', ''),
  170. ('eth_token_bal1', ''),
  171. ),
  172. 'token_swap': (
  173. 'token swap operations (BTC -> MM1)',
  174. ('swaptxcreate3', 'creating a BTC->MM1 swap transaction'),
  175. ('swaptxsign3', 'signing the swap transaction'),
  176. ('swaptxsend3', 'sending the swap transaction'),
  177. ),
  178. 'swap': (
  179. 'swap operations (BTC -> ETH)',
  180. ('swaptxcreate1', 'creating a BTC->ETH swap transaction'),
  181. ('swaptxcreate2', 'creating a BTC->ETH swap transaction (used account)'),
  182. ('swaptxsign1', 'signing the swap transaction'),
  183. ('swaptxsend1', 'sending the swap transaction'),
  184. ('swaptxbump1', 'bumping the swap transaction'),
  185. ('swaptxsign2', 'signing the bump transaction'),
  186. ('swaptxsend2', 'sending the bump transaction'),
  187. ('generate', 'generating a block'),
  188. ('bob_bal2', 'Bob’s balance'),
  189. ('swaptxdo1', 'creating, signing and sending a swap transaction'),
  190. ('generate', 'generating a block'),
  191. ('bob_bal3', 'Bob’s balance'),
  192. ),
  193. 'eth_swap': (
  194. 'swap operations (ETH -> BTC)',
  195. ('eth_swaptxcreate1', ''),
  196. ('eth_swaptxcreate2', ''),
  197. ('eth_swaptxsign1', ''),
  198. ('eth_swaptxsend1', ''),
  199. ('eth_swaptxstatus1', ''),
  200. ('eth_bal2', ''),
  201. ),
  202. 'eth_token_swap': (
  203. 'swap operations (ETH -> ERC20, ERC20 -> BTC, ERC20 -> ETH)',
  204. # ETH -> MM1
  205. ('eth_swaptxcreate3a', ''),
  206. ('eth_swaptxcreate3b', ''),
  207. ('eth_swaptxsign3', ''),
  208. ('eth_swaptxsend3', ''),
  209. ('eth_swaptxmemo3', ''),
  210. # MM1 -> BTC
  211. ('eth_swaptxcreate4', ''),
  212. ('eth_swaptxsign4', ''),
  213. ('eth_swaptxsend4', ''),
  214. ('eth_swaptxmemo4', ''),
  215. ('eth_swaptxstatus4', ''),
  216. ('eth_swaptxreceipt4', ''),
  217. ('eth_token_bal2', ''),
  218. # MM1 -> ETH
  219. ('eth_swaptxcreate5a', ''),
  220. ('eth_swaptxcreate5b', ''),
  221. ('eth_swaptxsign5', ''),
  222. ('eth_etherscan_server_start', ''),
  223. ('eth_swaptxsend5_test', ''),
  224. ('eth_swaptxsend5a', ''),
  225. ('eth_swaptxsend5b', ''),
  226. ('eth_swaptxsend5', ''),
  227. ('eth_etherscan_server_stop', ''),
  228. ),
  229. }
  230. eth_tests = [c[0] for v in tuple(cmd_subgroups.values()) + (cmd_group_in,)
  231. for c in v if isinstance(c, tuple) and c[0].startswith('eth_')]
  232. exec(''.join(method_template.format(name=k, eth_name=k.removeprefix('eth_')) for k in eth_tests))
  233. def __init__(self, cfg, trunner, cfgs, spawn):
  234. super().__init__(cfg, trunner, cfgs, spawn)
  235. if not trunner:
  236. return
  237. global ethswap_eth
  238. cfg = Config({
  239. '_clone': trunner.cfg,
  240. 'coin': 'eth',
  241. 'eth_daemon_id': trunner.cfg.eth_daemon_id,
  242. 'resume': None,
  243. 'resuming': None,
  244. 'resume_after': None,
  245. 'exit_after': None,
  246. 'log': None})
  247. t = trunner
  248. ethswap_eth = CmdTestRunner(cfg, t.repo_root, t.data_dir, t.trash_dir, t.trash_dir2)
  249. ethswap_eth.init_group(self.eth_group)
  250. thornode_server.start()
  251. def swaptxcreate1(self):
  252. t = self._swaptxcreate(['BTC', '8.765', 'ETH'])
  253. t.expect('OK? (Y/n): ', 'y')
  254. t.expect(':E:2')
  255. t.expect('OK? (Y/n): ', 'y')
  256. return self._swaptxcreate_ui_common(t)
  257. def swaptxcreate2(self):
  258. t = self._swaptxcreate(['BTC', '8.765', 'ETH', f'{dfl_sid}:E:1'])
  259. t.expect('OK? (Y/n): ', 'y')
  260. return self._swaptxcreate_ui_common(t)
  261. def swaptxsign1(self):
  262. return self._swaptxsign()
  263. def swaptxsend1(self):
  264. return self._swaptxsend()
  265. swaptxsign3 = swaptxsign2 = swaptxsign1
  266. swaptxsend3 = swaptxsend2 = swaptxsend1
  267. def swaptxbump1(self): # create one-output TX back to self to rescue funds
  268. return self._swaptxbump('40s', output_args=[f'{dfl_sid}:B:1'])
  269. def swaptxdo1(self):
  270. return self._swaptxcreate_ui_common(
  271. self._swaptxcreate(
  272. ['BTC', '0.223344', f'{dfl_sid}:B:3', 'ETH', f'{dfl_sid}:E:2'],
  273. action = 'txdo'),
  274. sign_and_send = True,
  275. file_desc = 'Sent transaction')
  276. def bob_bal2(self):
  277. return self._user_bal_cli('bob', chk='499.9999252')
  278. def bob_bal3(self):
  279. return self._user_bal_cli('bob', chk='499.77656902')
  280. def swaptxcreate3(self):
  281. t = self._swaptxcreate(['BTC', '0.87654321', 'ETH.MM1', f'{dfl_sid}:E:5'])
  282. t.expect('OK? (Y/n): ', 'y')
  283. return self._swaptxcreate_ui_common(t)
  284. def thornode_server_stop(self):
  285. self.spawn(msg_only=True)
  286. if self.cfg.no_daemon_stop:
  287. msg_r('(leaving thornode server running by user request)')
  288. imsg('')
  289. else:
  290. thornode_server.stop()
  291. return 'ok'
  292. class CmdTestEthSwapEth(CmdTestEthSwapMethods, CmdTestSwapMethods, CmdTestEthdev):
  293. 'Ethereum swap operations - Ethereum wallet'
  294. networks = ('eth',)
  295. tmpdir_nums = [48]
  296. fund_amt = '123.456'
  297. token_fund_amt = 1000
  298. bals = lambda self, k: {
  299. 'swap1': [('98831F3A:E:1', '123.456')],
  300. 'swap2': [('98831F3A:E:1', '114.690978056')],
  301. }[k]
  302. cmd_group_in = CmdTestEthdev.cmd_group_in + (
  303. # eth_fund:
  304. ('fund_mmgen_addr1', 'funding user address :1)'),
  305. ('fund_mmgen_addr1b', 'funding user address :3)'),
  306. ('fund_mmgen_addr2', 'funding user address :11)'),
  307. ('bal1', 'the ETH balance'),
  308. # eth_swap:
  309. ('swaptxcreate1', 'creating an ETH->BTC swap transaction'),
  310. ('swaptxcreate2', 'creating an ETH->BTC swap transaction (spec address, trade limit)'),
  311. ('swaptxsign1', 'signing the transaction'),
  312. ('swaptxsend1', 'sending the transaction'),
  313. ('swaptxstatus1', 'getting the transaction status (with --verbose)'),
  314. ('bal2', 'the ETH balance'),
  315. # token_init:
  316. ('token_compile1', 'compiling ERC20 token #1'),
  317. ('token_deploy_a', 'deploying ERC20 token MM1 (SafeMath)'),
  318. ('token_deploy_b', 'deploying ERC20 token MM1 (Owned)'),
  319. ('token_deploy_c', 'deploying ERC20 token MM1 (Token)'),
  320. ('token_compile_router', 'compiling THORChain router contract'),
  321. ('token_deploy_router', 'deploying THORChain router contract'),
  322. ('token_fund_user', 'transferring token funds from dev to user'),
  323. ('token_addrgen', 'generating token addresses'),
  324. ('token_addrimport', 'importing token addresses using token address (MM1)'),
  325. ('token_addrimport_inbound', 'importing THORNode inbound token address'),
  326. ('token_bal1', 'the token balance'),
  327. # eth_token_swap:
  328. # ETH -> MM1
  329. ('swaptxcreate3a', 'creating an ETH->MM1 swap transaction'),
  330. ('swaptxcreate3b', 'creating an ETH->MM1 swap transaction (specific address)'),
  331. ('swaptxsign3', 'signing the transaction'),
  332. ('swaptxsend3', 'sending the transaction'),
  333. ('swaptxmemo3', 'the memo of the sent transaction'),
  334. # MM1 -> BTC
  335. ('swaptxcreate4', 'creating an MM1->BTC swap transaction'),
  336. ('swaptxsign4', 'signing the transaction'),
  337. ('swaptxsend4', 'sending the transaction'),
  338. ('swaptxmemo4', 'checking the memo'),
  339. ('swaptxstatus4', 'getting the transaction status'),
  340. ('swaptxreceipt4', 'getting the transaction receipt'),
  341. ('token_bal2', 'the token balance'),
  342. # MM1 -> ETH
  343. ('swaptxcreate5a', 'creating an MM1->ETH swap transaction'),
  344. ('swaptxcreate5b', 'creating an MM1->ETH swap transaction (specific address)'),
  345. ('swaptxsign5', 'signing the transaction'),
  346. ('etherscan_server_start', 'starting the Etherscan server'),
  347. ('swaptxsend5_test', 'testing the transaction via Etherscan'),
  348. ('swaptxsend5a', 'sending the transaction via Etherscan (p1)'),
  349. ('swaptxsend5b', 'sending the transaction via Etherscan (p2)'),
  350. ('swaptxsend5', 'sending the transaction via Etherscan (complete)'),
  351. ('etherscan_server_stop', 'stopping the Etherscan server'),
  352. )
  353. def fund_mmgen_addr1b(self):
  354. return self._fund_mmgen_addr(arg=f'{dfl_sid}:E:3,0.001')
  355. def swaptxcreate1(self):
  356. t = self._swaptxcreate(['ETH', '8.765', 'BTC'])
  357. t.expect('OK? (Y/n): ', 'y')
  358. return self._swaptxcreate_ui_common(t)
  359. def swaptxcreate2(self):
  360. return self._swaptxcreate_ui_common(
  361. self._swaptxcreate(
  362. ['ETH', '8.765', 'BTC', f'{dfl_sid}:B:4'],
  363. add_opts = ['--trade-limit=3%' ,'--stream-interval=7']),
  364. expect = ':2019e4/7/0')
  365. def swaptxcreate3a(self):
  366. t = self._swaptxcreate(['ETH', '0.7654321', 'ETH.MM1'], add_opts=['--gas=fallback'])
  367. t.expect(f'{dfl_sid}:E:4') # check that correct unused address was found
  368. t.expect('(Y/n): ', 'y')
  369. return self._swaptxcreate_ui_common(t)
  370. def swaptxcreate3b(self):
  371. t = self._swaptxcreate(['ETH', '8.765', 'ETH.MM1', f'{dfl_sid}:E:5'], add_opts=['--gas=auto'])
  372. return self._swaptxcreate_ui_common(t)
  373. async def swaptxmemo3(self):
  374. self.spawn(msg_only=True)
  375. import json
  376. fn = self.get_file_with_ext('sigtx')
  377. tx = json.loads(get_data_from_file(self.cfg, fn, quiet=True).strip())
  378. txid = tx['MMGenTransaction']['coin_txid']
  379. chk = '=:ETH.MM1:0x48596c861c970eb4ca72c5082ff7fecd8ee5be9d:0/3/0' # E:5
  380. imsg(f'TxID: {txid}\nmemo: {chk}')
  381. res = await (await self.rpc).call('eth_getTransactionByHash', '0x' + txid)
  382. chk_equal(bytes.fromhex(res['input'].removeprefix('0x')).decode(), chk)
  383. return 'ok'
  384. def swaptxcreate4(self):
  385. t = self._swaptxcreate(['ETH.MM1', '87.654321', 'BTC', f'{dfl_sid}:C:2'], add_opts=['--gas=auto'])
  386. return self._swaptxcreate_ui_common(t)
  387. def swaptxcreate5a(self):
  388. t = self._swaptxcreate(
  389. ['ETH.MM1', '98.7654321', 'ETH'],
  390. add_opts = ['--gas=58000', '--router-gas=500000'])
  391. t.expect(f'{dfl_sid}:E:13') # check that correct unused address was found
  392. t.expect('(Y/n): ', 'y')
  393. return self._swaptxcreate_ui_common(t)
  394. def swaptxcreate5b(self):
  395. t = self._swaptxcreate(['ETH.MM1', '98.7654321', 'ETH', f'{dfl_sid}:E:12'])
  396. return self._swaptxcreate_ui_common(t)
  397. def swaptxsign1(self):
  398. return self._swaptxsign()
  399. def swaptxsend1(self):
  400. return self._swaptxsend()
  401. def swaptxstatus1(self):
  402. return self._swaptxsend(add_opts=['--verbose', '--status'], status=True)
  403. def swaptxmemo4(self):
  404. import time
  405. time.sleep(1)
  406. return self._check_token_swaptx_memo('=:b:mkQsXA7mqDtnUpkaXMbDtAL1KMeof4GPw3:0/3/0')
  407. def swaptxreceipt4(self):
  408. return self._swaptxsend(add_opts=['--receipt'], spawn_only=True)
  409. def swaptxsend5_test(self):
  410. return self._swaptxsend_eth_proxy(test=True)
  411. def swaptxsend5a(self):
  412. return self._swaptxsend_eth_proxy(add_opts=['--txhex-idx=1'])
  413. def swaptxsend5b(self):
  414. return self._swaptxsend_eth_proxy(add_opts=['--txhex-idx=2'])
  415. def swaptxsend5(self):
  416. return self._swaptxsend_eth_proxy()
  417. swaptxsign5 = swaptxsign4 = swaptxsign3 = swaptxsign1
  418. swaptxsend4 = swaptxsend3 = swaptxsend1
  419. swaptxstatus4 = swaptxstatus1
  420. def bal1(self):
  421. return self.bal('swap1')
  422. def bal2(self):
  423. return self.bal('swap2')