swap.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  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.httpd.thornode.swap: Thornode swap HTTP server
  12. """
  13. import time, re, json
  14. from wsgiref.util import request_uri
  15. from mmgen.cfg import Config
  16. from mmgen.amt import UniAmt
  17. from mmgen.protocol import init_proto
  18. from ...include.common import eth_inbound_addr, thorchain_router_addr_file
  19. from . import ThornodeServer
  20. cfg = Config()
  21. # https://thornode.ninerealms.com/thorchain/quote/swap?from_asset=BCH.BCH&to_asset=LTC.LTC&amount=1000000
  22. prices = {'BTC': 97000, 'LTC': 115, 'BCH': 330, 'ETH': 2304, 'MM1': 0.998, 'RUNE': 1.4}
  23. gas_rate_units = {'ETH': 'gwei', 'BTC': 'satsperbyte'}
  24. recommended_gas_rate = {'ETH': '1', 'BTC': '6'}
  25. data_template_from_rune = {
  26. 'outbound_delay_blocks': 0,
  27. 'outbound_delay_seconds': 0,
  28. 'fees': {
  29. 'asset': 'BTC.BTC',
  30. 'affiliate': '0',
  31. 'outbound': '1182',
  32. 'liquidity': '110',
  33. 'total': '1292',
  34. 'slippage_bps': 7,
  35. 'total_bps': 92
  36. },
  37. 'warning': 'Do not cache this response. Do not send funds after the expiry.',
  38. 'notes': 'Broadcast a MsgDeposit to the THORChain network with the appropriate memo. Do not use multi-in, multi-out transactions.',
  39. 'max_streaming_quantity': 0,
  40. 'streaming_swap_blocks': 0
  41. }
  42. data_template_to_rune = {
  43. 'inbound_confirmation_blocks': 2,
  44. 'inbound_confirmation_seconds': 24,
  45. 'outbound_delay_blocks': 0,
  46. 'outbound_delay_seconds': 0,
  47. 'fees': {
  48. 'asset': 'THOR.RUNE',
  49. 'affiliate': '0',
  50. 'outbound': '2000000',
  51. 'liquidity': '684966',
  52. 'total': '2684966',
  53. 'slippage_bps': 8,
  54. 'total_bps': 31
  55. },
  56. 'router': '0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146',
  57. 'warning': 'Do not cache this response. Do not send funds after the expiry.',
  58. '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.',
  59. 'dust_threshold': '1',
  60. 'recommended_gas_rate': '1',
  61. 'max_streaming_quantity': 0,
  62. 'streaming_swap_blocks': 0,
  63. 'total_swap_seconds': 24
  64. }
  65. data_template_btc = {
  66. 'inbound_confirmation_blocks': 4,
  67. 'inbound_confirmation_seconds': 2400,
  68. 'outbound_delay_blocks': 5,
  69. 'outbound_delay_seconds': 30,
  70. 'fees': {
  71. 'asset': 'LTC.LTC',
  72. 'affiliate': '0',
  73. 'outbound': '878656',
  74. 'liquidity': '8945012',
  75. 'total': '9823668',
  76. 'slippage_bps': 31,
  77. 'total_bps': 34
  78. },
  79. 'warning': 'Do not cache this response. Do not send funds after the expiry.',
  80. 'notes': 'First output should be to inbound_address, second output should be change back to self, third output should be OP_RETURN, limited to 80 bytes. Do not send below the dust threshold. Do not use exotic spend scripts, locks or address formats.',
  81. 'dust_threshold': '10000',
  82. 'max_streaming_quantity': 0,
  83. 'streaming_swap_blocks': 0,
  84. 'total_swap_seconds': 2430
  85. }
  86. data_template_eth = {
  87. 'inbound_confirmation_blocks': 2,
  88. 'inbound_confirmation_seconds': 24,
  89. 'outbound_delay_blocks': 0,
  90. 'outbound_delay_seconds': 0,
  91. 'fees': {
  92. 'asset': 'BTC.BTC',
  93. 'affiliate': '0',
  94. 'outbound': '1097',
  95. 'liquidity': '77',
  96. 'total': '1174',
  97. 'slippage_bps': 15,
  98. 'total_bps': 237
  99. },
  100. 'router': '0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146',
  101. 'warning': 'Do not cache this response. Do not send funds after the expiry.',
  102. '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.',
  103. 'recommended_gas_rate': '1',
  104. 'max_streaming_quantity': 0,
  105. 'streaming_swap_blocks': 0,
  106. 'total_swap_seconds': 24
  107. }
  108. def make_inbound_addr(proto, mmtype):
  109. if proto.is_evm:
  110. return '0x' + eth_inbound_addr # non-checksummed as per ninerealms thornode
  111. else:
  112. from mmgen.tool.coin import tool_cmd
  113. n = int(time.time()) // (60 * 60 * 24) # increments once every 24 hrs
  114. return tool_cmd(
  115. cfg = cfg,
  116. cmdname = 'pubhash2addr',
  117. proto = proto,
  118. mmtype = mmtype).pubhash2addr(f'{n:040x}')
  119. class ThornodeSwapServer(ThornodeServer):
  120. port = 18900
  121. name = 'thornode swap server'
  122. request_pat = r'/thorchain/quote/swap\?from_asset=(\S+)\.(\S+)&to_asset=(\S+)\.(\S+)&amount=(\d+)'
  123. def make_response_body(self, method, environ):
  124. m = re.search(self.request_pat, request_uri(environ))
  125. send_chain, send_asset, recv_chain, recv_asset, amt_atomic = m.groups()
  126. in_amt = UniAmt(int(amt_atomic), from_unit='satoshi')
  127. out_amt = in_amt * (prices[send_asset] / prices[recv_asset])
  128. data_template = (
  129. data_template_from_rune if send_asset == 'RUNE' else
  130. data_template_to_rune if recv_asset == 'RUNE' else
  131. data_template_eth if send_asset == 'ETH' else
  132. data_template_btc)
  133. data = data_template | {
  134. 'recommended_min_amount_in': str(int(70 * 10**8 / prices[send_asset])), # $70
  135. 'expected_amount_out': str(out_amt.to_unit('satoshi')),
  136. 'expiry': int(time.time()) + (10 * 60),
  137. }
  138. if send_asset != 'RUNE':
  139. send_proto = init_proto(cfg, send_chain, network='regtest', need_amt=True)
  140. data.update({
  141. 'inbound_address': make_inbound_addr(send_proto, send_proto.preferred_mmtypes[0]),
  142. 'gas_rate_units': gas_rate_units[send_proto.base_proto_coin],
  143. 'recommended_gas_rate': recommended_gas_rate[send_proto.base_proto_coin]
  144. })
  145. if send_asset == 'MM1':
  146. eth_proto = init_proto(cfg, 'eth', network='regtest')
  147. with open(thorchain_router_addr_file) as fh:
  148. raw_addr = fh.read().strip()
  149. data['router'] = '0x' + eth_proto.checksummed_addr(raw_addr)
  150. return json.dumps(data).encode()