new_swap.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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. tx.new_swap: new swap transaction class
  12. """
  13. from collections import namedtuple
  14. from .new import New
  15. from ..amt import UniAmt
  16. def get_swap_proto_mod(swap_proto_name):
  17. import importlib
  18. return importlib.import_module(f'mmgen.swap.proto.{swap_proto_name}')
  19. def init_swap_proto(cfg, asset):
  20. from ..protocol import init_proto
  21. return init_proto(
  22. cfg,
  23. asset.chain,
  24. network = cfg._proto.network,
  25. tokensym = asset.asset,
  26. need_amt = True)
  27. def get_send_proto(cfg):
  28. try:
  29. arg = cfg._args.pop(0)
  30. except:
  31. cfg._usage()
  32. return init_swap_proto(cfg, get_swap_proto_mod(cfg.swap_proto).SwapAsset(arg, 'send'))
  33. class NewSwap(New):
  34. desc = 'swap transaction'
  35. swap_quote_refresh_timeout = 30
  36. def __init__(self, *args, **kwargs):
  37. self.is_swap = True
  38. self.swap_proto = kwargs['cfg'].swap_proto
  39. New.__init__(self, *args, **kwargs)
  40. def check_addr_is_wallet_addr(self, output, *, message):
  41. if not output.mmid:
  42. self._non_wallet_addr_confirm(message)
  43. async def get_swap_output(self, proto, arg, addrfiles, desc):
  44. ret = namedtuple('swap_output', ['coin', 'network', 'addr', 'mmid'])
  45. if arg:
  46. from ..addrdata import TwAddrData
  47. pa = self.parse_cmdline_arg(
  48. proto,
  49. arg,
  50. self.get_addrdata_from_files(proto, addrfiles),
  51. await TwAddrData(self.cfg, proto))
  52. if pa.addr:
  53. await self.warn_addr_used(proto, pa, desc)
  54. return ret(proto.coin, proto.network, pa.addr, pa.mmid)
  55. full_desc = f'{desc} on the {proto.coin} {proto.network} network'
  56. res = await self.get_autochg_addr(proto, arg, exclude=[], desc=full_desc, all_addrtypes=not arg)
  57. self.confirm_autoselected_addr(res.twmmid, full_desc)
  58. return ret(proto.coin, proto.network, res.addr, res.twmmid)
  59. async def get_chg_output(self, arg, addrfiles):
  60. chg_output = await self.get_swap_output(self.proto, arg, addrfiles, 'change address')
  61. self.check_addr_is_wallet_addr(
  62. chg_output,
  63. message = 'Change address is not an MMGen wallet address!')
  64. return chg_output
  65. async def process_swap_cmdline_args(self, cmd_args, addrfiles):
  66. class CmdlineArgs: # listed in command-line order
  67. # send_coin # required: uppercase coin symbol
  68. send_amt = None # optional: Omit to skip change addr and send value of all inputs minus fees
  69. # to vault
  70. # chg_spec = None # optional: change address spec, e.g. ‘B’ ‘DEADBEEF:B’ ‘DEADBEEF:B:1’ or
  71. # coin address. Omit for autoselected change address. Use of
  72. # non-wallet change address will emit warning and prompt user
  73. # for confirmation
  74. # recv_coin # required: uppercase coin symbol
  75. recv_spec = None # optional: destination address spec. Same rules as for chg_spec
  76. def get_arg():
  77. try:
  78. return args_in.pop(0)
  79. except:
  80. self.cfg._usage()
  81. async def parse():
  82. # arg 1: send_coin - already popped and parsed by get_send_proto()
  83. from ..amt import is_coin_amt
  84. arg = get_arg()
  85. # arg 2: amt
  86. if is_coin_amt(self.proto, arg):
  87. UniAmt(arg) # throw exception on decimal overflow
  88. args.send_amt = self.proto.coin_amt(arg)
  89. arg = get_arg()
  90. # arg 3: chg_spec (change address spec)
  91. if args.send_amt and not (self.proto.is_evm or arg in sa.recv): # is change arg
  92. nonlocal chg_output
  93. chg_output = await self.get_chg_output(arg, addrfiles)
  94. arg = get_arg()
  95. # arg 4: recv_coin
  96. self.swap_recv_asset_spec = arg # this goes into the transaction file
  97. self.recv_proto = init_swap_proto(self.cfg, self.recv_asset)
  98. # arg 5: recv_spec (receive address spec)
  99. if args_in:
  100. args.recv_spec = get_arg()
  101. if args_in: # done parsing, all args consumed
  102. self.cfg._usage()
  103. sp = self.swap_proto_mod
  104. sa = sp.SwapAsset('BTC', 'send')
  105. args_in = list(cmd_args)
  106. args = CmdlineArgs()
  107. chg_output = None
  108. await parse()
  109. for a in (self.send_asset, self.recv_asset):
  110. if a.name not in sa.tested:
  111. from ..util import msg, ymsg
  112. from ..term import get_char
  113. ymsg(f'Warning: {a.direction} asset {a.name} is untested by the MMGen Project')
  114. get_char('Press any key to continue: ')
  115. msg('')
  116. if args.send_amt and not (chg_output or self.proto.is_evm):
  117. chg_output = await self.get_chg_output(None, addrfiles)
  118. recv_output = await self.get_swap_output(
  119. self.recv_proto,
  120. args.recv_spec,
  121. addrfiles,
  122. 'destination address')
  123. self.check_addr_is_wallet_addr(
  124. recv_output,
  125. message = (
  126. 'Swap destination address is not an MMGen wallet address!\n'
  127. 'To sign this transaction, autosign or txsign must be invoked'
  128. ' with --allow-non-wallet-swap'))
  129. memo = sp.Memo(
  130. self.recv_proto,
  131. self.recv_asset,
  132. recv_output.addr)
  133. # this goes into the transaction file:
  134. self.swap_recv_addr_mmid = recv_output.mmid
  135. return (
  136. [f'vault,{args.send_amt}', f'data:{memo}'] if args.send_amt and self.proto.is_evm else
  137. [f'vault,{args.send_amt}', chg_output.mmid, f'data:{memo}'] if args.send_amt else
  138. ['vault', f'data:{memo}'])
  139. def init_swap_cfg(self):
  140. if s := self.cfg.trade_limit:
  141. self.usr_trade_limit = (
  142. 1 - float(s[:-1]) / 100 if s.endswith('%') else
  143. UniAmt(self.cfg.trade_limit))
  144. else:
  145. self.usr_trade_limit = None
  146. def update_vault_addr(self, c, *, addr='inbound_address'):
  147. vault_idx = self.vault_idx
  148. assert vault_idx == 0, f'{vault_idx}: vault index is not zero!'
  149. o = self.outputs[vault_idx]._asdict()
  150. o['addr'] = getattr(c, addr)
  151. self.outputs[vault_idx] = self.Output(self.proto, **o)
  152. async def update_vault_output(self, amt, *, deduct_est_fee=False):
  153. c = self.swap_proto_mod.rpc_client(self, amt)
  154. import time
  155. from ..util import msg
  156. from ..term import get_char
  157. def get_trade_limit():
  158. if type(self.usr_trade_limit) is UniAmt:
  159. return self.usr_trade_limit
  160. elif type(self.usr_trade_limit) is float:
  161. return (
  162. UniAmt(int(c.data['expected_amount_out']), from_unit='satoshi')
  163. * self.usr_trade_limit)
  164. while True:
  165. self.cfg._util.qmsg(f'Retrieving data from {c.rpc.host}...')
  166. c.get_quote()
  167. self.cfg._util.qmsg('OK')
  168. self.swap_quote_refresh_time = time.time()
  169. await self.set_gas(to_addr=c.router if self.is_token else None)
  170. trade_limit = get_trade_limit()
  171. msg(await c.format_quote(trade_limit, deduct_est_fee=deduct_est_fee))
  172. ch = get_char('Press ‘r’ to refresh quote, any other key to continue: ')
  173. msg('')
  174. if ch not in 'Rr':
  175. break
  176. self.swap_quote_expiry = c.data['expiry']
  177. self.update_vault_addr(c)
  178. self.update_data_output(trade_limit)
  179. self.quote_data = c
  180. return c.rel_fee_hint