new.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2022 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
  9. # https://gitlab.com/mmgen/mmgen
  10. """
  11. tx.new: new transaction class
  12. """
  13. from ..globalvars import *
  14. from ..opts import opt
  15. from .base import Base
  16. from ..color import pink,yellow
  17. from ..obj import get_obj,MMGenList
  18. from ..util import msg,qmsg,fmt,die,suf,remove_dups,get_extension
  19. from ..addr import is_mmgen_id,CoinAddr,is_coin_addr
  20. def mmaddr2coinaddr(mmaddr,ad_w,ad_f,proto):
  21. def wmsg(k):
  22. messages = {
  23. 'addr_in_addrfile_only': f"""
  24. Warning: output address {mmaddr} is not in the tracking wallet, which
  25. means its balance will not be tracked. You're strongly advised to import
  26. the address into your tracking wallet before broadcasting this transaction.
  27. """,
  28. 'addr_not_found': f"""
  29. No data for {g.proj_name} address {mmaddr} could be found in either the
  30. tracking wallet or the supplied address file. Please import this address
  31. into your tracking wallet, or supply an address file on the command line.
  32. """,
  33. 'addr_not_found_no_addrfile': f"""
  34. No data for {g.proj_name} address {mmaddr} could be found in the tracking
  35. wallet. Please import this address into your tracking wallet or supply an
  36. address file for it on the command line.
  37. """
  38. }
  39. return '\n' + fmt(messages[k],indent=' ')
  40. # assume mmaddr has already been checked
  41. coin_addr = ad_w.mmaddr2coinaddr(mmaddr)
  42. if not coin_addr:
  43. if ad_f:
  44. coin_addr = ad_f.mmaddr2coinaddr(mmaddr)
  45. if coin_addr:
  46. msg(wmsg('addr_in_addrfile_only'))
  47. from ..ui import keypress_confirm
  48. if not (opt.yes or keypress_confirm('Continue anyway?')):
  49. sys.exit(1)
  50. else:
  51. die(2,wmsg('addr_not_found'))
  52. else:
  53. die(2,wmsg('addr_not_found_no_addrfile'))
  54. return CoinAddr(proto,coin_addr)
  55. class New(Base):
  56. fee_is_approximate = False
  57. msg_low_coin = 'Selected outputs insufficient to fund this transaction ({} {} needed)'
  58. msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)'
  59. msg_no_change_output = """
  60. ERROR: No change address specified. If you wish to create a transaction with
  61. only one output, specify a single output address with no {} amount
  62. """
  63. def update_output_amt(self,idx,amt):
  64. o = self.outputs[idx]._asdict()
  65. o['amt'] = amt
  66. self.outputs[idx] = self.Output(self.proto,**o)
  67. def add_mmaddrs_to_outputs(self,ad_w,ad_f):
  68. a = [e.addr for e in self.outputs]
  69. d = ad_w.make_reverse_dict(a)
  70. if ad_f:
  71. d.update(ad_f.make_reverse_dict(a))
  72. for e in self.outputs:
  73. if e.addr and e.addr in d:
  74. e.mmid,f = d[e.addr]
  75. if f:
  76. e.comment = f
  77. def check_dup_addrs(self,io_str):
  78. assert io_str in ('inputs','outputs')
  79. addrs = [e.addr for e in getattr(self,io_str)]
  80. if len(addrs) != len(set(addrs)):
  81. die(2,f'{addrs}: duplicate address in transaction {io_str}')
  82. # given tx size and absolute fee or fee spec, return absolute fee
  83. # relative fee is N+<first letter of unit name>
  84. def feespec2abs(self,tx_fee,tx_size):
  85. fee = get_obj(self.proto.coin_amt,num=tx_fee,silent=True)
  86. if fee:
  87. return fee
  88. else:
  89. import re
  90. units = {u[0]:u for u in self.proto.coin_amt.units}
  91. pat = re.compile(r'([1-9][0-9]*)({})'.format('|'.join(units)))
  92. if pat.match(tx_fee):
  93. amt,unit = pat.match(tx_fee).groups()
  94. return self.fee_rel2abs(tx_size,units,int(amt),unit)
  95. return False
  96. def get_usr_fee_interactive(self,tx_fee=None,desc='Starting'):
  97. abs_fee = None
  98. from ..ui import line_input
  99. while True:
  100. if tx_fee:
  101. abs_fee = self.convert_and_check_fee(tx_fee,desc)
  102. if abs_fee:
  103. prompt = '{} TX fee{}: {}{} {} ({} {})\n'.format(
  104. desc,
  105. (f' (after {opt.tx_fee_adj:.2f}X adjustment)'
  106. if opt.tx_fee_adj != 1 and desc.startswith('Network-estimated')
  107. else ''),
  108. ('','≈')[self.fee_is_approximate],
  109. abs_fee.hl(),
  110. self.coin,
  111. pink(str(self.fee_abs2rel(abs_fee))),
  112. self.rel_fee_disp)
  113. from ..ui import keypress_confirm
  114. if opt.yes or keypress_confirm(prompt+'OK?',default_yes=True):
  115. if opt.yes:
  116. msg(prompt)
  117. return abs_fee
  118. tx_fee = line_input(self.usr_fee_prompt)
  119. desc = 'User-selected'
  120. # we don't know fee yet, so perform preliminary check with fee == 0
  121. async def precheck_sufficient_funds(self,inputs_sum,sel_unspent,outputs_sum):
  122. if self.twuo.total < outputs_sum:
  123. msg(self.msg_wallet_low_coin.format(outputs_sum-inputs_sum,self.dcoin))
  124. return False
  125. if inputs_sum < outputs_sum:
  126. msg(self.msg_low_coin.format(outputs_sum-inputs_sum,self.dcoin))
  127. return False
  128. return True
  129. async def get_fee_from_user(self,have_estimate_fail=[]):
  130. if opt.tx_fee:
  131. desc = 'User-selected'
  132. start_fee = opt.tx_fee
  133. else:
  134. desc = 'Network-estimated ({}, {} conf{})'.format(
  135. opt.fee_estimate_mode.upper(),
  136. pink(str(opt.tx_confs)),
  137. suf(opt.tx_confs) )
  138. fee_per_kb,fe_type = await self.get_rel_fee_from_network()
  139. if fee_per_kb < 0:
  140. if not have_estimate_fail:
  141. msg(self.fee_fail_fs.format(c=opt.tx_confs,t=fe_type))
  142. have_estimate_fail.append(True)
  143. start_fee = None
  144. else:
  145. start_fee = self.fee_est2abs(fee_per_kb,fe_type)
  146. return self.get_usr_fee_interactive(start_fee,desc=desc)
  147. def add_output(self,coinaddr,amt,is_chg=None):
  148. self.outputs.append(self.Output(self.proto,addr=coinaddr,amt=amt,is_chg=is_chg))
  149. async def process_cmd_arg(self,arg,ad_f,ad_w):
  150. if ',' in arg:
  151. addr,amt = arg.split(',',1)
  152. err_desc = 'coin argument in command-line argument'
  153. else:
  154. addr,amt = (arg,None)
  155. err_desc = 'command-line argument'
  156. if is_mmgen_id(self.proto,addr):
  157. coin_addr = mmaddr2coinaddr(addr,ad_w,ad_f,self.proto)
  158. elif is_coin_addr(self.proto,addr):
  159. coin_addr = CoinAddr(self.proto,addr)
  160. else:
  161. die(2,f'{addr}: invalid {err_desc} {{!r}}'.format(f'{addr},{amt}' if amt else addr))
  162. if not amt and self.get_chg_output_idx() is not None:
  163. die(2,'ERROR: More than one change address {} on command line'.format(
  164. 'requested' if self.chg_autoselected else 'listed'))
  165. self.add_output(coin_addr,self.proto.coin_amt(amt or '0'),is_chg=not amt)
  166. async def process_cmd_args(self,cmd_args,ad_f,ad_w):
  167. for a in cmd_args:
  168. await self.process_cmd_arg(a,ad_f,ad_w)
  169. if self.get_chg_output_idx() == None:
  170. die(2,(
  171. fmt( self.msg_no_change_output.format(self.dcoin) ).strip()
  172. if len(self.outputs) == 1 else
  173. 'ERROR: No change output specified' ))
  174. if self.has_segwit_outputs() and not self.rpc.info('segwit_is_active'):
  175. die(2,f'{g.proj_name} Segwit address requested on the command line, '
  176. + 'but Segwit is not active on this chain')
  177. if not self.outputs:
  178. die(2,'At least one output must be specified on the command line')
  179. async def get_outputs_from_cmdline(self,cmd_args):
  180. from ..addrdata import AddrData,TwAddrData
  181. from ..addrlist import AddrList
  182. from ..addrfile import AddrFile
  183. addrfiles = remove_dups(
  184. tuple(a for a in cmd_args if get_extension(a) == AddrFile.ext),
  185. desc = 'command line',
  186. edesc = 'argument',
  187. )
  188. cmd_args = remove_dups(
  189. tuple(a for a in cmd_args if a not in addrfiles),
  190. desc = 'command line',
  191. edesc = 'argument',
  192. )
  193. ad_f = AddrData(self.proto)
  194. from ..fileutil import check_infile
  195. for a in addrfiles:
  196. check_infile(a)
  197. ad_f.add(AddrList(self.proto,a))
  198. ad_w = await TwAddrData(self.proto,twctl=self.twctl)
  199. await self.process_cmd_args(cmd_args,ad_f,ad_w)
  200. self.add_mmaddrs_to_outputs(ad_w,ad_f)
  201. self.check_dup_addrs('outputs')
  202. chg_idx = self.get_chg_output_idx()
  203. if chg_idx is not None:
  204. await self.warn_chg_addr_used(self.outputs[chg_idx])
  205. async def warn_chg_addr_used(self,chg):
  206. from ..tw.addresses import TwAddresses
  207. if (await TwAddresses(self.proto,get_data=True)).is_used(chg.addr):
  208. from ..ui import keypress_confirm
  209. if not keypress_confirm(
  210. '{a} {b} {c}\n{d}'.format(
  211. a = yellow(f'Requested change address'),
  212. b = (chg.mmid or chg.addr).hl(),
  213. c = yellow('is already used!'),
  214. d = yellow('Address reuse harms your privacy and security. Continue anyway? (y/N): ')
  215. ),
  216. complete_prompt = True,
  217. default_yes = False ):
  218. die(1,'Exiting at user request')
  219. # inputs methods
  220. def select_unspent(self,unspent):
  221. prompt = 'Enter a range or space-separated list of outputs to spend: '
  222. from ..ui import line_input
  223. while True:
  224. reply = line_input(prompt).strip()
  225. if reply:
  226. from ..addrlist import AddrIdxList
  227. selected = get_obj(AddrIdxList, fmt_str=','.join(reply.split()) )
  228. if selected:
  229. if selected[-1] <= len(unspent):
  230. return selected
  231. msg(f'Unspent output number must be <= {len(unspent)}')
  232. def select_unspent_cmdline(self,unspent):
  233. def idx2num(idx):
  234. uo = unspent[idx]
  235. mmid_disp = f' ({uo.twmmid})' if uo.twmmid.type == 'mmgen' else ''
  236. msg(f'Adding input: {idx + 1} {uo.addr}{mmid_disp}')
  237. return idx + 1
  238. def get_uo_nums():
  239. for addr in opt.inputs.split(','):
  240. if is_mmgen_id(self.proto,addr):
  241. attr = 'twmmid'
  242. elif is_coin_addr(self.proto,addr):
  243. attr = 'addr'
  244. else:
  245. die(1,f'{addr!r}: not an MMGen ID or {self.coin} address')
  246. found = False
  247. for idx in range(len(unspent)):
  248. if getattr(unspent[idx],attr) == addr:
  249. yield idx2num(idx)
  250. found = True
  251. if not found:
  252. die(1,f'{addr!r}: address not found in tracking wallet')
  253. return set(get_uo_nums()) # silently discard duplicates
  254. def copy_inputs_from_tw(self,tw_unspent_data):
  255. def gen_inputs():
  256. for d in tw_unspent_data:
  257. i = self.Input(
  258. self.proto,
  259. **{attr:getattr(d,attr) for attr in d.__dict__ if attr in self.Input.tw_copy_attrs} )
  260. if d.twmmid.type == 'mmgen':
  261. i.mmid = d.twmmid # twmmid -> mmid
  262. yield i
  263. self.inputs = type(self.inputs)(self,list(gen_inputs()))
  264. def warn_insufficient_funds(self,funds_left):
  265. msg(self.msg_low_coin.format(self.proto.coin_amt(-funds_left).hl(),self.coin))
  266. async def get_funds_left(self,fee,outputs_sum):
  267. return self.sum_inputs() - outputs_sum - fee
  268. async def get_inputs_from_user(self,outputs_sum):
  269. while True:
  270. us_f = self.select_unspent_cmdline if opt.inputs else self.select_unspent
  271. sel_nums = us_f(self.twuo.data)
  272. msg(f'Selected output{suf(sel_nums)}: {{}}'.format(' '.join(str(n) for n in sel_nums)))
  273. sel_unspent = MMGenList(self.twuo.data[i-1] for i in sel_nums)
  274. inputs_sum = sum(s.amt for s in sel_unspent)
  275. if not await self.precheck_sufficient_funds(inputs_sum,sel_unspent,outputs_sum):
  276. continue
  277. self.copy_inputs_from_tw(sel_unspent) # makes self.inputs
  278. self.usr_fee = await self.get_fee_from_user()
  279. funds_left = await self.get_funds_left(self.usr_fee,outputs_sum)
  280. if funds_left >= 0:
  281. p = self.final_inputs_ok_msg(funds_left)
  282. from ..ui import keypress_confirm
  283. if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
  284. if opt.yes:
  285. msg(p)
  286. return funds_left
  287. else:
  288. self.warn_insufficient_funds(funds_left)
  289. async def create(self,cmd_args,locktime=None,do_info=False,caller='txcreate'):
  290. assert isinstance( locktime, (int,type(None)) ), 'locktime must be of type int'
  291. from ..tw.unspent import TwUnspentOutputs
  292. if opt.comment_file:
  293. self.add_comment(opt.comment_file)
  294. twuo_addrs = await self.get_input_addrs_from_cmdline()
  295. self.twuo = await TwUnspentOutputs(self.proto,minconf=opt.minconf,addrs=twuo_addrs)
  296. await self.twuo.get_data()
  297. if not do_info:
  298. await self.get_outputs_from_cmdline(cmd_args)
  299. from ..ui import do_license_msg
  300. do_license_msg()
  301. if not opt.inputs:
  302. await self.twuo.view_filter_and_sort()
  303. self.twuo.display_total()
  304. if do_info:
  305. del self.twuo.twctl
  306. sys.exit(0)
  307. outputs_sum = self.sum_outputs()
  308. msg('Total amount to spend: {}'.format(
  309. f'{outputs_sum.hl()} {self.dcoin}' if outputs_sum else 'Unknown'
  310. ))
  311. funds_left = await self.get_inputs_from_user(outputs_sum)
  312. self.check_non_mmgen_inputs(caller)
  313. self.update_change_output(funds_left)
  314. if not opt.yes:
  315. self.add_comment() # edits an existing comment
  316. await self.create_serialized(locktime=locktime) # creates self.txid too
  317. self.add_timestamp()
  318. self.add_blockcount()
  319. self.chain = self.proto.chain_name
  320. self.check_fee()
  321. qmsg('Transaction successfully created')
  322. from . import UnsignedTX
  323. new = UnsignedTX(data=self.__dict__)
  324. if not opt.yes:
  325. new.info.view_with_prompt('View transaction details?')
  326. del new.twuo.twctl
  327. return new