new_swap.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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. proto.btc.tx.new_swap: Bitcoin new swap transaction class
  12. """
  13. from collections import namedtuple
  14. from ....cfg import gc
  15. from ....tx.new_swap import NewSwap as TxNewSwap
  16. from .new import New
  17. class NewSwap(New, TxNewSwap):
  18. desc = 'Bitcoin swap transaction'
  19. async def get_swap_output(self, proto, arg, addrfiles, desc):
  20. ret = namedtuple('swap_output', ['coin', 'network', 'addr', 'mmid'])
  21. if arg:
  22. from ..addrdata import TwAddrData
  23. pa = self.parse_cmdline_arg(
  24. proto,
  25. arg,
  26. self.get_addrdata_from_files(proto, addrfiles),
  27. await TwAddrData(self.cfg, proto, twctl=None)) # TODO: twctl required for Ethereum
  28. if pa.addr:
  29. await self.warn_addr_used(proto, pa, desc)
  30. return ret(proto.coin, proto.network, pa.addr, pa.mmid)
  31. full_desc = '{} on the {} {} network'.format(desc, proto.coin, proto.network)
  32. res = await self.get_autochg_addr(proto, arg, exclude=[], desc=full_desc, all_addrtypes=not arg)
  33. self.confirm_autoselected_addr(res.twmmid, full_desc)
  34. return ret(proto.coin, proto.network, res.addr, res.twmmid)
  35. async def process_swap_cmdline_args(self, cmd_args, addrfiles):
  36. class CmdlineArgs: # listed in command-line order
  37. # send_coin # required: uppercase coin symbol
  38. send_amt = None # optional: Omit to skip change addr and send value of all inputs minus fees
  39. # to vault
  40. chg_spec = None # optional: change address spec, e.g. ‘B’ ‘DEADBEEF:B’ ‘DEADBEEF:B:1’ or coin
  41. # address. Omit for autoselected change address. Use of non-wallet
  42. # change address will emit warning and prompt user for confirmation
  43. # recv_coin # required: uppercase coin symbol
  44. recv_spec = None # optional: destination address spec. Same rules as for chg_spec
  45. def check_coin_arg(coin, desc):
  46. if coin not in sp.params.coins[desc]:
  47. raise ValueError(f'{coin!r}: unsupported {desc} coin for {gc.proj_name} {sp.name} swap')
  48. return coin
  49. def get_arg():
  50. try:
  51. return args_in.pop(0)
  52. except:
  53. self.cfg._usage()
  54. def init_proto_from_coin(coinsym, desc):
  55. return init_proto(
  56. self.cfg,
  57. check_coin_arg(coinsym, desc),
  58. network = self.proto.network,
  59. need_amt = True)
  60. def parse():
  61. from ....amt import is_coin_amt
  62. arg = get_arg()
  63. # arg 1: send_coin
  64. self.send_proto = init_proto_from_coin(arg, 'send')
  65. arg = get_arg()
  66. # arg 2: amt
  67. if is_coin_amt(self.send_proto, arg):
  68. args.send_amt = self.send_proto.coin_amt(arg)
  69. arg = get_arg()
  70. # arg 3: chg_spec (change address spec)
  71. if args.send_amt:
  72. if not arg in sp.params.coins['receive']: # is change arg
  73. args.chg_spec = arg
  74. arg = get_arg()
  75. # arg 4: recv_coin
  76. self.recv_proto = init_proto_from_coin(arg, 'receive')
  77. # arg 5: recv_spec (receive address spec)
  78. if args_in:
  79. args.recv_spec = get_arg()
  80. if args_in: # done parsing, all args consumed
  81. self.cfg._usage()
  82. from ....protocol import init_proto
  83. sp = self.swap_proto_mod
  84. args_in = list(cmd_args)
  85. args = CmdlineArgs()
  86. parse()
  87. chg_output = (
  88. await self.get_swap_output(self.send_proto, args.chg_spec, addrfiles, 'change address')
  89. if args.send_amt else None)
  90. if chg_output:
  91. self.check_chg_addr_is_wallet_addr(chg_output)
  92. recv_output = await self.get_swap_output(self.recv_proto, args.recv_spec, addrfiles, 'destination address')
  93. self.check_chg_addr_is_wallet_addr(
  94. recv_output,
  95. message = (
  96. 'Swap destination address is not an MMGen wallet address!\n'
  97. 'To sign this transaction, autosign or txsign must be invoked with --allow-non-wallet-swap'))
  98. memo = sp.data(self.recv_proto, recv_output.addr)
  99. # this goes into the transaction file:
  100. self.swap_recv_addr_mmid = recv_output.mmid
  101. return (
  102. [f'vault,{args.send_amt}', chg_output.mmid, f'data:{memo}'] if args.send_amt else
  103. ['vault', f'data:{memo}'])
  104. def update_data_output(self, trade_limit):
  105. sp = self.swap_proto_mod
  106. o = self.data_output._asdict()
  107. parsed_memo = sp.data.parse(o['data'].decode())
  108. memo = sp.data(
  109. self.recv_proto,
  110. self.recv_proto.coin_addr(parsed_memo.address),
  111. trade_limit = trade_limit)
  112. o['data'] = f'data:{memo}'
  113. self.data_output = self.Output(self.proto, **o)
  114. def update_vault_addr(self, addr):
  115. vault_idx = self.vault_idx
  116. assert vault_idx == 0, f'{vault_idx}: vault index is not zero!'
  117. o = self.outputs[vault_idx]._asdict()
  118. o['addr'] = addr
  119. self.outputs[vault_idx] = self.Output(self.proto, **o)
  120. @property
  121. def vault_idx(self):
  122. return self._chg_output_ops('idx', 'is_vault')
  123. @property
  124. def vault_output(self):
  125. return self._chg_output_ops('output', 'is_vault')