tx.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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. from mmgen.term import do_pager
  27. def is_mmgen_seed_id(s): return SeedID(sid=s,on_fail='silent')
  28. def is_mmgen_idx(s): return AddrIdx(s,on_fail='silent')
  29. def is_mmgen_id(s): return MMGenID(s,on_fail='silent')
  30. def is_btc_addr(s): return BTCAddr(s,on_fail='silent')
  31. def is_b58_str(s):
  32. from mmgen.bitcoin import b58a
  33. return set(list(s)) <= set(b58a)
  34. def is_wif(s):
  35. if s == '': return False
  36. compressed = not s[0] == '5'
  37. from mmgen.bitcoin import wiftohex
  38. return wiftohex(s,compressed) is not False
  39. def _wiftoaddr(s):
  40. if s == '': return False
  41. compressed = not s[0] == '5'
  42. from mmgen.bitcoin import wiftohex,privnum2addr
  43. hex_key = wiftohex(s,compressed)
  44. if not hex_key: return False
  45. return privnum2addr(int(hex_key,16),compressed)
  46. def _wiftoaddr_keyconv(wif):
  47. if wif[0] == '5':
  48. from subprocess import check_output
  49. return check_output(['keyconv', wif]).split()[1]
  50. else:
  51. return _wiftoaddr(wif)
  52. def get_wif2addr_f():
  53. if opt.no_keyconv: return _wiftoaddr
  54. from mmgen.addr import test_for_keyconv
  55. return (_wiftoaddr,_wiftoaddr_keyconv)[bool(test_for_keyconv())]
  56. class MMGenTxInputOldFmt(MMGenListItem): # for converting old tx files only
  57. tr = {'amount':'amt', 'address':'addr', 'confirmations':'confs','comment':'label'}
  58. attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','wif'
  59. attrs_priv = 'tr',
  60. class MMGenTxInput(MMGenListItem):
  61. attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif'
  62. label = MMGenListItemAttr('label','MMGenAddrLabel')
  63. class MMGenTxOutput(MMGenListItem):
  64. attrs = 'txid','vout','amt','label','mmid','addr','have_wif'
  65. label = MMGenListItemAttr('label','MMGenAddrLabel')
  66. class MMGenTX(MMGenObject):
  67. ext = 'rawtx'
  68. raw_ext = 'rawtx'
  69. sig_ext = 'sigtx'
  70. txid_ext = 'txid'
  71. desc = 'transaction'
  72. max_fee = BTCAmt('0.01')
  73. def __init__(self,filename=None):
  74. self.inputs = []
  75. self.inputs_enc = []
  76. self.outputs = []
  77. self.outputs_enc = []
  78. self.change_addr = ''
  79. self.size = 0 # size of raw serialized tx
  80. self.fee = BTCAmt('0')
  81. self.send_amt = BTCAmt('0') # total amt minus change
  82. self.hex = '' # raw serialized hex transaction
  83. self.label = MMGenTXLabel('')
  84. self.txid = ''
  85. self.btc_txid = ''
  86. self.timestamp = ''
  87. self.chksum = ''
  88. self.fmt_data = ''
  89. self.blockcount = 0
  90. if filename:
  91. if get_extension(filename) == self.sig_ext:
  92. self.mark_signed()
  93. self.parse_tx_file(filename)
  94. def add_output(self,btcaddr,amt): # 'txid','vout','amount','label','mmid','address'
  95. self.outputs.append(MMGenTxOutput(addr=btcaddr,amt=amt))
  96. def del_output(self,btcaddr):
  97. for i in range(len(self.outputs)):
  98. if self.outputs[i].addr == btcaddr:
  99. self.outputs.pop(i); return
  100. raise ValueError
  101. def sum_outputs(self):
  102. return BTCAmt(sum([e.amt for e in self.outputs]))
  103. def add_mmaddrs_to_outputs(self,ad_w,ad_f):
  104. a = [e.addr for e in self.outputs]
  105. d = ad_w.make_reverse_dict(a)
  106. d.update(ad_f.make_reverse_dict(a))
  107. for e in self.outputs:
  108. if e.addr and e.addr in d:
  109. e.mmid,f = d[e.addr]
  110. if f: e.label = f
  111. # def encode_io(self,desc):
  112. # tr = getattr((MMGenTxOutput,MMGenTxInput)[desc=='inputs'],'tr')
  113. # tr_rev = dict([(v,k) for k,v in tr.items()])
  114. # return [dict([(tr_rev[e] if e in tr_rev else e,getattr(d,e)) for e in d.__dict__])
  115. # for d in getattr(self,desc)]
  116. #
  117. def create_raw(self,c):
  118. i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs]
  119. o = dict([(e.addr,e.amt) for e in self.outputs])
  120. self.hex = c.createrawtransaction(i,o)
  121. self.txid = make_chksum_6(unhexlify(self.hex)).upper()
  122. # returns true if comment added or changed
  123. def add_comment(self,infile=None):
  124. if infile:
  125. self.label = MMGenTXLabel(get_data_from_file(infile,'transaction comment'))
  126. else: # get comment from user, or edit existing comment
  127. m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.label)]
  128. if keypress_confirm(m,default_yes=False):
  129. while True:
  130. s = MMGenTXLabel(my_raw_input('Comment: ',insert_txt=self.label))
  131. if s:
  132. lbl_save = self.label
  133. self.label = s
  134. return (True,False)[lbl_save == self.label]
  135. else:
  136. msg('Invalid comment')
  137. return False
  138. def edit_comment(self):
  139. return self.add_comment(self)
  140. # https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
  141. def calculate_size_and_fee(self,fee_estimate):
  142. self.size = len(self.inputs)*180 + len(self.outputs)*34 + 10
  143. if fee_estimate:
  144. ftype,fee = 'Calculated',fee_estimate*opt.tx_fee_adj*self.size / 1024
  145. else:
  146. ftype,fee = 'User-selected',opt.tx_fee
  147. ufee = None
  148. if not keypress_confirm('{} TX fee is {} BTC. OK?'.format(ftype,fee.hl()),default_yes=True):
  149. while True:
  150. ufee = my_raw_input('Enter transaction fee: ')
  151. if BTCAmt(ufee,on_fail='return'):
  152. ufee = BTCAmt(ufee)
  153. if ufee > self.max_fee:
  154. msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,self.max_fee))
  155. else:
  156. fee = ufee
  157. break
  158. self.fee = fee
  159. vmsg('Inputs:{} Outputs:{} TX size:{}'.format(
  160. len(self.inputs),len(self.outputs),self.size))
  161. vmsg('Fee estimate: {} (1024 bytes, {} confs)'.format(fee_estimate,opt.tx_confs))
  162. m = ('',' (after %sx adjustment)' % opt.tx_fee_adj)[opt.tx_fee_adj != 1 and not ufee]
  163. vmsg('TX fee: {}{}'.format(self.fee,m))
  164. # inputs methods
  165. def list_wifs(self,desc,mmaddrs_only=False):
  166. return [e.wif for e in getattr(self,desc) if e.mmid] if mmaddrs_only \
  167. else [e.wif for e in getattr(self,desc)]
  168. def delete_attrs(self,desc,attr):
  169. for e in getattr(self,desc):
  170. if hasattr(e,attr): delattr(e,attr)
  171. def decode_io(self,desc,data):
  172. io = (MMGenTxOutput,MMGenTxInput)[desc=='inputs']
  173. return [io(**dict([(k,d[k]) for k in io.attrs
  174. if k in d and d[k] not in ('',None)])) for d in data]
  175. def decode_io_oldfmt(self,data):
  176. io = MMGenTxInputOldFmt
  177. tr_rev = dict([(v,k) for k,v in io.tr.items()])
  178. copy_keys = [tr_rev[k] if k in tr_rev else k for k in io.attrs]
  179. return [io(**dict([(io.tr[k] if k in io.tr else k,d[k])
  180. for k in copy_keys if k in d and d[k] != ''])) for d in data]
  181. def copy_inputs_from_tw(self,data):
  182. self.inputs = self.decode_io('inputs',[e.__dict__ for e in data])
  183. def get_input_sids(self):
  184. return set([e.mmid[:8] for e in self.inputs if e.mmid])
  185. def sum_inputs(self):
  186. return sum([e.amt for e in self.inputs])
  187. def add_timestamp(self):
  188. self.timestamp = make_timestamp()
  189. def add_blockcount(self,c):
  190. self.blockcount = int(c.getblockcount())
  191. def format(self):
  192. from mmgen.bitcoin import b58encode
  193. lines = (
  194. '{} {} {} {}'.format(
  195. self.txid,
  196. self.send_amt,
  197. self.timestamp,
  198. (self.blockcount or 'None')
  199. ),
  200. self.hex,
  201. repr([e.__dict__ for e in self.inputs]),
  202. repr([e.__dict__ for e in self.outputs])
  203. ) + ((b58encode(self.label),) if self.label else ())
  204. self.chksum = make_chksum_6(' '.join(lines))
  205. self.fmt_data = '\n'.join((self.chksum,) + lines)+'\n'
  206. def get_non_mmaddrs(self,desc):
  207. return list(set([i.addr for i in getattr(self,desc) if not i.mmid]))
  208. # return true or false, don't exit
  209. def sign(self,c,tx_num_str,keys):
  210. if not keys:
  211. msg('No keys. Cannot sign!')
  212. return False
  213. qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k')))
  214. dmsg('Keys:\n %s' % '\n '.join(keys))
  215. sig_data = [dict([(k,getattr(d,k)) for k in 'txid','vout','scriptPubKey'])
  216. for d in self.inputs]
  217. dmsg('Sig data:\n%s' % pp_format(sig_data))
  218. dmsg('Raw hex:\n%s' % self.hex)
  219. msg_r('Signing transaction{}...'.format(tx_num_str))
  220. sig_tx = c.signrawtransaction(self.hex,sig_data,keys)
  221. if sig_tx['complete']:
  222. msg('OK')
  223. self.hex = sig_tx['hex']
  224. self.mark_signed()
  225. return True
  226. else:
  227. msg('failed\nBitcoind returned the following errors:')
  228. pp_msg(sig_tx['errors'])
  229. return False
  230. def mark_signed(self):
  231. self.desc = 'signed transaction'
  232. self.ext = self.sig_ext
  233. def check_signed(self,c):
  234. d = c.decoderawtransaction(self.hex)
  235. ret = bool(d['vin'][0]['scriptSig']['hex'])
  236. if ret: self.mark_signed()
  237. return ret
  238. def send(self,c,bogus=False):
  239. if bogus:
  240. self.btc_txid = 'deadbeef' * 8
  241. m = 'BOGUS transaction NOT sent: %s'
  242. else:
  243. self.btc_txid = c.sendrawtransaction(self.hex) # exits on failure?
  244. m = 'Transaction sent: %s'
  245. msg(m % self.btc_txid)
  246. def write_txid_to_file(self,ask_write=False,ask_write_default_yes=True):
  247. fn = '%s[%s].%s' % (self.txid,self.send_amt,self.txid_ext)
  248. write_data_to_file(fn,self.btc_txid+'\n','transaction ID',
  249. ask_write=ask_write,
  250. ask_write_default_yes=ask_write_default_yes)
  251. def write_to_file(self,add_desc='',ask_write=True,ask_write_default_yes=False):
  252. if ask_write == False:
  253. ask_write_default_yes=True
  254. self.format()
  255. fn = '%s[%s].%s' % (self.txid,self.send_amt,self.ext)
  256. write_data_to_file(fn,self.fmt_data,self.desc+add_desc,
  257. ask_write=ask_write,
  258. ask_write_default_yes=ask_write_default_yes)
  259. def view_with_prompt(self,prompt=''):
  260. prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view'
  261. reply = prompt_and_get_char(prompt,'YyNnVvTt',enter_ok=True)
  262. if reply and reply in 'YyVvTt':
  263. self.view(pager=reply in 'Vv',terse=reply in 'Tt')
  264. def view(self,pager=False,pause=True,terse=False):
  265. o = self.format_view(terse=terse).encode('utf8')
  266. if pager: do_pager(o)
  267. else:
  268. sys.stdout.write(o)
  269. from mmgen.term import get_char
  270. if pause:
  271. get_char('Press any key to continue: ')
  272. msg('')
  273. def format_view(self,terse=False):
  274. try:
  275. blockcount = bitcoin_connection().getblockcount()
  276. except:
  277. blockcount = None
  278. fs = (
  279. 'TRANSACTION DATA\n\nHeader: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n',
  280. 'Transaction {} - {} BTC - {} UTC\n'
  281. )[bool(terse)]
  282. out = fs.format(self.txid,self.send_amt.hl(),self.timestamp)
  283. enl = ('\n','')[bool(terse)]
  284. if self.label:
  285. out += 'Comment: %s\n%s' % (self.label.hl(),enl)
  286. out += 'Inputs:\n' + enl
  287. nonmm_str = '(non-{pnm} address)'.format(pnm=g.proj_name)
  288. # for i in self.inputs: print i #DEBUG
  289. for n,e in enumerate(self.inputs):
  290. if blockcount:
  291. confs = e.confs + blockcount - self.blockcount
  292. days = int(confs * g.mins_per_block / (60*24))
  293. mmid_fmt = e.mmid.fmt(width=len(nonmm_str),encl='()',color=True) if e.mmid \
  294. else MMGenID.hlc(nonmm_str)
  295. if terse:
  296. out += '%3s: %s %s %s BTC' % (n+1, e.addr.fmt(color=True),mmid_fmt, e.amt.hl())
  297. else:
  298. for d in (
  299. (n+1, 'tx,vout:', '%s,%s' % (e.txid, e.vout)),
  300. ('', 'address:', e.addr.fmt(color=True) + ' ' + mmid_fmt),
  301. ('', 'comment:', e.label.hl() if e.label else ''),
  302. ('', 'amount:', '%s BTC' % e.amt.hl()),
  303. ('', 'confirmations:', '%s (around %s days)' % (confs,days) if blockcount else '')
  304. ):
  305. if d[2]: out += ('%3s %-8s %s\n' % d)
  306. out += '\n'
  307. out += 'Outputs:\n' + enl
  308. for n,e in enumerate(self.outputs):
  309. mmid_fmt = e.mmid.fmt(width=len(nonmm_str),encl='()',color=True) if e.mmid \
  310. else MMGenID.hlc(nonmm_str)
  311. if terse:
  312. out += '%3s: %s %s %s BTC' % (n+1, e.addr.fmt(color=True),mmid_fmt, e.amt.hl())
  313. else:
  314. for d in (
  315. (n+1, 'address:', e.addr.fmt(color=True) + ' ' + mmid_fmt),
  316. ('', 'comment:', e.label.hl() if e.label else ''),
  317. ('', 'amount:', '%s BTC' % e.amt.hl())
  318. ):
  319. if d[2]: out += ('%3s %-8s %s\n' % d)
  320. out += '\n'
  321. fs = (
  322. 'Total input: %s BTC\nTotal output: %s BTC\nTX fee: %s BTC\n',
  323. 'In %s BTC - Out %s BTC - Fee %s BTC\n'
  324. )[bool(terse)]
  325. total_in = self.sum_inputs()
  326. total_out = self.sum_outputs()
  327. out += fs % (
  328. total_in.hl(),
  329. total_out.hl(),
  330. (total_in-total_out).hl()
  331. )
  332. return out
  333. def parse_tx_file(self,infile):
  334. self.parse_tx_data(get_lines_from_file(infile,self.desc+' data'))
  335. def parse_tx_data(self,tx_data):
  336. err_str,err_fmt = '','Invalid %s in transaction file'
  337. if len(tx_data) == 6:
  338. self.chksum,metadata,self.hex,inputs_data,outputs_data,comment = tx_data
  339. elif len(tx_data) == 5:
  340. self.chksum,metadata,self.hex,inputs_data,outputs_data = tx_data
  341. comment = ''
  342. else:
  343. err_str = 'number of lines'
  344. if not err_str:
  345. if self.chksum != make_chksum_6(' '.join(tx_data[1:])):
  346. err_str = 'checksum'
  347. elif len(metadata.split()) != 4:
  348. err_str = 'metadata'
  349. else:
  350. self.txid,send_amt,self.timestamp,blockcount = metadata.split()
  351. self.send_amt = BTCAmt(send_amt)
  352. self.blockcount = int(blockcount)
  353. try: unhexlify(self.hex)
  354. except: err_str = 'hex data'
  355. else:
  356. try: self.inputs = self.decode_io('inputs',eval(inputs_data))
  357. except: err_str = 'inputs data'
  358. else:
  359. try: self.outputs = self.decode_io('outputs',eval(outputs_data))
  360. except: err_str = 'btc-to-mmgen address map data'
  361. else:
  362. if comment:
  363. from mmgen.bitcoin import b58decode
  364. comment = b58decode(comment)
  365. if comment == False:
  366. err_str = 'encoded comment (not base58)'
  367. else:
  368. self.label = MMGenTXLabel(comment,on_fail='return')
  369. if not self.label:
  370. err_str = 'comment'
  371. if err_str:
  372. msg(err_fmt % err_str)
  373. sys.exit(2)