tx.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
  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: Bitcoin transaction routines
  20. """
  21. import sys,os
  22. from stat import *
  23. from binascii import unhexlify
  24. from mmgen.common import *
  25. from mmgen.obj import *
  26. def is_mmgen_seed_id(s): return SeedID(sid=s,on_fail='silent')
  27. def is_mmgen_idx(s): return AddrIdx(s,on_fail='silent')
  28. def is_mmgen_id(s): return MMGenID(s,on_fail='silent')
  29. def is_btc_addr(s): return BTCAddr(s,on_fail='silent')
  30. def is_b58_str(s):
  31. from mmgen.bitcoin import b58a
  32. return set(list(s)) <= set(b58a)
  33. def is_wif(s):
  34. if s == '': return False
  35. from mmgen.bitcoin import wif2hex
  36. return bool(wif2hex(s))
  37. class MMGenTxInputOldFmt(MMGenListItem): # for converting old tx files only
  38. tr = {'amount':'amt', 'address':'addr', 'confirmations':'confs','comment':'label'}
  39. attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','wif'
  40. attrs_priv = 'tr',
  41. class MMGenTxInput(MMGenListItem):
  42. attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif','sequence'
  43. label = MMGenListItemAttr('label','MMGenAddrLabel')
  44. class MMGenTxOutput(MMGenListItem):
  45. attrs = 'txid','vout','amt','label','mmid','addr','have_wif','is_chg'
  46. label = MMGenListItemAttr('label','MMGenAddrLabel')
  47. class MMGenTX(MMGenObject):
  48. ext = 'rawtx'
  49. raw_ext = 'rawtx'
  50. sig_ext = 'sigtx'
  51. txid_ext = 'txid'
  52. desc = 'transaction'
  53. def __init__(self,filename=None):
  54. self.inputs = []
  55. self.inputs_enc = []
  56. self.outputs = []
  57. self.outputs_enc = []
  58. self.send_amt = BTCAmt('0') # total amt minus change
  59. self.hex = '' # raw serialized hex transaction
  60. self.label = MMGenTXLabel('')
  61. self.txid = ''
  62. self.btc_txid = ''
  63. self.timestamp = ''
  64. self.chksum = ''
  65. self.fmt_data = ''
  66. self.blockcount = 0
  67. if filename:
  68. if get_extension(filename) == self.sig_ext:
  69. self.mark_signed()
  70. self.parse_tx_file(filename)
  71. def add_output(self,btcaddr,amt,is_chg=None):
  72. self.outputs.append(MMGenTxOutput(addr=btcaddr,amt=amt,is_chg=is_chg))
  73. def get_chg_output_idx(self):
  74. for i in range(len(self.outputs)):
  75. if self.outputs[i].is_chg == True:
  76. return i
  77. return None
  78. def update_output_amt(self,idx,amt):
  79. o = self.outputs[idx].__dict__
  80. o['amt'] = amt
  81. self.outputs[idx] = MMGenTxOutput(**o)
  82. def del_output(self,idx):
  83. self.outputs.pop(idx)
  84. def sum_outputs(self,exclude=None):
  85. olist = self.outputs if exclude == None else \
  86. self.outputs[:exclude] + self.outputs[exclude+1:]
  87. return BTCAmt(sum([e.amt for e in olist]))
  88. def add_mmaddrs_to_outputs(self,ad_w,ad_f):
  89. a = [e.addr for e in self.outputs]
  90. d = ad_w.make_reverse_dict(a)
  91. d.update(ad_f.make_reverse_dict(a))
  92. for e in self.outputs:
  93. if e.addr and e.addr in d:
  94. e.mmid,f = d[e.addr]
  95. if f: e.label = f
  96. # def encode_io(self,desc):
  97. # tr = getattr((MMGenTxOutput,MMGenTxInput)[desc=='inputs'],'tr')
  98. # tr_rev = dict([(v,k) for k,v in tr.items()])
  99. # return [dict([(tr_rev[e] if e in tr_rev else e,getattr(d,e)) for e in d.__dict__])
  100. # for d in getattr(self,desc)]
  101. #
  102. def create_raw(self,c):
  103. i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs]
  104. if self.inputs[0].sequence:
  105. i[0]['sequence'] = self.inputs[0].sequence
  106. o = dict([(e.addr,e.amt) for e in self.outputs])
  107. self.hex = c.createrawtransaction(i,o)
  108. self.txid = MMGenTxID(make_chksum_6(unhexlify(self.hex)).upper())
  109. # returns true if comment added or changed
  110. def add_comment(self,infile=None):
  111. if infile:
  112. self.label = MMGenTXLabel(get_data_from_file(infile,'transaction comment'))
  113. else: # get comment from user, or edit existing comment
  114. m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.label)]
  115. if keypress_confirm(m,default_yes=False):
  116. while True:
  117. s = MMGenTXLabel(my_raw_input('Comment: ',insert_txt=self.label))
  118. if s:
  119. lbl_save = self.label
  120. self.label = s
  121. return (True,False)[lbl_save == self.label]
  122. else:
  123. msg('Invalid comment')
  124. return False
  125. def edit_comment(self):
  126. return self.add_comment(self)
  127. # https://bitcoin.stackexchange.com/questions/1195/
  128. # how-to-calculate-transaction-size-before-sending
  129. # 180: uncompressed, 148: compressed
  130. def get_size(self):
  131. if not self.inputs or not self.outputs: return None
  132. return len(self.inputs)*180 + len(self.outputs)*34 + 10
  133. def get_fee(self):
  134. return self.sum_inputs() - self.sum_outputs()
  135. def btc2spb(self,btc_fee):
  136. return int(btc_fee/g.satoshi/self.get_size())
  137. def get_relay_fee(self):
  138. assert self.get_size()
  139. kb_fee = BTCAmt(bitcoin_connection().getinfo()['relayfee'])
  140. vmsg('Relay fee: {} BTC/kB'.format(kb_fee))
  141. return kb_fee * self.get_size() / 1024
  142. def convert_fee_spec(self,tx_fee,tx_size,on_fail='throw'):
  143. if BTCAmt(tx_fee,on_fail='silent'):
  144. return BTCAmt(tx_fee)
  145. elif len(tx_fee) >= 2 and tx_fee[-1] == 's' and is_int(tx_fee[:-1]) and int(tx_fee[:-1]) >= 1:
  146. if tx_size:
  147. return BTCAmt(int(tx_fee[:-1]) * tx_size * g.satoshi)
  148. else:
  149. return None
  150. else:
  151. if on_fail == 'return':
  152. return False
  153. elif on_fail == 'throw':
  154. assert False, "'{}': invalid tx-fee argument".format(tx_fee)
  155. def get_usr_fee(self,tx_fee,desc='Missing description'):
  156. btc_fee = self.convert_fee_spec(tx_fee,self.get_size(),on_fail='return')
  157. if btc_fee == None:
  158. msg("'{}': cannot convert satoshis-per-byte to BTC because transaction size is unknown".format(tx_fee))
  159. assert False # because we shouldn't be calling this if tx size is unknown
  160. elif btc_fee == False:
  161. msg("'{}': invalid TX fee (not a BTC amount or satoshis-per-byte specification)".format(tx_fee))
  162. return False
  163. elif btc_fee > g.max_tx_fee:
  164. msg('{} BTC: {} fee too large (maximum fee: {} BTC)'.format(btc_fee,desc,g.max_tx_fee))
  165. return False
  166. elif btc_fee < self.get_relay_fee():
  167. msg('{} BTC: {} fee too small (below relay fee of {} BTC)'.format(str(btc_fee),desc,str(self.get_relay_fee())))
  168. return False
  169. else:
  170. return btc_fee
  171. def get_usr_fee_interactive(self,tx_fee=None,desc='Starting'):
  172. btc_fee = None
  173. while True:
  174. if tx_fee:
  175. btc_fee = self.get_usr_fee(tx_fee,desc)
  176. if btc_fee:
  177. m = ('',' (after {}x adjustment)'.format(opt.tx_fee_adj))[opt.tx_fee_adj != 1]
  178. p = '{} TX fee{}: {} BTC ({} satoshis per byte)'.format(desc,m,
  179. btc_fee.hl(),pink(str(self.btc2spb(btc_fee))))
  180. if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
  181. if opt.yes: msg(p)
  182. return btc_fee
  183. tx_fee = my_raw_input('Enter transaction fee: ')
  184. desc = 'User-selected'
  185. # inputs methods
  186. def list_wifs(self,desc,mmaddrs_only=False):
  187. return [e.wif for e in getattr(self,desc) if e.mmid] if mmaddrs_only \
  188. else [e.wif for e in getattr(self,desc)]
  189. def delete_attrs(self,desc,attr):
  190. for e in getattr(self,desc):
  191. if hasattr(e,attr): delattr(e,attr)
  192. def decode_io(self,desc,data):
  193. io = (MMGenTxOutput,MMGenTxInput)[desc=='inputs']
  194. return [io(**dict([(k,d[k]) for k in io.attrs
  195. if k in d and d[k] not in ('',None)])) for d in data]
  196. def decode_io_oldfmt(self,data):
  197. io = MMGenTxInputOldFmt
  198. tr_rev = dict([(v,k) for k,v in io.tr.items()])
  199. copy_keys = [tr_rev[k] if k in tr_rev else k for k in io.attrs]
  200. return [io(**dict([(io.tr[k] if k in io.tr else k,d[k])
  201. for k in copy_keys if k in d and d[k] != ''])) for d in data]
  202. def copy_inputs_from_tw(self,data):
  203. self.inputs = self.decode_io('inputs',[e.__dict__ for e in data])
  204. def get_input_sids(self):
  205. return set([e.mmid[:8] for e in self.inputs if e.mmid])
  206. def get_output_sids(self):
  207. return set([e.mmid[:8] for e in self.outputs if e.mmid])
  208. def sum_inputs(self):
  209. return sum([e.amt for e in self.inputs])
  210. def add_timestamp(self):
  211. self.timestamp = make_timestamp()
  212. def add_blockcount(self,c):
  213. self.blockcount = int(c.getblockcount())
  214. def format(self):
  215. from mmgen.bitcoin import b58encode
  216. lines = [
  217. '{} {} {} {}'.format(
  218. self.txid,
  219. self.send_amt,
  220. self.timestamp,
  221. self.blockcount
  222. ),
  223. self.hex,
  224. repr([e.__dict__ for e in self.inputs]),
  225. repr([e.__dict__ for e in self.outputs])
  226. ]
  227. if self.label:
  228. lines.append(b58encode(self.label.encode('utf8')))
  229. if self.btc_txid:
  230. if not self.label: lines.append('-') # keep old tx files backwards compatible
  231. lines.append(self.btc_txid)
  232. self.chksum = make_chksum_6(' '.join(lines))
  233. self.fmt_data = '\n'.join([self.chksum] + lines)+'\n'
  234. def get_non_mmaddrs(self,desc):
  235. return list(set([i.addr for i in getattr(self,desc) if not i.mmid]))
  236. # return true or false, don't exit
  237. def sign(self,c,tx_num_str,keys):
  238. if not keys:
  239. msg('No keys. Cannot sign!')
  240. return False
  241. qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k')))
  242. dmsg('Keys:\n %s' % '\n '.join(keys))
  243. sig_data = [dict([(k,getattr(d,k)) for k in 'txid','vout','scriptPubKey'])
  244. for d in self.inputs]
  245. dmsg('Sig data:\n%s' % pp_format(sig_data))
  246. dmsg('Raw hex:\n%s' % self.hex)
  247. msg_r('Signing transaction{}...'.format(tx_num_str))
  248. sig_tx = c.signrawtransaction(self.hex,sig_data,keys)
  249. if sig_tx['complete']:
  250. msg('OK')
  251. self.hex = sig_tx['hex']
  252. self.mark_signed()
  253. vmsg('Signed transaction size: {}'.format(len(self.hex)/2))
  254. return True
  255. else:
  256. msg('failed\nBitcoind returned the following errors:')
  257. pp_msg(sig_tx['errors'])
  258. return False
  259. def mark_raw(self):
  260. self.desc = 'transaction'
  261. self.ext = self.raw_ext
  262. def mark_signed(self):
  263. self.desc = 'signed transaction'
  264. self.ext = self.sig_ext
  265. def is_signed(self,color=False):
  266. ret = self.desc == 'signed transaction'
  267. return (red,green)[ret](str(ret)) if color else ret
  268. def check_signed(self,c):
  269. d = c.decoderawtransaction(self.hex)
  270. ret = bool(d['vin'][0]['scriptSig']['hex'])
  271. if ret: self.mark_signed()
  272. return ret
  273. def send(self,c,prompt_user=True):
  274. if self.get_fee() > g.max_tx_fee:
  275. die(2,'Transaction fee ({}) greater than max_tx_fee ({})!'.format(self.get_fee(),g.max_tx_fee))
  276. if prompt_user:
  277. m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)]
  278. m2 = 'broadcast this transaction to the network'
  279. m3 = ('YES, I REALLY WANT TO DO THIS','YES')[bool(opt.quiet or opt.yes)]
  280. confirm_or_exit(m1,m2,m3)
  281. msg('Sending transaction')
  282. if os.getenv('MMGEN_BOGUS_SEND'):
  283. ret = 'deadbeef' * 8
  284. m = 'BOGUS transaction NOT sent: %s'
  285. else:
  286. ret = c.sendrawtransaction(self.hex) # exits on failure
  287. m = 'Transaction sent: %s'
  288. if ret:
  289. self.btc_txid = BitcoinTxID(ret,on_fail='return')
  290. if self.btc_txid:
  291. self.desc = 'sent transaction'
  292. msg(m % self.btc_txid.hl())
  293. self.add_timestamp()
  294. self.add_blockcount(c)
  295. return True
  296. # rpc implementation exits on failure, so we won't get here
  297. msg('Sending of transaction {} failed'.format(self.txid))
  298. return False
  299. def write_txid_to_file(self,ask_write=False,ask_write_default_yes=True):
  300. fn = '%s[%s].%s' % (self.txid,self.send_amt,self.txid_ext)
  301. write_data_to_file(fn,self.btc_txid+'\n','transaction ID',
  302. ask_write=ask_write,
  303. ask_write_default_yes=ask_write_default_yes)
  304. def write_to_file(self,add_desc='',ask_write=True,ask_write_default_yes=False,ask_tty=True,ask_overwrite=True):
  305. if ask_write == False:
  306. ask_write_default_yes=True
  307. self.format()
  308. spbs = ('',',{}'.format(self.btc2spb(self.get_fee())))[self.is_rbf()]
  309. fn = '{}[{}{}].{}'.format(self.txid,self.send_amt,spbs,self.ext)
  310. write_data_to_file(fn,self.fmt_data,self.desc+add_desc,
  311. ask_overwrite=ask_overwrite,
  312. ask_write=ask_write,
  313. ask_tty=ask_tty,
  314. ask_write_default_yes=ask_write_default_yes)
  315. def view_with_prompt(self,prompt=''):
  316. prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view'
  317. reply = prompt_and_get_char(prompt,'YyNnVvTt',enter_ok=True)
  318. if reply and reply in 'YyVvTt':
  319. self.view(pager=reply in 'Vv',terse=reply in 'Tt')
  320. def view(self,pager=False,pause=True,terse=False):
  321. o = self.format_view(terse=terse)
  322. if pager: do_pager(o)
  323. else:
  324. sys.stdout.write(o)
  325. from mmgen.term import get_char
  326. if pause:
  327. get_char('Press any key to continue: ')
  328. msg('')
  329. # def is_rbf_fromhex(self,color=False):
  330. # try:
  331. # dec_tx = bitcoin_connection().decoderawtransaction(self.hex)
  332. # except:
  333. # return yellow('Unknown') if color else None
  334. # rbf = bool(dec_tx['vin'][0]['sequence'] == g.max_int - 2)
  335. # return (red,green)[rbf](str(rbf)) if color else rbf
  336. def is_rbf(self,color=False):
  337. ret = None < self.inputs[0].sequence <= g.max_int - 2
  338. return (red,green)[ret](str(ret)) if color else ret
  339. def signal_for_rbf(self):
  340. self.inputs[0].sequence = g.max_int - 2
  341. def format_view(self,terse=False):
  342. try:
  343. blockcount = bitcoin_connection().getblockcount()
  344. except:
  345. blockcount = None
  346. hdr_fs = (
  347. 'TRANSACTION DATA\n\nHeader: [ID:{}] [{} BTC] [{} UTC] [RBF:{}] [Signed:{}]\n',
  348. 'Transaction {} {} BTC ({} UTC) RBF={} Signed={}\n'
  349. )[bool(terse)]
  350. out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),self.timestamp,
  351. self.is_rbf(color=True),self.is_signed(color=True))
  352. enl = ('\n','')[bool(terse)]
  353. if self.btc_txid: out += 'Bitcoin TxID: {}\n'.format(self.btc_txid.hl())
  354. out += enl
  355. if self.label:
  356. out += 'Comment: %s\n%s' % (self.label.hl(),enl)
  357. out += 'Inputs:\n' + enl
  358. nonmm_str = '(non-{pnm} address){s}'.format(pnm=g.proj_name,s=('',' ')[terse])
  359. # for i in self.inputs: print i #DEBUG
  360. for n,e in enumerate(self.inputs):
  361. if blockcount:
  362. confs = e.confs + blockcount - self.blockcount
  363. days = int(confs * g.mins_per_block / (60*24))
  364. mmid_fmt = e.mmid.fmt(width=len(nonmm_str),encl='()',color=True) if e.mmid \
  365. else MMGenID.hlc(nonmm_str)
  366. if terse:
  367. out += '%3s: %s %s %s BTC' % (n+1, e.addr.fmt(color=True),mmid_fmt, e.amt.hl())
  368. else:
  369. for d in (
  370. (n+1, 'tx,vout:', '%s,%s' % (e.txid, e.vout)),
  371. ('', 'address:', e.addr.fmt(color=True) + ' ' + mmid_fmt),
  372. ('', 'comment:', e.label.hl() if e.label else ''),
  373. ('', 'amount:', '%s BTC' % e.amt.hl()),
  374. ('', 'confirmations:', '%s (around %s days)' % (confs,days) if blockcount else '')
  375. ):
  376. if d[2]: out += ('%3s %-8s %s\n' % d)
  377. out += '\n'
  378. out += 'Outputs:\n' + enl
  379. for n,e in enumerate(self.outputs):
  380. if e.mmid:
  381. app=('',' (chg)')[bool(e.is_chg and terse)]
  382. mmid_fmt = e.mmid.fmt(width=len(nonmm_str),encl='()',color=True,
  383. app=app,appcolor='green')
  384. else:
  385. mmid_fmt = MMGenID.hlc(nonmm_str)
  386. if terse:
  387. out += '%3s: %s %s %s BTC' % (n+1, e.addr.fmt(color=True),mmid_fmt, e.amt.hl())
  388. else:
  389. for d in (
  390. (n+1, 'address:', e.addr.fmt(color=True) + ' ' + mmid_fmt),
  391. ('', 'comment:', e.label.hl() if e.label else ''),
  392. ('', 'amount:', '%s BTC' % e.amt.hl()),
  393. ('', 'change:', green('True') if e.is_chg else '')
  394. ):
  395. if d[2]: out += ('%3s %-8s %s\n' % d)
  396. out += '\n'
  397. fs = (
  398. 'Total input: %s BTC\nTotal output: %s BTC\nTX fee: %s BTC (%s satoshis per byte)\n',
  399. 'In %s BTC - Out %s BTC - Fee %s BTC (%s satoshis/byte)\n'
  400. )[bool(terse)]
  401. total_in = self.sum_inputs()
  402. total_out = self.sum_outputs()
  403. out += fs % (
  404. total_in.hl(),
  405. total_out.hl(),
  406. (total_in-total_out).hl(),
  407. pink(str(self.btc2spb(total_in-total_out))),
  408. )
  409. if opt.verbose:
  410. ts = len(self.hex)/2 if self.hex else 'unknown'
  411. out += 'Transaction size: estimated - {}, actual - {}\n'.format(self.get_size(),ts)
  412. # only tx label may contain non-ascii chars
  413. # encode() is necessary for test suite with PopenSpawn
  414. return out.encode('utf8')
  415. def parse_tx_file(self,infile):
  416. self.parse_tx_data(get_lines_from_file(infile,self.desc+' data'))
  417. def parse_tx_data(self,tx_data):
  418. def do_err(s): die(2,'Invalid %s in transaction file' % s)
  419. if len(tx_data) < 5: do_err('number of lines')
  420. self.chksum = tx_data.pop(0)
  421. if self.chksum != make_chksum_6(' '.join(tx_data)):
  422. do_err('checksum')
  423. if len(tx_data) == 6:
  424. self.btc_txid = BitcoinTxID(tx_data.pop(-1),on_fail='return')
  425. if not self.btc_txid:
  426. do_err('Bitcoin TxID')
  427. if len(tx_data) == 5:
  428. c = tx_data.pop(-1)
  429. if c != '-':
  430. from mmgen.bitcoin import b58decode
  431. comment = b58decode(c)
  432. if comment == False:
  433. do_err('encoded comment (not base58)')
  434. else:
  435. self.label = MMGenTXLabel(comment,on_fail='return')
  436. if not self.label:
  437. do_err('comment')
  438. else:
  439. comment = ''
  440. if len(tx_data) == 4:
  441. metadata,self.hex,inputs_data,outputs_data = tx_data
  442. else:
  443. do_err('number of lines')
  444. if len(metadata.split()) != 4: do_err('metadata')
  445. self.txid,send_amt,self.timestamp,blockcount = metadata.split()
  446. self.txid = MMGenTxID(self.txid)
  447. self.send_amt = BTCAmt(send_amt)
  448. self.blockcount = int(blockcount)
  449. try: unhexlify(self.hex)
  450. except: do_err('hex data')
  451. try: self.inputs = self.decode_io('inputs',eval(inputs_data))
  452. except: do_err('inputs data')
  453. try: self.outputs = self.decode_io('outputs',eval(outputs_data))
  454. except: do_err('btc-to-mmgen address map data')
  455. class MMGenBumpTX(MMGenTX):
  456. min_fee = None
  457. bump_output_idx = None
  458. def __init__(self,filename,send=False):
  459. super(type(self),self).__init__(filename)
  460. if not self.is_rbf():
  461. die(1,"Transaction '{}' is not replaceable (RBF)".format(self.txid))
  462. # If sending, require tx to have been signed and broadcast
  463. if send:
  464. if not self.is_signed():
  465. die(1,"File '{}' is not a signed {} transaction file".format(filename,g.proj_name))
  466. if not self.btc_txid:
  467. die(1,"Transaction '{}' was not broadcast to the network".format(self.txid,g.proj_name))
  468. self.btc_txid = ''
  469. self.mark_raw()
  470. def choose_output(self):
  471. chg_idx = self.get_chg_output_idx()
  472. init_reply = opt.output_to_reduce
  473. while True:
  474. if init_reply == None:
  475. reply = my_raw_input('Which output do you wish to deduct the fee from? ')
  476. else:
  477. reply,init_reply = init_reply,None
  478. if chg_idx == None and not is_int(reply):
  479. msg("Output must be an integer")
  480. elif chg_idx != None and not is_int(reply) and reply != 'c':
  481. msg("Output must be an integer, or 'c' for the change output")
  482. else:
  483. idx = chg_idx if reply == 'c' else (int(reply) - 1)
  484. if idx < 0 or idx >= len(self.outputs):
  485. msg('Output must be in the range 1-{}'.format(len(self.outputs)))
  486. else:
  487. o_amt = self.outputs[idx].amt
  488. cs = ('',' (change output)')[chg_idx == idx]
  489. p = 'Fee will be deducted from output {}{} ({} BTC)'.format(idx+1,cs,o_amt)
  490. if o_amt < self.min_fee:
  491. msg('Minimum fee ({} BTC) is greater than output amount ({} BTC)'.format(
  492. self.min_fee,o_amt))
  493. elif opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
  494. if opt.yes: msg(p)
  495. self.bump_output_idx = idx
  496. return idx
  497. def set_min_fee(self):
  498. self.min_fee = self.sum_inputs() - self.sum_outputs() + self.get_relay_fee()
  499. def get_usr_fee(self,tx_fee,desc):
  500. ret = super(type(self),self).get_usr_fee(tx_fee,desc)
  501. if ret < self.min_fee:
  502. msg('{} BTC: {} fee too small. Minimum fee: {} BTC ({} satoshis per byte)'.format(
  503. ret,desc,self.min_fee,self.btc2spb(self.min_fee)))
  504. return False
  505. output_amt = self.outputs[self.bump_output_idx].amt
  506. if ret >= output_amt:
  507. msg('{} BTC: {} fee too large. Maximum fee: <{} BTC'.format(ret,desc,output_amt))
  508. return False
  509. return ret