tx.py 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. tx.py: Transaction routines for the MMGen suite
  20. """
  21. import sys,time
  22. from .globalvars import g
  23. from .opts import opt
  24. from .color import *
  25. from .util import (
  26. msg,
  27. ymsg,
  28. dmsg,
  29. vmsg,
  30. qmsg,
  31. msg_r,
  32. die,
  33. is_int,
  34. fmt,
  35. suf,
  36. altcoin_subclass,
  37. confirm_or_raise,
  38. remove_dups,
  39. get_extension,
  40. keypress_confirm,
  41. do_license_msg,
  42. line_input,
  43. make_chksum_6,
  44. make_timestamp,
  45. secs_to_dhms,
  46. )
  47. from .objmethods import MMGenObject
  48. from .obj import (
  49. ImmutableAttr,
  50. ListItemAttr,
  51. MMGenList,
  52. MMGenListItem,
  53. MMGenTxLabel,
  54. HexStr,
  55. MMGenTxID,
  56. MMGenDict,
  57. CoinTxID,
  58. get_obj,
  59. )
  60. from .addr import MMGenID,CoinAddr,is_mmgen_id,is_coin_addr
  61. wmsg = lambda k: {
  62. 'addr_in_addrfile_only': """
  63. Warning: output address {} is not in the tracking wallet, which means
  64. its balance will not be tracked. You're strongly advised to import the address
  65. into your tracking wallet before broadcasting this transaction.
  66. """.strip(),
  67. 'addr_not_found': """
  68. No data for {pnm} address {{}} could be found in either the tracking
  69. wallet or the supplied address file. Please import this address into your
  70. tracking wallet, or supply an address file for it on the command line.
  71. """.strip().format(pnm=g.proj_name),
  72. 'addr_not_found_no_addrfile': """
  73. No data for {pnm} address {{}} could be found in the tracking wallet.
  74. Please import this address into your tracking wallet or supply an address file
  75. for it on the command line.
  76. """.strip().format(pnm=g.proj_name),
  77. }[k]
  78. def strfmt_locktime(num,terse=False):
  79. # Locktime itself is an unsigned 4-byte integer which can be parsed two ways:
  80. #
  81. # If less than 500 million, locktime is parsed as a block height. The transaction can be
  82. # added to any block which has this height or higher.
  83. # MMGen note: s/this height or higher/a higher block height/
  84. #
  85. # If greater than or equal to 500 million, locktime is parsed using the Unix epoch time
  86. # format (the number of seconds elapsed since 1970-01-01T00:00 UTC). The transaction can be
  87. # added to any block whose block time is greater than the locktime.
  88. if num == None:
  89. return '(None)'
  90. elif num >= 5 * 10**6:
  91. return ' '.join(time.strftime('%c',time.gmtime(num)).split()[1:])
  92. elif num > 0:
  93. return '{}{}'.format(('block height ','')[terse],num)
  94. else:
  95. die(2,f'{num!r}: invalid nLockTime value!')
  96. def mmaddr2coinaddr(mmaddr,ad_w,ad_f,proto):
  97. # assume mmaddr has already been checked
  98. coin_addr = ad_w.mmaddr2coinaddr(mmaddr)
  99. if not coin_addr:
  100. if ad_f:
  101. coin_addr = ad_f.mmaddr2coinaddr(mmaddr)
  102. if coin_addr:
  103. msg(wmsg('addr_in_addrfile_only').format(mmaddr))
  104. if not (opt.yes or keypress_confirm('Continue anyway?')):
  105. sys.exit(1)
  106. else:
  107. die(2,wmsg('addr_not_found').format(mmaddr))
  108. else:
  109. die(2,wmsg('addr_not_found_no_addrfile').format(mmaddr))
  110. return CoinAddr(proto,coin_addr)
  111. def addr2pubhash(proto,addr):
  112. ap = proto.parse_addr(addr)
  113. assert ap,f'coin address {addr!r} could not be parsed'
  114. return ap.bytes.hex()
  115. def addr2scriptPubKey(proto,addr):
  116. return {
  117. 'p2pkh': '76a914' + addr2pubhash(proto,addr) + '88ac',
  118. 'p2sh': 'a914' + addr2pubhash(proto,addr) + '87',
  119. 'bech32': proto.witness_vernum_hex + '14' + addr2pubhash(proto,addr)
  120. }[addr.addr_fmt]
  121. def scriptPubKey2addr(proto,s):
  122. if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac':
  123. return proto.pubhash2addr(bytes.fromhex(s[6:-4]),p2sh=False),'p2pkh'
  124. elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87':
  125. return proto.pubhash2addr(bytes.fromhex(s[4:-2]),p2sh=True),'p2sh'
  126. elif len(s) == 44 and s[:4] == proto.witness_vernum_hex + '14':
  127. return proto.pubhash2bech32addr(bytes.fromhex(s[4:])),'bech32'
  128. else:
  129. raise NotImplementedError(f'Unknown scriptPubKey ({s})')
  130. class DeserializedTX(dict,MMGenObject):
  131. """
  132. Parse a serialized Bitcoin transaction
  133. For checking purposes, additionally reconstructs the raw (unsigned) tx hex from signed tx hex
  134. """
  135. def __init__(self,proto,txhex):
  136. def bytes2int(bytes_le):
  137. if bytes_le[-1] & 0x80: # sign bit is set
  138. die(3,"{}: Negative values not permitted in transaction!".format(bytes_le[::-1].hex()))
  139. return int(bytes_le[::-1].hex(),16)
  140. def bytes2coin_amt(bytes_le):
  141. return proto.coin_amt(bytes2int(bytes_le) * proto.coin_amt.satoshi)
  142. def bshift(n,skip=False,sub_null=False):
  143. ret = tx[self.idx:self.idx+n]
  144. self.idx += n
  145. if sub_null:
  146. self.raw_tx += b'\x00'
  147. elif not skip:
  148. self.raw_tx += ret
  149. return ret
  150. # https://bitcoin.org/en/developer-reference#compactsize-unsigned-integers
  151. # For example, the number 515 is encoded as 0xfd0302.
  152. def readVInt(skip=False):
  153. s = tx[self.idx]
  154. self.idx += 1
  155. if not skip:
  156. self.raw_tx.append(s)
  157. vbytes_len = 1 if s < 0xfd else 2 if s == 0xfd else 4 if s == 0xfe else 8
  158. if vbytes_len == 1:
  159. return s
  160. else:
  161. vbytes = tx[self.idx:self.idx+vbytes_len]
  162. self.idx += vbytes_len
  163. if not skip:
  164. self.raw_tx += vbytes
  165. return int(vbytes[::-1].hex(),16)
  166. def make_txid(tx_bytes):
  167. from hashlib import sha256
  168. return sha256(sha256(tx_bytes).digest()).digest()[::-1].hex()
  169. self.idx = 0
  170. self.raw_tx = bytearray()
  171. tx = bytes.fromhex(txhex)
  172. d = { 'version': bytes2int(bshift(4)) }
  173. has_witness = tx[self.idx] == 0
  174. if has_witness:
  175. u = bshift(2,skip=True).hex()
  176. if u != '0001':
  177. raise IllegalWitnessFlagValue(f'{u!r}: Illegal value for flag in transaction!')
  178. d['num_txins'] = readVInt()
  179. d['txins'] = MMGenList([{
  180. 'txid': bshift(32)[::-1].hex(),
  181. 'vout': bytes2int(bshift(4)),
  182. 'scriptSig': bshift(readVInt(skip=True),sub_null=True).hex(),
  183. 'nSeq': bshift(4)[::-1].hex()
  184. } for i in range(d['num_txins'])])
  185. d['num_txouts'] = readVInt()
  186. d['txouts'] = MMGenList([{
  187. 'amount': bytes2coin_amt(bshift(8)),
  188. 'scriptPubKey': bshift(readVInt()).hex()
  189. } for i in range(d['num_txouts'])])
  190. for o in d['txouts']:
  191. o['address'] = scriptPubKey2addr(proto,o['scriptPubKey'])[0]
  192. if has_witness:
  193. # https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
  194. # A non-witness program (defined hereinafter) txin MUST be associated with an empty
  195. # witness field, represented by a 0x00.
  196. d['txid'] = make_txid(tx[:4] + tx[6:self.idx] + tx[-4:])
  197. d['witness_size'] = len(tx) - self.idx + 2 - 4 # add len(marker+flag), subtract len(locktime)
  198. for txin in d['txins']:
  199. if tx[self.idx] == 0:
  200. bshift(1,skip=True)
  201. continue
  202. txin['witness'] = [
  203. bshift(readVInt(skip=True),skip=True).hex() for item in range(readVInt(skip=True)) ]
  204. else:
  205. d['txid'] = make_txid(tx)
  206. d['witness_size'] = 0
  207. if len(tx) - self.idx != 4:
  208. raise TxHexParseError('TX hex has invalid length: {} extra bytes'.format(len(tx)-self.idx-4))
  209. d['lock_time'] = bytes2int(bshift(4))
  210. d['unsigned_hex'] = self.raw_tx.hex()
  211. dict.__init__(self,d)
  212. class MMGenTxIO(MMGenListItem):
  213. vout = ListItemAttr(int,typeconv=False)
  214. amt = ImmutableAttr(None)
  215. label = ListItemAttr('TwComment',reassign_ok=True)
  216. mmid = ListItemAttr(MMGenID,include_proto=True)
  217. addr = ImmutableAttr(CoinAddr,include_proto=True)
  218. confs = ListItemAttr(int) # confs of type long exist in the wild, so convert
  219. txid = ListItemAttr('CoinTxID')
  220. have_wif = ListItemAttr(bool,typeconv=False,delete_ok=True)
  221. invalid_attrs = {'proto','tw_copy_attrs'}
  222. def __init__(self,proto,**kwargs):
  223. self.__dict__['proto'] = proto
  224. MMGenListItem.__init__(self,**kwargs)
  225. @property
  226. def mmtype(self):
  227. """
  228. Attempt to determine input or output’s MMGenAddrType. For non-MMGen
  229. addresses, infer the type from the address format, returning None for
  230. P2PKH, which could be either 'L' or 'C'.
  231. """
  232. return (
  233. str(self.mmid.mmtype) if self.mmid else
  234. 'B' if self.addr.addr_fmt == 'bech32' else
  235. 'S' if self.addr.addr_fmt == 'p2sh' else
  236. None )
  237. class conv_funcs:
  238. def amt(self,value):
  239. return self.proto.coin_amt(value)
  240. class MMGenTxInput(MMGenTxIO):
  241. scriptPubKey = ListItemAttr('HexStr')
  242. sequence = ListItemAttr(int,typeconv=False)
  243. tw_copy_attrs = { 'scriptPubKey','vout','amt','label','mmid','addr','confs','txid' }
  244. class MMGenTxOutput(MMGenTxIO):
  245. is_chg = ListItemAttr(bool,typeconv=False)
  246. class MMGenTxIOList(MMGenObject):
  247. def __init__(self,parent,data=None):
  248. self.parent = parent
  249. if data:
  250. assert isinstance(data,list), 'MMGenTxIOList_check1'
  251. self.data = data
  252. else:
  253. self.data = list()
  254. def __getitem__(self,val): return self.data.__getitem__(val)
  255. def __setitem__(self,key,val): return self.data.__setitem__(key,val)
  256. def __delitem__(self,val): return self.data.__delitem__(val)
  257. def __contains__(self,val): return self.data.__contains__(val)
  258. def __iter__(self): return self.data.__iter__()
  259. def __len__(self): return self.data.__len__()
  260. def __add__(self,val): return self.data.__add__(val)
  261. def __eq__(self,val): return self.data.__eq__(val)
  262. def append(self,val): return self.data.append(val)
  263. def sort(self,*args,**kwargs): return self.data.sort(*args,**kwargs)
  264. class MMGenTxInputList(MMGenTxIOList):
  265. desc = 'transaction inputs'
  266. member_type = 'MMGenTxInput'
  267. # Lexicographical Indexing of Transaction Inputs and Outputs
  268. # https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki
  269. def sort_bip69(self):
  270. def sort_func(a):
  271. return (
  272. bytes.fromhex(a.txid)
  273. + int.to_bytes(a.vout,4,'big') )
  274. self.sort(key=sort_func)
  275. class MMGenTxOutputList(MMGenTxIOList):
  276. desc = 'transaction outputs'
  277. member_type = 'MMGenTxOutput'
  278. def sort_bip69(self):
  279. def sort_func(a):
  280. return (
  281. int.to_bytes(a.amt.to_unit('satoshi'),8,'big')
  282. + bytes.fromhex(addr2scriptPubKey(self.parent.proto,a.addr)) )
  283. self.sort(key=sort_func)
  284. class MMGenTX:
  285. filename_api = True
  286. class Base(MMGenObject):
  287. desc = 'transaction'
  288. hex = '' # raw serialized hex transaction
  289. label = MMGenTxLabel('')
  290. txid = ''
  291. coin_txid = ''
  292. timestamp = ''
  293. blockcount = 0
  294. coin = None
  295. dcoin = None
  296. locktime = None
  297. chain = None
  298. rel_fee_desc = 'satoshis per byte'
  299. rel_fee_disp = 'sat/byte'
  300. non_mmgen_inputs_msg = f"""
  301. This transaction includes inputs with non-{g.proj_name} addresses. When
  302. signing the transaction, private keys for the addresses listed below must
  303. be supplied using the --keys-from-file option. The key file must contain
  304. one key per line. Please note that this transaction cannot be autosigned,
  305. as autosigning does not support the use of key files.
  306. Non-{g.proj_name} addresses found in inputs:
  307. {{}}
  308. """
  309. def __new__(cls,*args,**kwargs):
  310. """
  311. determine correct protocol and pass the proto to altcoin_subclass(), which returns the
  312. transaction object
  313. """
  314. assert args == (), f'MMGenTX.Base_chk1: only keyword args allowed in {cls.__name__} initializer'
  315. if 'proto' in kwargs:
  316. return MMGenObject.__new__(altcoin_subclass(cls,kwargs['proto'],'tx'))
  317. elif 'data' in kwargs:
  318. return MMGenObject.__new__(altcoin_subclass(cls,kwargs['data']['proto'],'tx'))
  319. elif 'filename' in kwargs:
  320. from .txfile import MMGenTxFile
  321. tmp_tx = MMGenObject.__new__(cls)
  322. MMGenTxFile(tmp_tx).parse(
  323. infile = kwargs['filename'],
  324. quiet_open = kwargs.get('quiet_open'),
  325. metadata_only = True )
  326. me = MMGenObject.__new__(altcoin_subclass(cls,tmp_tx.proto,'tx'))
  327. me.proto = tmp_tx.proto
  328. return me
  329. elif cls.__name__ == 'Base' and args == () and kwargs == {}: # allow instantiation of empty Base()
  330. return cls
  331. else:
  332. raise ValueError(
  333. f"MMGenTX.Base: {cls.__name__} must be instantiated with 'proto','data' or 'filename' keyword")
  334. def __init__(self):
  335. self.inputs = MMGenTxInputList(self)
  336. self.outputs = MMGenTxOutputList(self)
  337. self.name = type(self).__name__
  338. @property
  339. def coin(self):
  340. return self.proto.coin
  341. @property
  342. def dcoin(self):
  343. return self.proto.dcoin
  344. def check_correct_chain(self):
  345. if hasattr(self,'rpc'):
  346. if self.chain != self.rpc.chain:
  347. raise TransactionChainMismatch(
  348. f'Transaction is for {self.chain}, but coin daemon chain is {self.rpc.chain}!')
  349. def sum_inputs(self):
  350. return sum(e.amt for e in self.inputs)
  351. def sum_outputs(self,exclude=None):
  352. if exclude == None:
  353. olist = self.outputs
  354. else:
  355. olist = self.outputs[:exclude] + self.outputs[exclude+1:]
  356. if not olist:
  357. return self.proto.coin_amt('0')
  358. return self.proto.coin_amt(sum(e.amt for e in olist))
  359. def get_chg_output_idx(self):
  360. ch_ops = [x.is_chg for x in self.outputs]
  361. try:
  362. return ch_ops.index(True)
  363. except ValueError:
  364. return None
  365. def has_segwit_inputs(self):
  366. return any(i.mmtype in ('S','B') for i in self.inputs)
  367. def has_segwit_outputs(self):
  368. return any(o.mmtype in ('S','B') for o in self.outputs)
  369. # https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
  370. # 180: uncompressed, 148: compressed
  371. def estimate_size_old(self):
  372. if not self.inputs or not self.outputs:
  373. return None
  374. return len(self.inputs)*180 + len(self.outputs)*34 + 10
  375. # https://bitcoincore.org/en/segwit_wallet_dev/
  376. # vsize: 3 times of the size with original serialization, plus the size with new
  377. # serialization, divide the result by 4 and round up to the next integer.
  378. # TODO: results differ slightly from actual transaction size
  379. def estimate_size(self):
  380. if not self.inputs or not self.outputs:
  381. return None
  382. sig_size = 72 # sig in DER format
  383. pubkey_size_uncompressed = 65
  384. pubkey_size_compressed = 33
  385. def get_inputs_size():
  386. # txid vout [scriptSig size (vInt)] scriptSig (<sig> <pubkey>) nSeq
  387. isize_common = 32 + 4 + 1 + 4 # txid vout [scriptSig size] nSeq = 41
  388. input_size = {
  389. 'L': isize_common + sig_size + pubkey_size_uncompressed, # = 180
  390. 'C': isize_common + sig_size + pubkey_size_compressed, # = 148
  391. 'S': isize_common + 23, # = 64
  392. 'B': isize_common + 0 # = 41
  393. }
  394. ret = sum(input_size[i.mmtype] for i in self.inputs if i.mmtype)
  395. # We have no way of knowing whether a non-MMGen P2PKH addr is compressed or uncompressed
  396. # until we see the key, so assume compressed for fee-estimation purposes. If fee estimate
  397. # is off by more than 5%, sign() aborts and user is instructed to use --vsize-adj option.
  398. return ret + sum(input_size['C'] for i in self.inputs if not i.mmtype)
  399. def get_outputs_size():
  400. # output bytes = amt: 8, byte_count: 1+, pk_script
  401. # pk_script bytes: p2pkh: 25, p2sh: 23, bech32: 22
  402. return sum({'p2pkh':34,'p2sh':32,'bech32':31}[o.addr.addr_fmt] for o in self.outputs)
  403. # https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
  404. # The witness is a serialization of all witness data of the transaction. Each txin is
  405. # associated with a witness field. A witness field starts with a var_int to indicate the
  406. # number of stack items for the txin. It is followed by stack items, with each item starts
  407. # with a var_int to indicate the length. Witness data is NOT script.
  408. # A non-witness program txin MUST be associated with an empty witness field, represented
  409. # by a 0x00. If all txins are not witness program, a transaction's wtxid is equal to its txid.
  410. def get_witness_size():
  411. if not self.has_segwit_inputs():
  412. return 0
  413. wf_size = 1 + 1 + sig_size + 1 + pubkey_size_compressed # vInt vInt sig vInt pubkey = 108
  414. return sum((1,wf_size)[i.mmtype in ('S','B')] for i in self.inputs)
  415. isize = get_inputs_size()
  416. osize = get_outputs_size()
  417. wsize = get_witness_size()
  418. # TODO: compute real varInt sizes instead of assuming 1 byte
  419. # old serialization: [nVersion] [vInt][txins][vInt][txouts] [nLockTime]
  420. old_size = 4 + 1 + isize + 1 + osize + 4
  421. # marker = 0x00, flag = 0x01
  422. # new serialization: [nVersion][marker][flag][vInt][txins][vInt][txouts][witness][nLockTime]
  423. new_size = 4 + 1 + 1 + 1 + isize + 1 + osize + wsize + 4 \
  424. if wsize else old_size
  425. ret = (old_size * 3 + new_size) // 4
  426. dmsg('\nData from estimate_size():')
  427. dmsg(f' inputs size: {isize}, outputs size: {osize}, witness size: {wsize}')
  428. dmsg(f' size: {new_size}, vsize: {ret}, old_size: {old_size}')
  429. return int(ret * (opt.vsize_adj or 1))
  430. # convert absolute BTC fee to satoshis-per-byte using estimated size
  431. def fee_abs2rel(self,abs_fee,to_unit=None):
  432. unit = getattr(self.proto.coin_amt,to_unit or 'satoshi')
  433. return int(abs_fee / unit / self.estimate_size())
  434. def get_hex_locktime(self):
  435. return int(bytes.fromhex(self.hex[-8:])[::-1].hex(),16)
  436. def set_hex_locktime(self,val):
  437. assert isinstance(val,int),'locktime value not an integer'
  438. self.hex = self.hex[:-8] + bytes.fromhex('{:08x}'.format(val))[::-1].hex()
  439. def add_timestamp(self):
  440. self.timestamp = make_timestamp()
  441. def add_blockcount(self):
  442. self.blockcount = self.rpc.blockcount
  443. # returns true if comment added or changed
  444. def add_comment(self,infile=None):
  445. if infile:
  446. from .fileutil import get_data_from_file
  447. self.label = MMGenTxLabel(get_data_from_file(infile,'transaction comment'))
  448. else: # get comment from user, or edit existing comment
  449. m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.label)]
  450. if keypress_confirm(m,default_yes=False):
  451. while True:
  452. s = MMGenTxLabel(line_input('Comment: ',insert_txt=self.label))
  453. if not s:
  454. ymsg('Warning: comment is empty')
  455. lbl_save = self.label
  456. self.label = s
  457. return (True,False)[lbl_save == self.label]
  458. return False
  459. def get_non_mmaddrs(self,desc):
  460. return remove_dups(
  461. (i.addr for i in getattr(self,desc) if not i.mmid),
  462. edesc = 'non-MMGen address',
  463. quiet = True )
  464. def check_non_mmgen_inputs(self,caller,non_mmaddrs=None):
  465. non_mmaddrs = non_mmaddrs or self.get_non_mmaddrs('inputs')
  466. if non_mmaddrs:
  467. indent = ' '
  468. fs = fmt(self.non_mmgen_inputs_msg,strip_char='\t',indent=indent).strip()
  469. m = fs.format('\n '.join(non_mmaddrs))
  470. if caller in ('txdo','txsign'):
  471. if not opt.keys_from_file:
  472. raise UserOptError(f'\n{indent}ERROR: {m}\n')
  473. else:
  474. msg(f'\n{indent}WARNING: {m}\n')
  475. if not (opt.yes or keypress_confirm('Continue?',default_yes=True)):
  476. die(1,'Exiting at user request')
  477. class New(Base):
  478. usr_fee_prompt = 'Enter transaction fee: '
  479. fee_is_approximate = False
  480. fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})'
  481. no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
  482. msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)'
  483. msg_low_coin = 'Selected outputs insufficient to fund this transaction ({} {} needed)'
  484. msg_no_change_output = fmt("""
  485. ERROR: No change address specified. If you wish to create a transaction with
  486. only one output, specify a single output address with no {} amount
  487. """).strip()
  488. def __init__(self,proto,tw=None): # tw required for resolving ERC20 token data
  489. MMGenTX.Base.__init__(self)
  490. self.proto = proto
  491. self.tw = tw
  492. def del_output(self,idx):
  493. self.outputs.pop(idx)
  494. def update_output_amt(self,idx,amt):
  495. o = self.outputs[idx]._asdict()
  496. o['amt'] = amt
  497. self.outputs[idx] = MMGenTxOutput(self.proto,**o)
  498. def add_mmaddrs_to_outputs(self,ad_w,ad_f):
  499. a = [e.addr for e in self.outputs]
  500. d = ad_w.make_reverse_dict(a)
  501. if ad_f:
  502. d.update(ad_f.make_reverse_dict(a))
  503. for e in self.outputs:
  504. if e.addr and e.addr in d:
  505. e.mmid,f = d[e.addr]
  506. if f:
  507. e.label = f
  508. def check_dup_addrs(self,io_str):
  509. assert io_str in ('inputs','outputs')
  510. addrs = [e.addr for e in getattr(self,io_str)]
  511. if len(addrs) != len(set(addrs)):
  512. die(2,f'{addrs}: duplicate address in transaction {io_str}')
  513. # coin-specific fee routines
  514. @property
  515. def relay_fee(self):
  516. kb_fee = self.proto.coin_amt(self.rpc.cached['networkinfo']['relayfee'])
  517. ret = kb_fee * self.estimate_size() / 1024
  518. vmsg('Relay fee: {} {c}/kB, for transaction: {} {c}'.format(kb_fee,ret,c=self.coin))
  519. return ret
  520. async def get_rel_fee_from_network(self):
  521. try:
  522. ret = await self.rpc.call('estimatesmartfee',opt.tx_confs,opt.fee_estimate_mode.upper())
  523. fee_per_kb = ret['feerate'] if 'feerate' in ret else -2
  524. fe_type = 'estimatesmartfee'
  525. except:
  526. args = () if self.coin=='BCH' and self.rpc.daemon_version >= 190100 else (opt.tx_confs,)
  527. fee_per_kb = await self.rpc.call('estimatefee',*args)
  528. fe_type = 'estimatefee'
  529. return fee_per_kb,fe_type
  530. # given tx size, rel fee and units, return absolute fee
  531. def fee_rel2abs(self,tx_size,units,amt,unit):
  532. if tx_size:
  533. return self.proto.coin_amt(amt * tx_size * getattr(self.proto.coin_amt,units[unit]))
  534. else:
  535. return None
  536. # given network fee estimate in BTC/kB, return absolute fee using estimated tx size
  537. def fee_est2abs(self,fee_per_kb,fe_type=None):
  538. tx_size = self.estimate_size()
  539. f = fee_per_kb * opt.tx_fee_adj * tx_size / 1024
  540. ret = self.proto.coin_amt(f,from_decimal=True)
  541. if opt.verbose:
  542. msg(fmt(f"""
  543. {fe_type.upper()} fee for {opt.tx_confs} confirmations: {fee_per_kb} {self.coin}/kB
  544. TX size (estimated): {tx_size} bytes
  545. Fee adjustment factor: {opt.tx_fee_adj}
  546. Absolute fee (fee_per_kb * adj_factor * tx_size / 1024): {ret} {self.coin}
  547. """).strip())
  548. return ret
  549. def convert_and_check_fee(self,tx_fee,desc='Missing description'):
  550. abs_fee = self.feespec2abs(tx_fee,self.estimate_size())
  551. if abs_fee == None:
  552. raise ValueError(f'{tx_fee}: cannot convert {self.rel_fee_desc} to {self.coin}'
  553. + ' because transaction size is unknown')
  554. if abs_fee == False:
  555. err = f'{tx_fee!r}: invalid TX fee (not a {self.coin} amount or {self.rel_fee_desc} specification)'
  556. elif abs_fee > self.proto.max_tx_fee:
  557. err = f'{abs_fee} {self.coin}: {desc} fee too large (maximum fee: {self.proto.max_tx_fee} {self.coin})'
  558. elif abs_fee < self.relay_fee:
  559. err = f'{abs_fee} {self.coin}: {desc} fee too small (less than relay fee of {self.relay_fee} {self.coin})'
  560. else:
  561. return abs_fee
  562. msg(err)
  563. return False
  564. # non-coin-specific fee routines
  565. # given tx size and absolute fee or fee spec, return absolute fee
  566. # relative fee is N+<first letter of unit name>
  567. def feespec2abs(self,tx_fee,tx_size):
  568. fee = get_obj(self.proto.coin_amt,num=tx_fee,silent=True)
  569. if fee:
  570. return fee
  571. else:
  572. import re
  573. units = {u[0]:u for u in self.proto.coin_amt.units}
  574. pat = re.compile(r'([1-9][0-9]*)({})'.format('|'.join(units)))
  575. if pat.match(tx_fee):
  576. amt,unit = pat.match(tx_fee).groups()
  577. return self.fee_rel2abs(tx_size,units,int(amt),unit)
  578. return False
  579. def get_usr_fee_interactive(self,tx_fee=None,desc='Starting'):
  580. abs_fee = None
  581. while True:
  582. if tx_fee:
  583. abs_fee = self.convert_and_check_fee(tx_fee,desc)
  584. if abs_fee:
  585. prompt = '{} TX fee{}: {}{} {} ({} {})\n'.format(
  586. desc,
  587. (f' (after {opt.tx_fee_adj}X adjustment)'
  588. if opt.tx_fee_adj != 1 and desc.startswith('Network-estimated')
  589. else ''),
  590. ('','≈')[self.fee_is_approximate],
  591. abs_fee.hl(),
  592. self.coin,
  593. pink(str(self.fee_abs2rel(abs_fee))),
  594. self.rel_fee_disp)
  595. if opt.yes or keypress_confirm(prompt+'OK?',default_yes=True):
  596. if opt.yes:
  597. msg(prompt)
  598. return abs_fee
  599. tx_fee = line_input(self.usr_fee_prompt)
  600. desc = 'User-selected'
  601. async def get_fee_from_user(self,have_estimate_fail=[]):
  602. if opt.tx_fee:
  603. desc = 'User-selected'
  604. start_fee = opt.tx_fee
  605. else:
  606. desc = 'Network-estimated ({}, {} conf{})'.format(
  607. opt.fee_estimate_mode.upper(),
  608. pink(str(opt.tx_confs)),
  609. suf(opt.tx_confs) )
  610. fee_per_kb,fe_type = await self.get_rel_fee_from_network()
  611. if fee_per_kb < 0:
  612. if not have_estimate_fail:
  613. msg(self.fee_fail_fs.format(c=opt.tx_confs,t=fe_type))
  614. have_estimate_fail.append(True)
  615. start_fee = None
  616. else:
  617. start_fee = self.fee_est2abs(fee_per_kb,fe_type)
  618. return self.get_usr_fee_interactive(start_fee,desc=desc)
  619. def add_output(self,coinaddr,amt,is_chg=None):
  620. self.outputs.append(MMGenTxOutput(self.proto,addr=coinaddr,amt=amt,is_chg=is_chg))
  621. def process_cmd_arg(self,arg,ad_f,ad_w):
  622. def add_output_chk(addr,amt,err_desc):
  623. if not amt and self.get_chg_output_idx() != None:
  624. die(2,'ERROR: More than one change address listed on command line')
  625. if is_mmgen_id(self.proto,addr) or is_coin_addr(self.proto,addr):
  626. coin_addr = ( mmaddr2coinaddr(addr,ad_w,ad_f,self.proto) if is_mmgen_id(self.proto,addr)
  627. else CoinAddr(self.proto,addr) )
  628. self.add_output(coin_addr,self.proto.coin_amt(amt or '0'),is_chg=not amt)
  629. else:
  630. die(2,f'{addr}: invalid {err_desc} {{!r}}'.format(f'{addr},{amt}' if amt else addr))
  631. if ',' in arg:
  632. addr,amt = arg.split(',',1)
  633. add_output_chk(addr,amt,'coin argument in command-line argument')
  634. else:
  635. add_output_chk(arg,None,'command-line argument')
  636. async def get_cmdline_input_addrs(self):
  637. # Bitcoin full node, call doesn't go to the network, so just call listunspent with addrs=[]
  638. return []
  639. def process_cmd_args(self,cmd_args,ad_f,ad_w):
  640. for a in cmd_args:
  641. self.process_cmd_arg(a,ad_f,ad_w)
  642. if self.get_chg_output_idx() == None:
  643. die(2,( 'ERROR: No change output specified',
  644. self.msg_no_change_output.format(self.dcoin))[len(self.outputs) == 1])
  645. if self.has_segwit_outputs() and not self.rpc.info('segwit_is_active'):
  646. rdie(2,f'{g.proj_name} Segwit address requested on the command line, '
  647. + 'but Segwit is not active on this chain')
  648. if not self.outputs:
  649. die(2,'At least one output must be specified on the command line')
  650. async def get_outputs_from_cmdline(self,cmd_args):
  651. from .addrdata import AddrData,TwAddrData
  652. from .addrlist import AddrList
  653. from .addrfile import AddrFile
  654. addrfiles = remove_dups(
  655. tuple(a for a in cmd_args if get_extension(a) == AddrFile.ext),
  656. desc = 'command line',
  657. edesc = 'argument',
  658. )
  659. cmd_args = remove_dups(
  660. tuple(a for a in cmd_args if a not in addrfiles),
  661. desc = 'command line',
  662. edesc = 'argument',
  663. )
  664. ad_f = AddrData(self.proto)
  665. from .fileutil import check_infile
  666. for a in addrfiles:
  667. check_infile(a)
  668. ad_f.add(AddrList(self.proto,a))
  669. ad_w = await TwAddrData(self.proto,wallet=self.tw)
  670. self.process_cmd_args(cmd_args,ad_f,ad_w)
  671. self.add_mmaddrs_to_outputs(ad_w,ad_f)
  672. self.check_dup_addrs('outputs')
  673. # inputs methods
  674. def select_unspent(self,unspent):
  675. prompt = 'Enter a range or space-separated list of outputs to spend: '
  676. while True:
  677. reply = line_input(prompt).strip()
  678. if reply:
  679. from .addrlist import AddrIdxList
  680. selected = get_obj(AddrIdxList, fmt_str=','.join(reply.split()) )
  681. if selected:
  682. if selected[-1] <= len(unspent):
  683. return selected
  684. msg(f'Unspent output number must be <= {len(unspent)}')
  685. def select_unspent_cmdline(self,unspent):
  686. def idx2num(idx):
  687. uo = unspent[idx]
  688. mmid_disp = f' ({uo.twmmid})' if uo.twmmid.type == 'mmgen' else ''
  689. msg(f'Adding input: {idx + 1} {uo.addr}{mmid_disp}')
  690. return idx + 1
  691. def get_uo_nums():
  692. for addr in opt.inputs.split(','):
  693. if is_mmgen_id(self.proto,addr):
  694. attr = 'twmmid'
  695. elif is_coin_addr(self.proto,addr):
  696. attr = 'addr'
  697. else:
  698. die(1,f'{addr!r}: not an MMGen ID or {self.coin} address')
  699. found = False
  700. for idx in range(len(unspent)):
  701. if getattr(unspent[idx],attr) == addr:
  702. yield idx2num(idx)
  703. found = True
  704. if not found:
  705. die(1,f'{addr!r}: address not found in tracking wallet')
  706. return set(get_uo_nums()) # silently discard duplicates
  707. # we don't know fee yet, so perform preliminary check with fee == 0
  708. async def precheck_sufficient_funds(self,inputs_sum,sel_unspent,outputs_sum):
  709. if self.twuo.total < outputs_sum:
  710. msg(self.msg_wallet_low_coin.format(outputs_sum-inputs_sum,self.dcoin))
  711. return False
  712. if inputs_sum < outputs_sum:
  713. msg(self.msg_low_coin.format(outputs_sum-inputs_sum,self.dcoin))
  714. return False
  715. return True
  716. def copy_inputs_from_tw(self,tw_unspent_data):
  717. def gen_inputs():
  718. for d in tw_unspent_data:
  719. i = MMGenTxInput(
  720. self.proto,
  721. **{attr:getattr(d,attr) for attr in d.__dict__ if attr in MMGenTxInput.tw_copy_attrs} )
  722. if d.twmmid.type == 'mmgen':
  723. i.mmid = d.twmmid # twmmid -> mmid
  724. yield i
  725. self.inputs = MMGenTxInputList(self,list(gen_inputs()))
  726. async def get_funds_left(self,fee,outputs_sum):
  727. return self.sum_inputs() - outputs_sum - fee
  728. def final_inputs_ok_msg(self,funds_left):
  729. return 'Transaction produces {} {} in change'.format(
  730. self.proto.coin_amt(funds_left).hl(),
  731. self.coin
  732. )
  733. def warn_insufficient_funds(self,funds_left):
  734. msg(self.msg_low_coin.format(self.proto.coin_amt(-funds_left).hl(),self.coin))
  735. async def get_inputs_from_user(self,outputs_sum):
  736. while True:
  737. us_f = self.select_unspent_cmdline if opt.inputs else self.select_unspent
  738. sel_nums = us_f(self.twuo.unspent)
  739. msg(f'Selected output{suf(sel_nums)}: {{}}'.format(' '.join(str(n) for n in sel_nums)))
  740. sel_unspent = self.twuo.MMGenTwOutputList([self.twuo.unspent[i-1] for i in sel_nums])
  741. inputs_sum = sum(s.amt for s in sel_unspent)
  742. if not await self.precheck_sufficient_funds(inputs_sum,sel_unspent,outputs_sum):
  743. continue
  744. self.copy_inputs_from_tw(sel_unspent) # makes self.inputs
  745. self.usr_fee = await self.get_fee_from_user()
  746. funds_left = await self.get_funds_left(self.usr_fee,outputs_sum)
  747. if funds_left >= 0:
  748. p = self.final_inputs_ok_msg(funds_left)
  749. if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
  750. if opt.yes:
  751. msg(p)
  752. return funds_left
  753. else:
  754. self.warn_insufficient_funds(funds_left)
  755. def update_change_output(self,funds_left):
  756. chg_idx = self.get_chg_output_idx()
  757. if funds_left == 0:
  758. msg(self.no_chg_msg)
  759. self.del_output(chg_idx)
  760. else:
  761. self.update_output_amt(chg_idx,self.proto.coin_amt(funds_left))
  762. def check_fee(self):
  763. fee = self.sum_inputs() - self.sum_outputs()
  764. if fee > self.proto.max_tx_fee:
  765. c = self.proto.coin
  766. raise MaxFeeExceeded(f'Transaction fee of {fee} {c} too high! (> {self.proto.max_tx_fee} {c})')
  767. def update_txid(self):
  768. self.txid = MMGenTxID(make_chksum_6(bytes.fromhex(self.hex)).upper())
  769. async def create_raw(self):
  770. i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs]
  771. if self.inputs[0].sequence:
  772. i[0]['sequence'] = self.inputs[0].sequence
  773. o = {e.addr:e.amt for e in self.outputs}
  774. self.hex = HexStr(await self.rpc.call('createrawtransaction',i,o))
  775. self.update_txid()
  776. async def create(self,cmd_args,locktime,do_info=False,caller='txcreate'):
  777. assert isinstance(locktime,int),'locktime must be of type int'
  778. from .twuo import TwUnspentOutputs
  779. if opt.comment_file:
  780. self.add_comment(opt.comment_file)
  781. twuo_addrs = await self.get_cmdline_input_addrs()
  782. self.twuo = await TwUnspentOutputs(self.proto,minconf=opt.minconf,addrs=twuo_addrs)
  783. await self.twuo.get_unspent_data()
  784. if not do_info:
  785. await self.get_outputs_from_cmdline(cmd_args)
  786. do_license_msg()
  787. if not opt.inputs:
  788. await self.twuo.view_and_sort(self)
  789. self.twuo.display_total()
  790. if do_info:
  791. del self.twuo.wallet
  792. sys.exit(0)
  793. outputs_sum = self.sum_outputs()
  794. msg('Total amount to spend: {}'.format(
  795. f'{outputs_sum.hl()} {self.dcoin}' if outputs_sum else 'Unknown'
  796. ))
  797. funds_left = await self.get_inputs_from_user(outputs_sum)
  798. self.check_non_mmgen_inputs(caller)
  799. self.update_change_output(funds_left)
  800. if self.proto.base_proto == 'Bitcoin':
  801. self.inputs.sort_bip69()
  802. self.outputs.sort_bip69()
  803. # do this only after inputs are sorted
  804. if opt.rbf:
  805. self.inputs[0].sequence = self.proto.max_int - 2 # handles the nLockTime case too
  806. elif locktime:
  807. self.inputs[0].sequence = self.proto.max_int - 1
  808. if not opt.yes:
  809. self.add_comment() # edits an existing comment
  810. await self.create_raw() # creates self.hex, self.txid
  811. if self.proto.base_proto == 'Bitcoin' and locktime:
  812. msg(f'Setting nLockTime to {strfmt_locktime(locktime)}!')
  813. self.set_hex_locktime(locktime)
  814. self.update_txid()
  815. self.locktime = locktime
  816. self.add_timestamp()
  817. self.add_blockcount()
  818. self.chain = self.proto.chain_name
  819. self.check_fee()
  820. qmsg('Transaction successfully created')
  821. new = MMGenTX.Unsigned(data=self.__dict__)
  822. if not opt.yes:
  823. new.view_with_prompt('View transaction details?')
  824. del new.twuo.wallet
  825. return new
  826. class Completed(Base):
  827. """
  828. signed or unsigned transaction with associated file
  829. """
  830. fn_fee_unit = 'satoshi'
  831. view_sort_orders = ('addr','raw')
  832. dfl_view_sort_order = 'addr'
  833. txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n'
  834. txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n'
  835. txview_ftr_fs = fmt("""
  836. Input amount: {i} {d}
  837. Spend amount: {s} {d}
  838. Change: {C} {d}
  839. Fee: {a} {c}{r}
  840. """)
  841. parsed_hex = None
  842. def __init__(self,filename=None,quiet_open=False,data=None):
  843. MMGenTX.Base.__init__(self)
  844. if data:
  845. assert filename is None, 'MMGenTX.Completed_chk1'
  846. assert type(data) is dict, 'MMGenTX.Completed_chk2'
  847. self.__dict__ = data
  848. return
  849. elif filename:
  850. assert data is None, 'MMGenTX.Completed_chk3'
  851. from .txfile import MMGenTxFile
  852. MMGenTxFile(self).parse(filename,quiet_open=quiet_open)
  853. self.check_pubkey_scripts()
  854. # repeat with sign and send, because coin daemon could be restarted
  855. self.check_correct_chain()
  856. # check signature and witness data
  857. def check_sigs(self): # return False if no sigs, raise exception on error
  858. txins = (self.parsed_hex or DeserializedTX(self.proto,self.hex))['txins']
  859. has_ss = any(ti['scriptSig'] for ti in txins)
  860. has_witness = any('witness' in ti and ti['witness'] for ti in txins)
  861. if not (has_ss or has_witness):
  862. return False
  863. fs = "Hex TX has {} scriptSig but input is of type '{}'!"
  864. for n in range(len(txins)):
  865. ti,mmti = txins[n],self.inputs[n]
  866. if ti['scriptSig'] == '' or ( len(ti['scriptSig']) == 46 and # native P2WPKH or P2SH-P2WPKH
  867. ti['scriptSig'][:6] == '16' + self.proto.witness_vernum_hex + '14' ):
  868. assert 'witness' in ti, 'missing witness'
  869. assert type(ti['witness']) == list and len(ti['witness']) == 2, 'malformed witness'
  870. assert len(ti['witness'][1]) == 66, 'incorrect witness pubkey length'
  871. assert mmti.mmtype == ('S','B')[ti['scriptSig']==''], fs.format('witness-type',mmti.mmtype)
  872. else: # non-witness
  873. assert mmti.mmtype not in ('S','B'), fs.format('signature in',mmti.mmtype)
  874. assert not 'witness' in ti, 'non-witness input has witness'
  875. # sig_size 72 (DER format), pubkey_size 'compressed':33, 'uncompressed':65
  876. assert (200 < len(ti['scriptSig']) < 300), 'malformed scriptSig' # VERY rough check
  877. return True
  878. def check_pubkey_scripts(self):
  879. for n,i in enumerate(self.inputs,1):
  880. addr,fmt = scriptPubKey2addr(self.proto,i.scriptPubKey)
  881. if i.addr != addr:
  882. if fmt != i.addr.addr_fmt:
  883. m = 'Address format of scriptPubKey ({}) does not match that of address ({}) in input #{}'
  884. msg(m.format(fmt,i.addr.addr_fmt,n))
  885. m = 'ERROR: Address and scriptPubKey of transaction input #{} do not match!'
  886. die(3,(m+'\n {:23}{}'*3).format(n, 'address:',i.addr,
  887. 'scriptPubKey:',i.scriptPubKey,
  888. 'scriptPubKey->address:',addr ))
  889. # def is_replaceable_from_rpc(self):
  890. # dec_tx = await self.rpc.call('decoderawtransaction',self.hex)
  891. # return None < dec_tx['vin'][0]['sequence'] <= self.proto.max_int - 2
  892. def is_replaceable(self):
  893. return self.inputs[0].sequence == self.proto.max_int - 2
  894. def check_txfile_hex_data(self):
  895. self.hex = HexStr(self.hex)
  896. def parse_txfile_hex_data(self):
  897. pass
  898. def write_to_file(self,*args,**kwargs):
  899. from .txfile import MMGenTxFile
  900. MMGenTxFile(self).write(*args,**kwargs)
  901. def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse,sort):
  902. if sort not in self.view_sort_orders:
  903. die(1,'{!r}: invalid transaction view sort order. Valid options: {}'.format(
  904. sort,
  905. ','.join(self.view_sort_orders) ))
  906. def format_io(desc):
  907. io = getattr(self,desc)
  908. is_input = desc == 'inputs'
  909. yield desc.capitalize() + ':\n' + enl
  910. confs_per_day = 60*60*24 // self.proto.avg_bdi
  911. io_sorted = {
  912. 'addr': lambda: sorted(
  913. io, # prepend '+' (sorts before '0') to ensure non-MMGen addrs are displayed first
  914. key = lambda o: (o.mmid.sort_key if o.mmid else f'+{o.addr}') + f'{o.amt:040.20f}' ),
  915. 'raw': lambda: io
  916. }[sort]
  917. for n,e in enumerate(io_sorted()):
  918. if is_input and blockcount:
  919. confs = e.confs + blockcount - self.blockcount
  920. days = int(confs // confs_per_day)
  921. if e.mmid:
  922. mmid_fmt = e.mmid.fmt(
  923. width=max_mmwid,
  924. encl='()',
  925. color=True,
  926. append_chars=('',' (chg)')[bool(not is_input and e.is_chg and terse)],
  927. append_color='green')
  928. else:
  929. mmid_fmt = MMGenID.fmtc(nonmm_str,width=max_mmwid,color=True)
  930. if terse:
  931. yield '{:3} {} {} {} {}\n'.format(
  932. n+1,
  933. e.addr.fmt(color=True,width=addr_w),
  934. mmid_fmt,
  935. e.amt.hl(),
  936. self.dcoin )
  937. else:
  938. def gen():
  939. if is_input:
  940. yield (n+1, 'tx,vout:', f'{e.txid.hl()},{red(str(e.vout))}')
  941. yield ('', 'address:', f'{e.addr.hl()} {mmid_fmt}')
  942. else:
  943. yield (n+1, 'address:', f'{e.addr.hl()} {mmid_fmt}')
  944. if e.label:
  945. yield ('', 'comment:', e.label.hl())
  946. yield ('', 'amount:', f'{e.amt.hl()} {self.dcoin}')
  947. if is_input and blockcount:
  948. yield ('', 'confirmations:', f'{confs} (around {days} days)')
  949. if not is_input and e.is_chg:
  950. yield ('', 'change:', green('True'))
  951. yield '\n'.join('{:>3} {:<8} {}'.format(*d) for d in gen()) + '\n\n'
  952. addr_w = max(len(e.addr) for f in (self.inputs,self.outputs) for e in f)
  953. return (
  954. 'Displaying inputs and outputs in {} sort order'.format({'raw':'raw','addr':'address'}[sort])
  955. + ('\n\n','\n')[terse]
  956. + ''.join(format_io('inputs'))
  957. + ''.join(format_io('outputs')) )
  958. @property
  959. def send_amt(self):
  960. return self.sum_outputs(
  961. exclude = None if len(self.outputs) == 1 else self.get_chg_output_idx()
  962. )
  963. @property
  964. def fee(self):
  965. return self.sum_inputs() - self.sum_outputs()
  966. @property
  967. def change(self):
  968. return self.sum_outputs() - self.send_amt
  969. def format_view_rel_fee(self,terse):
  970. return ' ({} {}, {} of spend amount)'.format(
  971. pink(str(self.fee_abs2rel(self.fee))),
  972. self.rel_fee_disp,
  973. pink('{:0.6f}%'.format( self.fee / self.send_amt * 100 ))
  974. )
  975. def format_view_abs_fee(self):
  976. return self.proto.coin_amt(self.fee).hl()
  977. def format_view_verbose_footer(self):
  978. tsize = len(self.hex)//2 if self.hex else 'unknown'
  979. out = f'Transaction size: Vsize {self.estimate_size()} (estimated), Total {tsize}'
  980. if self.name == 'Signed':
  981. wsize = DeserializedTX(self.proto,self.hex)['witness_size']
  982. out += f', Base {tsize-wsize}, Witness {wsize}'
  983. return out + '\n'
  984. def format_view(self,terse=False,sort=dfl_view_sort_order):
  985. blockcount = None
  986. if self.proto.base_coin != 'ETH':
  987. try:
  988. blockcount = self.rpc.blockcount
  989. except:
  990. pass
  991. def get_max_mmwid(io):
  992. if io == self.inputs:
  993. sel_f = lambda o: len(o.mmid) + 2 # len('()')
  994. else:
  995. sel_f = lambda o: len(o.mmid) + (2,8)[bool(o.is_chg)] # + len(' (chg)')
  996. return max(max([sel_f(o) for o in io if o.mmid] or [0]),len(nonmm_str))
  997. nonmm_str = f'(non-{g.proj_name} address)'
  998. max_mmwid = max(get_max_mmwid(self.inputs),get_max_mmwid(self.outputs))
  999. def gen_view():
  1000. yield (self.txview_hdr_fs_short if terse else self.txview_hdr_fs).format(
  1001. i = self.txid.hl(),
  1002. a = self.send_amt.hl(),
  1003. c = self.dcoin,
  1004. t = self.timestamp,
  1005. r = (red('False'),green('True'))[self.is_replaceable()],
  1006. s = (red('False'),green('True'))[self.name == 'Signed'],
  1007. l = (green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)] )
  1008. if self.chain != 'mainnet': # if mainnet has a coin-specific name, display it
  1009. yield green(f'Chain: {self.chain.upper()}') + '\n'
  1010. if self.coin_txid:
  1011. yield f'{self.coin} TxID: {self.coin_txid.hl()}\n'
  1012. enl = ('\n','')[bool(terse)]
  1013. yield enl
  1014. if self.label:
  1015. yield f'Comment: {self.label.hl()}\n{enl}'
  1016. yield self.format_view_body(blockcount,nonmm_str,max_mmwid,enl,terse=terse,sort=sort)
  1017. yield self.txview_ftr_fs.format(
  1018. i = self.sum_inputs().hl(),
  1019. o = self.sum_outputs().hl(),
  1020. C = self.change.hl(),
  1021. s = self.send_amt.hl(),
  1022. a = self.format_view_abs_fee(),
  1023. r = self.format_view_rel_fee(terse),
  1024. d = self.dcoin,
  1025. c = self.coin )
  1026. if opt.verbose:
  1027. yield self.format_view_verbose_footer()
  1028. return ''.join(gen_view()) # TX label might contain non-ascii chars
  1029. def view_with_prompt(self,prompt='',pause=True):
  1030. prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view: '
  1031. from .term import get_char
  1032. ok_chars = 'YyNnVvTt'
  1033. while True:
  1034. reply = get_char(prompt,immed_chars=ok_chars).strip('\n\r')
  1035. msg('')
  1036. if reply == '' or reply in 'Nn':
  1037. break
  1038. elif reply in 'YyVvTt':
  1039. self.view(pager=reply in 'Vv',terse=reply in 'Tt',pause=pause)
  1040. break
  1041. else:
  1042. msg('Invalid reply')
  1043. def view(self,pager=False,pause=True,terse=False):
  1044. o = self.format_view(terse=terse)
  1045. if pager:
  1046. do_pager(o)
  1047. else:
  1048. msg_r(o)
  1049. from .term import get_char
  1050. if pause:
  1051. get_char('Press any key to continue: ')
  1052. msg('')
  1053. class Unsigned(Completed):
  1054. desc = 'unsigned transaction'
  1055. ext = 'rawtx'
  1056. def __init__(self,*args,**kwargs):
  1057. super().__init__(*args,**kwargs)
  1058. if self.check_sigs():
  1059. die(1,'Transaction is signed!')
  1060. def delete_attrs(self,desc,attr):
  1061. for e in getattr(self,desc):
  1062. if hasattr(e,attr):
  1063. delattr(e,attr)
  1064. def get_sids(self,desc):
  1065. return remove_dups(
  1066. (e.mmid.sid for e in getattr(self,desc) if e.mmid),
  1067. quiet = True )
  1068. async def sign(self,tx_num_str,keys): # return signed object or False; don't exit or raise exception
  1069. try:
  1070. self.check_correct_chain()
  1071. except TransactionChainMismatch:
  1072. return False
  1073. if (self.has_segwit_inputs() or self.has_segwit_outputs()) and not self.proto.cap('segwit'):
  1074. ymsg(f"TX has Segwit inputs or outputs, but {self.coin} doesn't support Segwit!")
  1075. return False
  1076. self.check_pubkey_scripts()
  1077. qmsg(f'Passing {len(keys)} key{suf(keys)} to {self.rpc.daemon.exec_fn}')
  1078. if self.has_segwit_inputs():
  1079. from .addr import KeyGenerator,AddrGenerator
  1080. kg = KeyGenerator(self.proto,'std')
  1081. ag = AddrGenerator(self.proto,'segwit')
  1082. keydict = MMGenDict([(d.addr,d.sec) for d in keys])
  1083. sig_data = []
  1084. for d in self.inputs:
  1085. e = {k:getattr(d,k) for k in ('txid','vout','scriptPubKey','amt')}
  1086. e['amount'] = e['amt']
  1087. del e['amt']
  1088. if d.mmtype == 'S':
  1089. e['redeemScript'] = ag.to_segwit_redeem_script(kg.gen_data(keydict[d.addr]))
  1090. sig_data.append(e)
  1091. msg_r(f'Signing transaction{tx_num_str}...')
  1092. wifs = [d.sec.wif for d in keys]
  1093. try:
  1094. args = (
  1095. ('signrawtransaction', self.hex,sig_data,wifs,self.proto.sighash_type),
  1096. ('signrawtransactionwithkey',self.hex,wifs,sig_data,self.proto.sighash_type)
  1097. )['sign_with_key' in self.rpc.caps]
  1098. ret = await self.rpc.call(*args)
  1099. except Exception as e:
  1100. msg(yellow((
  1101. e.args[0],
  1102. 'This is not the BCH chain.\nRe-run the script without the --coin=bch option.'
  1103. )['Invalid sighash param' in e.args[0]]))
  1104. return False
  1105. try:
  1106. self.hex = HexStr(ret['hex'])
  1107. self.parsed_hex = dtx = DeserializedTX(self.proto,self.hex)
  1108. new = MMGenTX.Signed(data=self.__dict__)
  1109. tx_decoded = await self.rpc.call('decoderawtransaction',ret['hex'])
  1110. new.compare_size_and_estimated_size(tx_decoded)
  1111. new.check_hex_tx_matches_mmgen_tx(dtx)
  1112. new.coin_txid = CoinTxID(dtx['txid'])
  1113. if not new.coin_txid == tx_decoded['txid']:
  1114. raise BadMMGenTxID('txid mismatch (after signing)')
  1115. msg('OK')
  1116. return new
  1117. except Exception as e:
  1118. try: m = '{}'.format(e.args[0])
  1119. except: m = repr(e.args[0])
  1120. msg('\n'+yellow(m))
  1121. if g.traceback:
  1122. import traceback
  1123. ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info())))
  1124. return False
  1125. class Signed(Completed):
  1126. desc = 'signed transaction'
  1127. ext = 'sigtx'
  1128. def __init__(self,*args,**kwargs):
  1129. if 'tw' in kwargs:
  1130. self.tw = kwargs['tw']
  1131. del kwargs['tw']
  1132. super().__init__(*args,**kwargs)
  1133. if not self.check_sigs():
  1134. die(1,'Transaction is not signed!')
  1135. # check that a malicious, compromised or malfunctioning coin daemon hasn't altered hex tx data:
  1136. # does not check witness or signature data
  1137. def check_hex_tx_matches_mmgen_tx(self,dtx):
  1138. m = 'A malicious or malfunctioning coin daemon or other program may have altered your data!'
  1139. lt = dtx['lock_time']
  1140. if lt != int(self.locktime or 0):
  1141. m2 = 'Transaction hex nLockTime ({}) does not match MMGen transaction nLockTime ({})\n{}'
  1142. raise TxHexMismatch(m2.format(lt,self.locktime,m))
  1143. def check_equal(desc,hexio,mmio):
  1144. if mmio != hexio:
  1145. msg('\nMMGen {}:\n{}'.format(desc,pp_fmt(mmio)))
  1146. msg('Hex {}:\n{}'.format(desc,pp_fmt(hexio)))
  1147. m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n'
  1148. raise TxHexMismatch((m2+m).format(desc.capitalize()))
  1149. seq_hex = [int(i['nSeq'],16) for i in dtx['txins']]
  1150. seq_mmgen = [i.sequence or self.proto.max_int for i in self.inputs]
  1151. check_equal('sequence numbers',seq_hex,seq_mmgen)
  1152. d_hex = sorted((i['txid'],i['vout']) for i in dtx['txins'])
  1153. d_mmgen = sorted((i.txid,i.vout) for i in self.inputs)
  1154. check_equal('inputs',d_hex,d_mmgen)
  1155. d_hex = sorted((o['address'],self.proto.coin_amt(o['amount'])) for o in dtx['txouts'])
  1156. d_mmgen = sorted((o.addr,o.amt) for o in self.outputs)
  1157. check_equal('outputs',d_hex,d_mmgen)
  1158. uh = dtx['unsigned_hex']
  1159. if str(self.txid) != make_chksum_6(bytes.fromhex(uh)).upper():
  1160. raise TxHexMismatch(f'MMGen TxID ({self.txid}) does not match hex transaction data!\n{m}')
  1161. def compare_size_and_estimated_size(self,tx_decoded):
  1162. est_vsize = self.estimate_size()
  1163. d = tx_decoded
  1164. vsize = d['vsize'] if 'vsize' in d else d['size']
  1165. vmsg(f'\nVsize: {vsize} (true) {est_vsize} (estimated)')
  1166. ratio = float(est_vsize) / vsize
  1167. if not (0.95 < ratio < 1.05): # allow for 5% error
  1168. from .exception import BadTxSizeEstimate
  1169. raise BadTxSizeEstimate(fmt(f"""
  1170. Estimated transaction vsize is {ratio:1.2f} times the true vsize
  1171. Your transaction fee estimates will be inaccurate
  1172. Please re-create and re-sign the transaction using the option --vsize-adj={1/ratio:1.2f}
  1173. """).strip())
  1174. async def get_status(self,status=False):
  1175. class r(object):
  1176. pass
  1177. async def is_in_wallet():
  1178. try: ret = await self.rpc.call('gettransaction',self.coin_txid)
  1179. except: return False
  1180. if ret.get('confirmations',0) > 0:
  1181. r.confs = ret['confirmations']
  1182. return True
  1183. else:
  1184. return False
  1185. async def is_in_utxos():
  1186. try: return 'txid' in await self.rpc.call('getrawtransaction',self.coin_txid,True)
  1187. except: return False
  1188. async def is_in_mempool():
  1189. try: return 'height' in await self.rpc.call('getmempoolentry',self.coin_txid)
  1190. except: return False
  1191. async def is_replaced():
  1192. if await is_in_mempool():
  1193. return False
  1194. try:
  1195. ret = await self.rpc.call('gettransaction',self.coin_txid)
  1196. except:
  1197. return False
  1198. else:
  1199. if 'bip125-replaceable' in ret and ret.get('confirmations',1) <= 0:
  1200. r.replacing_confs = -ret['confirmations']
  1201. r.replacing_txs = ret['walletconflicts']
  1202. return True
  1203. else:
  1204. return False
  1205. if await is_in_mempool():
  1206. if status:
  1207. d = await self.rpc.call('gettransaction',self.coin_txid)
  1208. rep = ('' if d.get('bip125-replaceable') == 'yes' else 'NOT ') + 'replaceable'
  1209. t = d['timereceived']
  1210. if opt.quiet:
  1211. msg('Transaction is in mempool')
  1212. else:
  1213. msg(f'TX status: in mempool, {rep}')
  1214. msg('Sent {} ({} ago)'.format(
  1215. time.strftime('%c',time.gmtime(t)),
  1216. secs_to_dhms(int(time.time()-t))) )
  1217. else:
  1218. msg('Warning: transaction is in mempool!')
  1219. elif await is_in_wallet():
  1220. die(0,f'Transaction has {r.confs} confirmation{suf(r.confs)}')
  1221. elif await is_in_utxos():
  1222. die(2,red('ERROR: transaction is in the blockchain (but not in the tracking wallet)!'))
  1223. elif await is_replaced():
  1224. msg('Transaction has been replaced')
  1225. msg('Replacement transaction ' + (
  1226. f'has {r.replacing_confs} confirmation{suf(r.replacing_confs)}'
  1227. if r.replacing_confs else
  1228. 'is in mempool' ) )
  1229. if not opt.quiet:
  1230. msg('Replacing transactions:')
  1231. d = []
  1232. for txid in r.replacing_txs:
  1233. try: d.append(await self.rpc.call('getmempoolentry',txid))
  1234. except: d.append({})
  1235. for txid,mp_entry in zip(r.replacing_txs,d):
  1236. msg(f' {txid}' + (' in mempool' if 'height' in mp_entry else '') )
  1237. die(0,'')
  1238. def confirm_send(self):
  1239. confirm_or_raise(
  1240. ('' if opt.quiet else "Once this transaction is sent, there's no taking it back!"),
  1241. f'broadcast this transaction to the {self.proto.coin} {self.proto.network.upper()} network',
  1242. ('YES' if opt.quiet or opt.yes else 'YES, I REALLY WANT TO DO THIS') )
  1243. msg('Sending transaction')
  1244. async def send(self,prompt_user=True,exit_on_fail=False):
  1245. self.check_correct_chain()
  1246. self.check_pubkey_scripts()
  1247. self.check_hex_tx_matches_mmgen_tx(DeserializedTX(self.proto,self.hex))
  1248. if not g.bogus_send:
  1249. if self.has_segwit_outputs() and not self.rpc.info('segwit_is_active'):
  1250. die(2,'Transaction has Segwit outputs, but this blockchain does not support Segwit'
  1251. + ' at the current height')
  1252. if self.fee > self.proto.max_tx_fee:
  1253. die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
  1254. self.fee,
  1255. self.proto.name,
  1256. self.proto.max_tx_fee,
  1257. self.proto.coin ))
  1258. await self.get_status()
  1259. if prompt_user:
  1260. self.confirm_send()
  1261. if g.bogus_send:
  1262. ret = None
  1263. else:
  1264. try:
  1265. ret = await self.rpc.call('sendrawtransaction',self.hex)
  1266. except Exception as e:
  1267. errmsg = e
  1268. ret = False
  1269. if ret == False: # TODO: test send errors
  1270. if errmsg.count('Signature must use SIGHASH_FORKID'):
  1271. m = ('The Aug. 1 2017 UAHF has activated on this chain.\n'
  1272. + 'Re-run the script with the --coin=bch option.' )
  1273. elif errmsg.count('Illegal use of SIGHASH_FORKID'):
  1274. m = ('The Aug. 1 2017 UAHF is not yet active on this chain.\n'
  1275. + 'Re-run the script without the --coin=bch option.' )
  1276. elif errmsg.count('64: non-final'):
  1277. m = "Transaction with nLockTime {!r} can't be included in this block!".format(
  1278. strfmt_locktime(self.get_hex_locktime()) )
  1279. else:
  1280. m = errmsg
  1281. ymsg(m)
  1282. rmsg(f'Send of MMGen transaction {self.txid} failed')
  1283. if exit_on_fail:
  1284. sys.exit(1)
  1285. return False
  1286. else:
  1287. if g.bogus_send:
  1288. m = 'BOGUS transaction NOT sent: {}'
  1289. else:
  1290. m = 'Transaction sent: {}'
  1291. assert ret == self.coin_txid, 'txid mismatch (after sending)'
  1292. msg(m.format(self.coin_txid.hl()))
  1293. self.add_timestamp()
  1294. self.add_blockcount()
  1295. self.desc = 'sent transaction'
  1296. return True
  1297. def print_contract_addr(self):
  1298. pass
  1299. @staticmethod
  1300. async def get_tracking_wallet(filename):
  1301. from .txfile import MMGenTxFile
  1302. tmp_tx = MMGenTX.Base()
  1303. MMGenTxFile(tmp_tx).parse(filename,metadata_only=True)
  1304. if tmp_tx.proto.tokensym:
  1305. from .twctl import TrackingWallet
  1306. return await TrackingWallet(tmp_tx.proto)
  1307. else:
  1308. return None
  1309. class Bump(Completed,New):
  1310. desc = 'fee-bumped transaction'
  1311. ext = 'rawtx'
  1312. min_fee = None
  1313. bump_output_idx = None
  1314. def __init__(self,data,send,tw=None):
  1315. MMGenTX.Completed.__init__(self,data=data)
  1316. self.tw = tw
  1317. if not self.is_replaceable():
  1318. die(1,f'Transaction {self.txid} is not replaceable')
  1319. # If sending, require original tx to be sent
  1320. if send and not self.coin_txid:
  1321. die(1,'Transaction {self.txid!r} was not broadcast to the network')
  1322. self.coin_txid = ''
  1323. def check_sufficient_funds_for_bump(self):
  1324. if not [o.amt for o in self.outputs if o.amt >= self.min_fee]:
  1325. die(1,
  1326. 'Transaction cannot be bumped.\n' +
  1327. f'All outputs contain less than the minimum fee ({self.min_fee} {self.coin})')
  1328. def choose_output(self):
  1329. chg_idx = self.get_chg_output_idx()
  1330. init_reply = opt.output_to_reduce
  1331. def check_sufficient_funds(o_amt):
  1332. if o_amt < self.min_fee:
  1333. msg(f'Minimum fee ({self.min_fee} {self.coin}) is greater than output amount ({o_amt} {self.coin})')
  1334. return False
  1335. return True
  1336. if len(self.outputs) == 1:
  1337. if check_sufficient_funds(self.outputs[0].amt):
  1338. self.bump_output_idx = 0
  1339. return 0
  1340. else:
  1341. die(1,'Insufficient funds to bump transaction')
  1342. while True:
  1343. if init_reply == None:
  1344. m = 'Choose an output to deduct the fee from (Hit ENTER for the change output): '
  1345. reply = line_input(m) or 'c'
  1346. else:
  1347. reply,init_reply = init_reply,None
  1348. if chg_idx == None and not is_int(reply):
  1349. msg('Output must be an integer')
  1350. elif chg_idx != None and not is_int(reply) and reply != 'c':
  1351. msg("Output must be an integer, or 'c' for the change output")
  1352. else:
  1353. idx = chg_idx if reply == 'c' else (int(reply) - 1)
  1354. if idx < 0 or idx >= len(self.outputs):
  1355. msg(f'Output must be in the range 1-{len(self.outputs)}')
  1356. else:
  1357. o_amt = self.outputs[idx].amt
  1358. cm = ' (change output)' if chg_idx == idx else ''
  1359. prompt = f'Fee will be deducted from output {idx+1}{cm} ({o_amt} {self.coin})'
  1360. if check_sufficient_funds(o_amt):
  1361. if opt.yes or keypress_confirm(prompt+'. OK?',default_yes=True):
  1362. if opt.yes:
  1363. msg(prompt)
  1364. self.bump_output_idx = idx
  1365. return idx
  1366. @property
  1367. def min_fee(self):
  1368. return self.sum_inputs() - self.sum_outputs() + self.relay_fee
  1369. def bump_fee(self,idx,fee):
  1370. self.update_output_amt(
  1371. idx,
  1372. self.sum_inputs() - self.sum_outputs(exclude=idx) - fee
  1373. )
  1374. def convert_and_check_fee(self,tx_fee,desc):
  1375. ret = super().convert_and_check_fee(tx_fee,desc)
  1376. if ret < self.min_fee:
  1377. msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} {})'.format(
  1378. ret.hl(),
  1379. desc,
  1380. self.min_fee,
  1381. self.fee_abs2rel(self.min_fee.hl()),
  1382. self.rel_fee_desc,
  1383. c = self.coin ))
  1384. return False
  1385. output_amt = self.outputs[self.bump_output_idx].amt
  1386. if ret >= output_amt:
  1387. msg('{} {c}: {} fee too large. Maximum fee: <{} {c}'.format(
  1388. ret.hl(),
  1389. desc,
  1390. output_amt.hl(),
  1391. c = self.coin ))
  1392. return False
  1393. return ret
  1394. # NOT MAINTAINED
  1395. # class Split(Base):
  1396. #
  1397. # async def get_outputs_from_cmdline(self,mmid): # TODO: check that addr is empty
  1398. #
  1399. # from .addrdata import TwAddrData
  1400. # ad_w = await TwAddrData()
  1401. #
  1402. # if is_mmgen_id(self.proto,mmid):
  1403. # coin_addr = mmaddr2coinaddr(mmid,ad_w,None) if is_mmgen_id(self.proto,mmid) else CoinAddr(mmid)
  1404. # self.add_output(coin_addr,self.proto.coin_amt('0'),is_chg=True)
  1405. # else:
  1406. # die(2,'{}: invalid command-line argument'.format(mmid))
  1407. #
  1408. # self.add_mmaddrs_to_outputs(ad_w,None)
  1409. #
  1410. # if not segwit_is_active() and self.has_segwit_outputs():
  1411. # fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain'
  1412. # rdie(2,fs.format(g.proj_name))
  1413. #
  1414. # def get_split_fee_from_user(self):
  1415. # if opt.rpc_host2:
  1416. # g.rpc_host = opt.rpc_host2
  1417. # if opt.tx_fees:
  1418. # opt.tx_fee = opt.tx_fees.split(',')[1]
  1419. # return super().get_fee_from_user()
  1420. #
  1421. # async def create_split(self,mmid):
  1422. #
  1423. # self.outputs = self.MMGenTxOutputList(self)
  1424. # await self.get_outputs_from_cmdline(mmid)
  1425. #
  1426. # while True:
  1427. # funds_left = self.sum_inputs() - self.get_split_fee_from_user()
  1428. # if funds_left >= 0:
  1429. # p = 'Transaction produces {} {} in change'.format(funds_left.hl(),self.coin)
  1430. # if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
  1431. # if opt.yes:
  1432. # msg(p)
  1433. # break
  1434. # else:
  1435. # self.warn_insufficient_funds(funds_left)
  1436. #
  1437. # self.update_output_amt(0,funds_left)
  1438. #
  1439. # if not opt.yes:
  1440. # self.add_comment() # edits an existing comment
  1441. #
  1442. # await self.create_raw() # creates self.hex, self.txid
  1443. #
  1444. # self.add_timestamp()
  1445. # self.add_blockcount() # TODO
  1446. # self.chain = g.chain
  1447. #
  1448. # assert self.sum_inputs() - self.sum_outputs() <= self.proto.max_tx_fee
  1449. #
  1450. # qmsg('Transaction successfully created')
  1451. #
  1452. # if not opt.yes:
  1453. # self.view_with_prompt('View transaction details?')