From 3f2a866c47687ca40b5fcfdc7712d4fed1771ba4 Mon Sep 17 00:00:00 2001 From: philemon Date: Mon, 4 Jul 2016 21:47:32 +0300 Subject: [PATCH] New object-oriented TX code --- mmgen/globalvars.py | 7 +- mmgen/main_txcreate.py | 141 +++++------ mmgen/main_txsend.py | 35 ++- mmgen/main_txsign.py | 219 +++++++---------- mmgen/obj.py | 7 +- mmgen/opts.py | 2 +- mmgen/rpc.py | 64 +++-- mmgen/tool.py | 21 +- mmgen/tx.py | 538 ++++++++++++++++++++++++++++------------- mmgen/util.py | 33 ++- scripts/tx-old2new.py | 116 +++++++++ test/tooltest.py | 2 +- 12 files changed, 748 insertions(+), 437 deletions(-) create mode 100755 scripts/tx-old2new.py diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index 5b23d560..acc65353 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -35,6 +35,8 @@ no_license = os.getenv('MMGEN_NOLICENSE') bogus_wallet_data = os.getenv('MMGEN_BOGUS_WALLET_DATA') disable_hold_protect = os.getenv('MMGEN_DISABLE_HOLD_PROTECT') +btc_amt_decimal_places = 8 + from decimal import Decimal tx_fee = Decimal('0.0003') max_tx_fee = Decimal('0.01') @@ -77,8 +79,9 @@ seedfile_exts = ( wallet_ext, seed_ext, mn_ext, brain_ext, incog_ext, incog_hex_ext ) -rawtx_ext = 'raw' -sigtx_ext = 'sig' +rawtx_ext = 'rawtx' +sigtx_ext = 'sigtx' +txid_ext = 'txid' addrfile_ext = 'addrs' addrfile_chksum_ext = 'chk' keyfile_ext = 'keys' diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index cb76255e..9dfd26dc 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -127,7 +127,7 @@ def format_unspent_outputs_for_printing(out,sort_info,total): return \ 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'.format( - make_timestr(), ' '.join(sort_info), '\n'.join(pout), total + make_timestr(), ' '.join(sort_info), '\n'.join(pout), normalize_btc_amt(total) ) @@ -146,7 +146,7 @@ def sort_and_view(unspent): sort,group,show_days,show_mmaddr,reverse = 'age',False,False,True,True unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default - total = trim_exponent(sum([i['amount'] for i in unspent])) + total = sum([i['amount'] for i in unspent]) max_acct_len = max([len(i['mmid']+' '+i['comment']) for i in unspent]) hdr_fmt = 'UNSPENT OUTPUTS (sort order: %s) Total BTC: %s' @@ -188,7 +188,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen elif sort == 'txid' and a['txid'] == b['txid']: b['skip'] = 'txid' for i in unsp: - amt = str(trim_exponent(i['amount'])) + amt = str(normalize_btc_amt(i['amount'])) lfill = 3 - len(amt.split('.')[0]) if '.' in amt else 3 - len(amt) i['amt'] = ' '*lfill + amt i['days'] = int(i['confirmations'] * g.mins_per_block / (60*24)) @@ -216,7 +216,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen if group and (sort == 'address' or sort == 'txid'): sort_info.append('grouped') - out = [hdr_fmt % (' '.join(sort_info), total), table_hdr] + out = [hdr_fmt % (' '.join(sort_info), normalize_btc_amt(total)), table_hdr] out += [fs % (str(n+1)+')',i['tx'],i['vout'],i['addr'],i['amt'],i['age']) for n,i in enumerate(unsp)] @@ -301,12 +301,12 @@ def mmaddr2btcaddr_unspent(unspent,mmaddr): def mmaddr2btcaddr(c,mmaddr,ail_w,ail_f): # assume mmaddr has already been checked - btcaddr = ail_w.mmaddr2btcaddr(mmaddr) + btc_addr = ail_w.mmaddr2btcaddr(mmaddr) - if not btcaddr: + if not btc_addr: if ail_f: - btcaddr = ail_f.mmaddr2btcaddr(mmaddr) - if btcaddr: + btc_addr = ail_f.mmaddr2btcaddr(mmaddr) + if btc_addr: msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr)) if not keypress_confirm('Continue anyway?'): sys.exit(1) @@ -315,15 +315,31 @@ def mmaddr2btcaddr(c,mmaddr,ail_w,ail_f): else: die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr)) - return btcaddr + return btc_addr -def make_b2m_map(inputs_data,tx_out,ail_w,ail_f): - d = dict([(d['address'], (d['mmid'],d['comment'])) - for d in inputs_data if d['mmid']]) - d.update(ail_w.make_reverse_dict(tx_out.keys())) - d.update(ail_f.make_reverse_dict(tx_out.keys())) - return d +def get_fee_estimate(): + if 'tx_fee' in opt.set_by_user: + return None + else: + ret = c.estimatefee(opt.tx_confs) + if ret != -1: + return ret + else: + m = """ +Fee estimation failed! +Your possible courses of action (from best to worst): + 1) Re-run script with a different '--tx-confs' parameter (now '{c}') + 2) Re-run script with the '--tx-fee' option (specify fee manually) + 3) Accept the global default fee of {f} BTC +Accept the global default fee of {f} BTC? +""".format(c=opt.tx_confs,f=opt.tx_fee).strip() + if keypress_confirm(m): + return None + else: + die(1,'Exiting at user request') + +# main(): execution begins here def get_fee_estimate(): if 'tx_fee' in opt.set_by_user: @@ -372,16 +388,15 @@ def get_tx_size_and_fee(inputs,outputs): cmd_args = opts.init(opts_data) -if opt.comment_file: - comment = get_tx_comment_from_file(opt.comment_file) +tx = MMGenTX() + +if opt.comment_file: tx.add_comment(opt.comment_file) c = bitcoin_connection() if not opt.info: do_license_msg(immed=True) - tx_out,change_addr = {},'' - addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext] cmd_args = set(cmd_args) - set(addrfiles) @@ -397,27 +412,27 @@ if not opt.info: if ',' in a: a1,a2 = split2(a,',') if is_btc_addr(a1): - btcaddr = a1 + btc_addr = a1 elif is_mmgen_addr(a1): - btcaddr = mmaddr2btcaddr(c,a1,ail_w,ail_f) + btc_addr = mmaddr2btcaddr(c,a1,ail_w,ail_f) else: die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a)) - ret = normalize_btc_amt(a2) - if ret: - tx_out[btcaddr] = ret + btc_amt = convert_to_btc_amt(a2) + if btc_amt: + tx.add_output(btc_addr,btc_amt) else: die(2,"%s: invalid amount in argument '%s'" % (a2,a)) elif is_mmgen_addr(a) or is_btc_addr(a): - if change_addr: + if tx.change_addr: die(2,'ERROR: More than one change address specified: %s, %s' % (change_addr, a)) - change_addr = a if is_btc_addr(a) else mmaddr2btcaddr(c,a,ail_w,ail_f) - tx_out[change_addr] = 0 + tx.change_addr = a if is_btc_addr(a) else mmaddr2btcaddr(c,a,ail_w,ail_f) + tx.add_output(tx.change_addr,Decimal('0')) else: die(2,'%s: unrecognized argument' % a) - if not tx_out: + if not tx.outputs: die(2,'At least one output must be specified on the command line') if opt.tx_fee > g.max_tx_fee: @@ -440,13 +455,14 @@ for o in us: del o['account'] unspent = sort_and_view(us) -total = trim_exponent(sum([i['amount'] for i in unspent])) +total = sum([i['amount'] for i in unspent]) -msg('Total unspent: %s BTC (%s outputs)' % (total, len(unspent))) +msg('Total unspent: %s BTC (%s outputs)' % (normalize_btc_amt(total), len(unspent))) if opt.info: sys.exit() -send_amt = sum([tx_out[i] for i in tx_out.keys()]) -msg('Total amount to spend: %s' % ('%s BTC'%send_amt,'Unknown')[bool(send_amt)]) +tx.send_amt = tx.sum_outputs() + +msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt,)[bool(tx.send_amt)]) while True: sel_nums = select_outputs(unspent, @@ -465,53 +481,42 @@ while True: if not keypress_confirm('Accept?'): continue - total_in = trim_exponent(sum([i['amount'] for i in sel_unspent])) - tx_size,tx_fee = get_tx_size_and_fee(sel_unspent,tx_out) - change = trim_exponent(total_in - (send_amt + tx_fee)) + tx.copy_inputs(sel_unspent) # makes tx.inputs - if change >= 0: - prompt = 'Transaction produces %s BTC in change. OK?' % change + tx.calculate_size_and_fee(fee_estimate) # sets tx.size, tx.fee + + change_amt = tx.sum_inputs() - tx.send_amt - tx.fee + + if change_amt >= 0: + prompt = 'Transaction produces %s BTC in change. OK?' % change_amt if keypress_confirm(prompt,default_yes=True): break else: - msg(wmsg['not_enough_btc'] % change) + msg(wmsg['not_enough_btc'] % change_amt) -if change > 0 and not change_addr: - die(2,wmsg['throwaway_change'] % change) - -if change_addr in tx_out and not change: +if change_amt > 0: + if not tx.change_addr: + die(2,wmsg['throwaway_change'] % change_amt) + tx.add_output(tx.change_addr,change_amt) +elif tx.change_addr: msg('Warning: Change address will be unused as transaction produces no change') - del tx_out[change_addr] + tx.del_output(tx.change_addr) -for k,v in tx_out.items(): tx_out[k] = float(v) +if not tx.send_amt: + tx.send_amt = change_amt -if change > 0: tx_out[change_addr] = float(change) +dmsg('tx: %s' % tx) -tx_in = [{'txid':i['txid'], 'vout':i['vout']} for i in sel_unspent] +tx.add_comment() # edits an existing comment +tx.create_raw(c) # creates tx.hex, tx.txid +tx.add_mmaddrs_to_outputs(ail_w,ail_f) +tx.add_timestamp() +tx.add_blockcount(c) -dmsg('tx_in: %s\ntx_out: %s' % (repr(tx_in),repr(tx_out))) - -if opt.comment_file: - if keypress_confirm('Edit comment?',False): - comment = get_tx_comment_from_user(comment) -else: - if keypress_confirm('Add a comment to transaction?',False): - comment = get_tx_comment_from_user() - else: comment = False - -tx_hex = c.createrawtransaction(tx_in,tx_out) qmsg('Transaction successfully created') -amt = send_amt or change -tx_id = make_chksum_6(unhexlify(tx_hex)).upper() -metadata = tx_id, amt, make_timestamp() +dmsg('TX (final): %s' % tx) -b2m_map = make_b2m_map(sel_unspent,tx_out,ail_w,ail_f) +tx.view_with_prompt('View decoded transaction?') -prompt_and_view_tx_data(c,'View decoded transaction?', - sel_unspent,tx_hex,b2m_map,comment,metadata) - -outfile = 'tx_%s[%s].%s' % (tx_id,amt,g.rawtx_ext) -data = make_tx_data('{} {} {}'.format(*metadata), - tx_hex,sel_unspent,b2m_map,comment) -write_data_to_file(outfile,data,'transaction',ask_write_default_yes=False) +tx.write_to_file(ask_write_default_yes=False) diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index ff151284..0837fb51 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -44,39 +44,32 @@ else: opts.usage() do_license_msg() -tx_data = get_lines_from_file(infile,'signed transaction data') +tx = MMGenTX() -metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,infile) - -qmsg("Signed transaction file '%s' is valid" % infile) +tx.parse_tx_file(infile,'signed transaction data') c = bitcoin_connection() -prompt_and_view_tx_data(c,'View transaction data?', - inputs_data,tx_hex,b2m_map,comment,metadata) +if not tx.check_signed(c): + die(1,'Transaction has no signature!') -if keypress_confirm('Edit transaction comment?'): - comment = get_tx_comment_from_user(comment) - data = make_tx_data('{} {} {}'.format(*metadata), tx_hex, - inputs_data, b2m_map, comment) - write_data_to_file(infile,data,'signed transaction with edited comment') +qmsg("Signed transaction file '%s' is valid" % infile) + +tx.view_with_prompt('View transaction data?') + +if tx.add_comment(): # edits an existing comment, returns true if changed + tx.write_to_file(ask_write_default_yes=True) warn = "Once this transaction is sent, there's no taking it back!" action = 'broadcast this transaction to the network' -expect = 'YES, I REALLY WANT TO DO THIS' +expect = 'YES, I REALLY WANT TO DO THIS' if opt.quiet: warn,expect = '','YES' -confirm_or_exit(warn, action, expect) +confirm_or_exit(warn,action,expect) msg('Sending transaction') -try: - tx_id = c.sendrawtransaction(tx_hex) -except: - die(3,'Unable to send transaction') +tx.send(c,bogus=True) -msg('Transaction sent: %s' % tx_id) - -of = 'tx_{}[{}].txid'.format(*metadata[:2]) -write_data_to_file(of, tx_id+'\n','transaction ID',ask_overwrite=True) +tx.write_txid_to_file() diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index a8ea64b9..ed5c5ee3 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -25,10 +25,11 @@ from mmgen.tx import * from mmgen.seed import SeedSource pnm = g.proj_name -pnl = pnm.lower() +# Unneeded, as MMGen transactions cannot contain non-MMGen inputs +# -w, --use-wallet-dat Get keys from a running bitcoind opts_data = { - 'desc': 'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnl), + 'desc': 'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnm.lower()), 'usage': '[opts] ... [seed source]...', 'options': """ -h, --help Print this help message. @@ -64,8 +65,7 @@ opts_data = { -I, --info Display information about the transaction and exit. -t, --terse-info Like '--info', but produce more concise output. -v, --verbose Produce more verbose output --w, --use-wallet-dat Get keys from a running bitcoind -""".format(g=g,pnm=pnm,pnl=pnl), +""".format(g=g,pnm=pnm,pnl=pnm.lower()), 'notes': """ Transactions with either {pnm} or non-{pnm} input addresses may be signed. @@ -97,7 +97,7 @@ FMT CODES: {f} """.format( f='\n '.join(SeedSource.format_fmt_codes().splitlines()), - g=g,pnm=pnm,pnl=pnl + g=g,pnm=pnm,pnl=pnm.lower() ) } @@ -114,7 +114,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file def get_seed_for_seed_id(seed_id,infiles,saved_seeds): - if seed_id in saved_seeds.keys(): + if seed_id in saved_seeds: return saved_seeds[seed_id] while True: @@ -131,8 +131,7 @@ def get_seed_for_seed_id(seed_id,infiles,saved_seeds): if ss.seed.sid == seed_id: return ss.seed.data - -def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds): +def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds): seed_ids = set([i[:8] for i in mmgen_addrs]) vmsg('Need seed%s: %s' % (suf(seed_ids,'k'),' '.join(seed_ids))) @@ -148,70 +147,31 @@ def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds): d += [('{}:{}'.format(seed_id,e.idx),e.addr,e.wif) for e in ai.addrdata] return d - -def sign_transaction(c,tx_hex,tx_num_str,sig_data,keys=None): - - if keys: - qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k'))) - dmsg('Keys:\n %s' % '\n '.join(keys)) - - msg_r('Signing transaction{}...'.format(tx_num_str)) -# from mmgen.rpc import exceptions - try: - sig_tx = c.signrawtransaction(tx_hex,sig_data,keys) -# except exceptions.InvalidAddressOrKey: - except: # TODO - die(3,'failed\nInvalid address or key') - - return sig_tx - - -def sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys): - - try: - sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys) - except: -# from mmgen.rpc import exceptions +# function unneeded, as MMGen transactions cannot contain non-MMGen inputs +def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys): + ok = tx.sign(c,tx_num_str,keys) # returns false on failure + if ok: + return ok + else: msg('Using keys in wallet.dat as per user request') prompt = 'Enter passphrase for bitcoind wallet: ' while True: passwd = get_bitcoind_passphrase(prompt) - - try: - c.walletpassphrase(passwd, 9999) -# except exceptions.WalletPassphraseIncorrect: - except: # TODO - msg('Passphrase incorrect (or some other error)') + ret = c.walletpassphrase(passwd, 9999,ret_on_error=True) + if rpc_error(ret): + if rpc_errmsg(ret,'unencrypted wallet, but walletpassphrase was called'): + msg('Wallet is unencrypted'); break else: msg('Passphrase OK'); break - sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys) + ok = tx.sign(c,tx_num_str,keys) msg('Locking wallet') - try: - c.walletlock() - except: + ret = c.walletlock(ret_on_error=True) + if rpc_error(ret): msg('Failed to lock wallet') - return sig_tx - - -def check_maps_from_seeds(maplist,desc,infiles,saved_seeds,return_keys=False): - - if not maplist: return [] - qmsg('Checking {pnm} -> BTC address mappings for {w}s (from seed(s))'.format( - pnm=pnm,w=desc)) - d = get_keys_for_mmgen_addrs(maplist.keys(),infiles,saved_seeds) -# 0=mmaddr 1=addr 2=wif - m = dict([(e[0],e[1]) for e in d]) - for a,b in zip(sorted(m),sorted(maplist)): - if a != b: - al,bl = 'generated seed:','tx file:' - die(3,wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,maplist[b])) - if return_keys: - ret = [e[2] for e in d] - vmsg('Added %s wif key%s from seeds' % (len(ret),suf(ret,'k'))) - return ret + return ok def missing_keys_errormsg(addrs): Msg(""" @@ -219,7 +179,6 @@ A key file must be supplied (or use the '--use-wallet-dat' option) for the following non-{pnm} address{suf}:\n {l}""".format( pnm=pnm, suf=suf(addrs,'a'), l='\n '.join(addrs)).strip()) - def parse_mmgen_keyaddr_file(): from mmgen.addr import AddrInfo ai = AddrInfo(opt.mmgen_keys_from_file,has_keys=True) @@ -229,21 +188,18 @@ def parse_mmgen_keyaddr_file(): return dict( [('%s:%s'%(ai.seed_id,e.idx), (e.addr,e.wif)) for e in ai.addrdata]) - -def parse_keylist(from_file): +def parse_keylist(key_data): fn = opt.keys_from_file from mmgen.crypto import mmgen_decrypt_file_maybe dec = mmgen_decrypt_file_maybe(fn,'non-{} keylist file'.format(pnm)) - keys_all = remove_comments(dec.splitlines()) # DOS-safe # Key list could be bitcoind dump, so remove first space and everything following - keys_all = [k.split()[0] for k in keys_all] - keys_all = set(keys_all) # Remove possible dups + keys_all = set([line.split()[0] for line in remove_comments(dec.splitlines())]) # DOS-safe dmsg(repr(keys_all)) - d = from_file['mmdata'] - kawifs = [d[k][1] for k in d.keys()] - keys = [k for k in keys_all if k not in kawifs] + ka_keys = [d[k][1] for k in key_data['kafile']] + keys = [k for k in keys_all if k not in ka_keys] removed = len(keys_all) - len(keys) - if removed: vmsg(wmsg['removed_dups'] % (removed,suf(removed,'k'))) + if removed: + vmsg(wmsg['removed_dups'] % (removed,suf(removed,'k'))) addrs = [] wif2addr_f = get_wif2addr_f() for n,k in enumerate(keys,1): @@ -253,41 +209,55 @@ def parse_keylist(from_file): return dict(zip(addrs,keys)) - # Check inputs and outputs maps against key-address file, deleting entries: -def check_maps_from_kafile(imap,desc,kadata,return_keys=False): +def check_maps_from_kafile(io_map,desc,kadata,return_keys=False): if not kadata: return [] qmsg('Checking {pnm} -> BTC address mappings for {w}s (from key-address file)'.format(pnm=pnm,w=desc)) ret = [] - for k in imap.keys(): - if k in kadata.keys(): - if kadata[k][0] == imap[k]: - del imap[k] + for k in io_map.keys(): + if k in kadata: + if kadata[k][0] == io_map[k]: + del io_map[k] ret += [kadata[k][1]] else: kl,il = 'key-address file:','tx file:' - die(2,wmsg['mm2btc_mapping_error']%(kl,k,kadata[k][0],il,k,imap[k])) + die(2,wmsg['mm2btc_mapping_error']%(kl,k,kadata[k][0],il,k,io_map[k])) if ret: vmsg('Removed %s address%s from %ss map' % (len(ret),suf(ret,'a'),desc)) if return_keys: vmsg('Added %s wif key%s from %ss map' % (len(ret),suf(ret,'k'),desc)) return ret +# Check inputs and outputs maps against values generated from seeds +def check_maps_from_seeds(io_map,desc,infiles,saved_seeds,return_keys=False): -def get_keys_from_keylist(kldata,other_addrs): + if not io_map: return [] + qmsg('Checking {pnm} -> BTC address mappings for {w}s (from seed(s))'.format( + pnm=pnm,w=desc)) + d = generate_keys_for_mmgen_addrs(io_map.keys(),infiles,saved_seeds) +# 0=mmaddr 1=addr 2=wif + m = dict([(e[0],e[1]) for e in d]) + for a,b in zip(sorted(m),sorted(io_map)): + if a != b: + al,bl = 'generated seed:','tx file:' + die(3,wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,io_map[b])) + if return_keys: + vmsg('Added %s wif key%s from seeds' % (len(d),suf(ret,'k'))) + return [e[2] for e in d] + +def get_keys_from_keylist(kldata,addrs): ret = [] - for addr in other_addrs[:]: - if addr in kldata.keys(): + for addr in addrs[:]: + if addr in kldata: ret += [kldata[addr]] - other_addrs.remove(addr) + addrs.remove(addr) vmsg('Added %s wif key%s from user-supplied keylist' % (len(ret),suf(ret,'k'))) return ret +# main(): execution begins here infiles = opts.init(opts_data,add_opts=['b16']) -#if opt.from_incog_hex or opt.from_incog_hidden: opt.from_incog = True - if not infiles: opts.usage() for i in infiles: check_infile(i) @@ -295,16 +265,21 @@ c = bitcoin_connection() saved_seeds = {} tx_files = [i for i in infiles if get_extension(i) == g.rawtx_ext] -seed_files = [i for i in infiles if get_extension(i) != g.rawtx_ext] +seed_files = [i for i in infiles if get_extension(i) in g.seedfile_exts] + +if not tx_files: + die(1,'You must specify a raw transaction file!') +if not (seed_files or opt.mmgen_keys_from_file or opt.keys_from_file or opt.use_wallet_dat): + die(1,'You must specify a seed or key source!') if not opt.info and not opt.terse_info: do_license_msg(immed=True) -from_file = { 'mmdata':{}, 'kldata':{} } +key_data = { 'kafile':{}, 'klfile':{} } if opt.mmgen_keys_from_file: - from_file['mmdata'] = parse_mmgen_keyaddr_file() or {} + key_data['kafile'] = parse_mmgen_keyaddr_file() or {} if opt.keys_from_file: - from_file['kldata'] = parse_keylist(from_file) or {} + key_data['klfile'] = parse_keylist(key_data) or {} tx_num_str = '' for tx_num,tx_file in enumerate(tx_files,1): @@ -312,72 +287,54 @@ for tx_num,tx_file in enumerate(tx_files,1): msg('\nTransaction #%s of %s:' % (tx_num,len(tx_files))) tx_num_str = ' #%s' % tx_num - m = ('transaction data','')[bool(opt.tx_id)] - tx_data = get_lines_from_file(tx_file,m) + tx = MMGenTX() + tx.parse_tx_file(tx_file,('transaction data','')[bool(opt.tx_id)]) + + if tx.check_signed(c): + die(1,'Transaction is already signed!') - metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,tx_file) vmsg("Successfully opened transaction file '%s'" % tx_file) - if opt.tx_id: die(0,metadata[0]) + if opt.tx_id: die(0,tx.txid) if opt.info or opt.terse_info: - view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pause=False, - terse=opt.terse_info) + tx.view(pause=False,terse=opt.terse_info) sys.exit() - prompt_and_view_tx_data(c,'View data for transaction{}?'.format(tx_num_str), - inputs_data,tx_hex,b2m_map,comment,metadata) + tx.view_with_prompt('View data for transaction%s?' % tx_num_str) # Start - other_addrs = list(set([i['address'] for i in inputs_data if not i['mmid']])) + other_addrs = list(set([i['address'] for i in tx.inputs if not i['mmid']])) - keys = get_keys_from_keylist(from_file['kldata'],other_addrs) + # should remove all elements from other_addrs + keys = get_keys_from_keylist(key_data['klfile'],other_addrs) if other_addrs and not opt.use_wallet_dat: missing_keys_errormsg(other_addrs) sys.exit(2) - imap = dict([(i['mmid'],i['address']) for i in inputs_data if i['mmid']]) - omap = dict([(j[0],i) for i,j in b2m_map.items()]) - sids = set([i[:8] for i in imap.keys()]) + imap = dict([(i['mmid'],i['address']) for i in tx.inputs if i['mmid']]) + omap = dict([(tx.outputs[k][1],k) for k in tx.outputs if len(tx.outputs[k]) > 1]) + sids = set([i[:8] for i in imap]) - keys += check_maps_from_kafile(imap,'input',from_file['mmdata'],True) - check_maps_from_kafile(omap,'output',from_file['mmdata']) + keys += check_maps_from_kafile(imap,'input',key_data['kafile'],True) + check_maps_from_kafile(omap,'output',key_data['kafile']) keys += check_maps_from_seeds(imap,'input',seed_files,saved_seeds,True) check_maps_from_seeds(omap,'output',seed_files,saved_seeds) - extra_sids = set(saved_seeds.keys()) - sids + extra_sids = set(saved_seeds) - sids if extra_sids: msg('Unused Seed ID%s: %s' % (suf(extra_sids,'k'),' '.join(extra_sids))) - # Begin signing - sig_data = [ - {'txid':i['txid'],'vout':i['vout'],'scriptPubKey':i['scriptPubKey']} - for i in inputs_data] +# if opt.use_wallet_dat: +# ok = sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys) +# else: + ok = tx.sign(c,tx_num_str,keys) - if opt.use_wallet_dat: - sig_tx = sign_tx_with_bitcoind_wallet( - c,tx_hex,tx_num_str,sig_data,keys) + if ok: + tx.add_comment() # edits an existing comment + tx.write_to_file(ask_write_default_yes=True) else: - sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys) - - if sig_tx['complete']: - msg('OK') - if keypress_confirm('Edit transaction comment?'): - comment = get_tx_comment_from_user(comment) - outfile = 'tx_%s[%s].%s' % (metadata[0],metadata[1],g.sigtx_ext) - data = make_tx_data( - '{} {} {t}'.format(*metadata[:2], - t=make_timestamp()), - sig_tx['hex'], inputs_data, b2m_map, comment - ) - write_data_to_file( - outfile,data, - 'signed transaction{}'.format(tx_num_str), - ask_write_prompt='Save signed transaction?' - ) - else: - msg_r('failed\nSome keys were missing. ') - die(3,'Transaction %scould not be signed.' % tx_num_str) + die(3,'failed\nSome keys were missing. Transaction %scould not be signed.' % tx_num_str) diff --git a/mmgen/obj.py b/mmgen/obj.py index 02faba1b..c2b29199 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -20,6 +20,7 @@ obj.py: The MMGenObject class and methods """ import mmgen.globalvars as g +from decimal import Decimal lvl = 0 @@ -38,16 +39,16 @@ class MMGenObject(object): def conv(v,col_w): vret = '' - if type(v) == str: + if type(v) in (str,unicode): if not (set(list(v)) <= set(list(g.printable))): vret = repr(v) else: vret = fix_linebreaks(v,fixed_indent=0) - elif type(v) == int or type(v) == long: + elif type(v) in (int,long,Decimal): vret = str(v) elif type(v) == dict: sep = '\n{}{}'.format(indent,' '*4) - cw = max(len(k) for k in v) + 2 + cw = (max(len(k) for k in v) if v else 0) + 2 t = sep.join(['{:<{w}}: {}'.format( repr(k), (fix_linebreaks(v[k],fixed_indent=0) if type(v[k]) == str else v[k]), diff --git a/mmgen/opts.py b/mmgen/opts.py index 389a93ae..ed7b628c 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -125,7 +125,7 @@ def init(opts_data,add_opts=[],opt_filter=None): for k in 'prog_name','desc','usage','options','notes': if k in opts_data: del opts_data[k] - # Transfer uopts into opt, setting required opts to None if not set by user + # Transfer uopts into opt, setting program's opts + required opts to None if not set by user for o in [s.rstrip('=') for s in long_opts] + \ g.required_opts + add_opts + skipped_opts: setattr(opt,o,uopts[o] if o in uopts else None) diff --git a/mmgen/rpc.py b/mmgen/rpc.py index 6d7f7311..108587a7 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -52,9 +52,12 @@ class BitcoinRPCConnection(object): # Normal mode: call with arg list unrolled, exactly as with 'bitcoin-cli' # Batch mode: call with list of arg lists as first argument # kwargs are for local use and are not passed to server + + # By default, dies with an error msg on all errors and exceptions + # With ret_on_error=True, returns 'rpcfail',(resp_object,(die_args)) def request(self,cmd,*args,**kwargs): - cf = { 'timeout': g.http_timeout, 'batch': False } + cf = { 'timeout':g.http_timeout, 'batch':False, 'ret_on_error':False } for k in cf: if k in kwargs and kwargs[k]: cf[k] = kwargs[k] @@ -66,6 +69,12 @@ class BitcoinRPCConnection(object): else: p = {'method':cmd,'params':args,'id':1} + def die_maybe(*args): + if cf['ret_on_error']: + return 'rpcfail',args + else: + die(*args[1:]) + dmsg('=== rpc.py debug ===') dmsg(' RPC POST data ==> %s\n' % p) @@ -75,34 +84,33 @@ class BitcoinRPCConnection(object): 'Authorization': 'Basic ' + base64.b64encode(self.auth_str) }) except Exception as e: - die(2,'%s\nUnable to connect to bitcoind' % e) + return die_maybe(None,2,'%s\nUnable to connect to bitcoind' % e) r = c.getresponse() # returns HTTPResponse instance - if r.status == 401: - m1 = 'RPC authentication error' - m2 = 'Check that rpcuser/rpcpassword in Bitcoin config file are correct' - m3 = '(or, alternatively, copy the authentication cookie to Bitcoin data dir' - m4 = 'if {} and Bitcoin are running as different users)'.format(g.proj_name) - die(1,'\n'.join((m1,m2,m3,m4))) - elif r.status != 200: - die(1,'RPC error: %s %s\n%s' % (r.status, r.reason, r.read())) + if r.status != 200: + e1 = r.read() + try: + e2 = json.loads(e1)['error']['message'] + except: + e2 = str(e1) + return die_maybe(r,1,e2) r2 = r.read() dmsg(' RPC REPLY data ==> %s\n' % r2) if not r2: - die(2,'Error: empty reply') + return die_maybe(r,2,'Error: empty reply') r3 = json.loads(r2.decode('utf8'), parse_float=decimal.Decimal) ret = [] for resp in r3 if cf['batch'] else [r3]: if 'error' in resp and resp['error'] != None: - die(1,'Bitcoind returned an error: %s' % resp['error']) + return die_maybe(r,1,'Bitcoind returned an error: %s' % resp['error']) elif 'result' not in resp: - die(1, 'Missing JSON-RPC result\n' + repr(resps)) + return die_maybe(r,1, 'Missing JSON-RPC result\n' + repr(resps)) else: ret.append(resp['result']) @@ -110,20 +118,28 @@ class BitcoinRPCConnection(object): rpcmethods = ( - 'estimatefee', - 'getinfo', - 'getbalance', - 'getaddressesbyaccount', - 'listunspent', - 'listaccounts', - 'importaddress', - 'decoderawtransaction', 'createrawtransaction', - 'signrawtransaction', + 'backupwallet', + 'decoderawtransaction', + 'estimatefee', + 'getaddressesbyaccount', + 'getbalance', + 'getblock', + 'getblockcount', + 'getblockhash', + 'getinfo', + 'importaddress', + 'listaccounts', + 'listunspent', 'sendrawtransaction', - 'walletpassphrase', - 'walletlock', + 'signrawtransaction', ) for name in rpcmethods: exec "def {n}(self,*a,**k):return self.request('{n}',*a,**k)\n".format(n=name) + +def rpc_error(ret): + return ret is list and ret and ret[0] == 'rpcfail' + +def rpc_errmsg(ret,e): + return (False,True)[ret[1][2].find(e) == -1] diff --git a/mmgen/tool.py b/mmgen/tool.py index adb753a1..c510343f 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -432,10 +432,10 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa for k in sorted(addrs,key=s_mmgen): if out and k.split('_')[0] != out[-1].split(':')[0]: out.append('') baddr = ' ' + addrs[k][2] if showbtcaddrs else '' - out.append(fs % (k.replace('_',':'), baddr, addrs[k][1], trim_exponent(addrs[k][0]))) + out.append(fs % (k.replace('_',':'), baddr, addrs[k][1], normalize_btc_amt(addrs[k][0]))) o = (fs + '\n%s\nTOTAL: %s BTC') % ( - 'ADDRESS','','COMMENT','BALANCE', '\n'.join(out), trim_exponent(total) + 'ADDRESS','','COMMENT','BALANCE', '\n'.join(out), normalize_btc_amt(total) ) if pager: do_pager(o) else: Msg(o) @@ -444,7 +444,9 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa def getbalance(minconf=1): accts = {} - for d in bitcoin_connection().listunspent(0): + us = bitcoin_connection().listunspent(0) +# pp_die(us) + for d in us: ma = split2(d['account'])[0] keys = ['TOTAL'] if d['spendable']: keys += ['SPENDABLE'] @@ -453,7 +455,7 @@ def getbalance(minconf=1): i = (1,2)[confs >= minconf] for key in keys: - if key not in accts: accts[key] = [0,0,0] + if key not in accts: accts[key] = [Decimal('0')] * 3 for j in ([],[0])[confs==0] + [i]: accts[key][j] += d['amount'] @@ -461,15 +463,14 @@ def getbalance(minconf=1): mc,lbl = str(minconf),'confirms' Msg(fs.format('Wallet','Unconfirmed','<%s %s'%(mc,lbl),'>=%s %s'%(mc,lbl))) for key in sorted(accts.keys()): - Msg(fs.format(key+':', *[str(trim_exponent(a))+' BTC' - for a in accts[key]])) + line = [str(normalize_btc_amt(a))+' BTC' for a in accts[key]] + Msg(fs.format(key+':', *line)) def txview(infile,pager=False,terse=False): c = bitcoin_connection() - tx_data = get_lines_from_file(infile,'transaction data') - - metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,infile) - view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager,pause=False,terse=terse) + tx = MMGenTX() + tx.parse_tx_file(infile,'transaction data') + tx.view(pager,pause=False,terse=terse) def add_label(mmaddr,label,remove=False): if not is_mmgen_addr(mmaddr): diff --git a/mmgen/tx.py b/mmgen/tx.py index d3627861..3cc97ee0 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -22,40 +22,61 @@ tx.py: Bitcoin transaction routines import sys, os from stat import * -from binascii import unhexlify +from binascii import hexlify,unhexlify from decimal import Decimal from collections import OrderedDict from mmgen.common import * from mmgen.term import do_pager -def trim_exponent(n): +def normalize_btc_amt(amt): '''Remove exponent and trailing zeros. ''' - d = Decimal(n) - return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize() + # to_integral() needed to keep ints > 9 from being shown in exp. notation + if is_btc_amt(amt): + return amt.quantize(Decimal(1)) if amt == amt.to_integral() else amt.normalize() + else: + die(2,'%s: not a BTC amount' % amt) -def normalize_btc_amt(amt): +def is_btc_amt(amt): + + if type(amt) is not Decimal: + msg('%s: not a decimal number' % amt) + return False + + if amt.as_tuple()[-1] < -g.btc_amt_decimal_places: + msg('%s: Too many decimal places in amount' % amt) + return False + + return True + +def convert_to_btc_amt(amt,return_on_fail=False): # amt must be a string! from decimal import Decimal try: ret = Decimal(amt) except: - msg('%s: Invalid BTC amount' % amt) - return False + m = '%s: amount cannot be converted to decimal' % amt + if return_on_fail: + msg(m); return False + else: + die(2,m) - dmsg('Decimal(amt): %s\nAs tuple: %s' % (amt,repr(ret.as_tuple()))) + dmsg('Decimal(amt): %s' % repr(amt)) - if ret.as_tuple()[-1] < -8: - msg('%s: Too many decimal places in amount' % amt) - return False + if ret.as_tuple()[-1] < -g.btc_amt_decimal_places: + m = '%s: Too many decimal places in amount' % amt + if return_on_fail: + msg(m); return False + else: + die(2,m) if ret == 0: - msg('Requested zero BTC amount') - return False + msg('WARNING: BTC amount is zero') + + return ret - return trim_exponent(ret) def parse_mmgen_label(s,check_label_len=False): l = split2(s) @@ -129,140 +150,6 @@ Only ASCII printable characters are permitted. """.strip() % (ch,label)) sys.exit(3) -def prompt_and_view_tx_data(c,prompt,inputs_data,tx_hex,adata,comment,metadata): - - prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view' - - reply = prompt_and_get_char(prompt,'YyNnVvTt',enter_ok=True) - - if reply and reply in 'YyVvTt': - view_tx_data(c,inputs_data,tx_hex,adata,comment,metadata, - pager=reply in 'Vv',terse=reply in 'Tt') - - -def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False,pause=True,terse=False): - - td = c.decoderawtransaction(tx_hex) - - fs = ( - 'TRANSACTION DATA\n\nHeader: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n', - 'Transaction {} - {} BTC - {} UTC\n' - )[bool(terse)] - - out = fs.format(*metadata) - - enl = ('\n','')[bool(terse)] - if comment: out += 'Comment: %s\n%s' % (comment,enl) - out += 'Inputs:\n' + enl - - nonmm_str = 'non-{pnm} address'.format(pnm=g.proj_name) - - total_in = 0 - for n,i in enumerate(td['vin']): - for j in inputs_data: - if j['txid'] == i['txid'] and j['vout'] == i['vout']: - days = int(j['confirmations'] * g.mins_per_block / (60*24)) - total_in += j['amount'] - if not j['mmid']: j['mmid'] = nonmm_str - mmid_fmt = ' ({:>{l}})'.format(j['mmid'],l=34-len(j['address'])) - if terse: - out += ' %s: %-54s %s BTC' % (n+1,j['address'] + mmid_fmt, - trim_exponent(j['amount'])) - else: - for d in ( - (n+1, 'tx,vout:', '%s,%s' % (i['txid'], i['vout'])), - ('', 'address:', j['address'] + mmid_fmt), - ('', 'comment:', j['comment']), - ('', 'amount:', '%s BTC' % trim_exponent(j['amount'])), - ('', 'confirmations:', '%s (around %s days)' % (j['confirmations'], days)) - ): - if d[2]: out += ('%3s %-8s %s\n' % d) - out += '\n' - - break - total_out = 0 - out += 'Outputs:\n' + enl - for n,i in enumerate(td['vout']): - btcaddr = i['scriptPubKey']['addresses'][0] - mmid,comment=b2m_map[btcaddr] if btcaddr in b2m_map else (nonmm_str,'') - mmid_fmt = ' ({:>{l}})'.format(mmid,l=34-len(j['address'])) - total_out += i['value'] - if terse: - out += ' %s: %-54s %s BTC' % (n+1,btcaddr + mmid_fmt, - trim_exponent(i['value'])) - else: - for d in ( - (n+1, 'address:', btcaddr + mmid_fmt), - ('', 'comment:', comment), - ('', 'amount:', trim_exponent(i['value'])) - ): - if d[2]: out += ('%3s %-8s %s\n' % d) - out += '\n' - - fs = ( - 'Total input: %s BTC\nTotal output: %s BTC\nTX fee: %s BTC\n', - 'In %s BTC - Out %s BTC - Fee %s BTC\n' - )[bool(terse)] - - out += fs % ( - trim_exponent(total_in), - trim_exponent(total_out), - trim_exponent(total_in-total_out) - ) - - o = out.encode('utf8') - if pager: do_pager(o) - else: - sys.stdout.write(o) - from mmgen.term import get_char - if pause: - get_char('Press any key to continue: ') - msg('') - - -def parse_tx_file(tx_data,infile): - - err_str,err_fmt = '','Invalid %s in transaction file' - - if len(tx_data) == 5: - metadata,tx_hex,inputs_data,outputs_data,comment = tx_data - elif len(tx_data) == 4: - metadata,tx_hex,inputs_data,outputs_data = tx_data - comment = '' - else: - err_str = 'number of lines' - - if not err_str: - if len(metadata.split()) != 3: - err_str = 'metadata' - else: - try: unhexlify(tx_hex) - except: err_str = 'hex data' - else: - try: inputs_data = eval(inputs_data) - except: err_str = 'inputs data' - else: - try: outputs_data = eval(outputs_data) - except: err_str = 'mmgen-to-btc address map data' - else: - if comment: - from mmgen.bitcoin import b58decode - comment = b58decode(comment) - if comment == False: - err_str = 'encoded comment (not base58)' - else: - if is_valid_tx_comment(comment): - comment = comment.decode('utf8') - else: - err_str = 'comment' - - if err_str: - msg(err_fmt % err_str) - sys.exit(2) - else: - return metadata.split(),tx_hex,inputs_data,outputs_data,comment - - def wiftoaddr_keyconv(wif): if wif[0] == '5': from subprocess import check_output @@ -276,30 +163,6 @@ def get_wif2addr_f(): return (wiftoaddr,wiftoaddr_keyconv)[bool(test_for_keyconv())] -def get_tx_comment_from_file(infile): - s = get_data_from_file(infile,'transaction comment') - if is_valid_tx_comment(s): - return s.decode('utf8').strip() - else: - sys.exit(2) - -def get_tx_comment_from_user(comment=''): - try: - while True: - s = my_raw_input('Comment: ',insert_txt=comment.encode('utf8')) - if s == '': return False - if is_valid_tx_comment(s): - return s.decode('utf8') - except KeyboardInterrupt: - msg('User interrupt') - return False - -def make_tx_data(metadata_fmt, tx_hex, inputs_data, b2m_map, comment): - from mmgen.bitcoin import b58encode - s = (b58encode(comment.encode('utf8')),) if comment else () - lines = (metadata_fmt, tx_hex, repr(inputs_data), repr(b2m_map)) + s - return '\n'.join(lines)+'\n' - def mmaddr2btcaddr_addrdata(mmaddr,addr_data,source=''): seed_id,idx = mmaddr.split(':') if seed_id in addr_data: @@ -309,3 +172,334 @@ def mmaddr2btcaddr_addrdata(mmaddr,addr_data,source=''): return addr_data[seed_id][idx] return '','' + +from mmgen.obj import * + +class MMGenTX(MMGenObject): + def __init__(self,filename=None): + self.inputs = [] + self.outputs = {} + self.change_addr = '' + self.size = 0 # size of raw serialized tx + self.fee = Decimal('0') + self.send_amt = Decimal('0') # total amt minus change + self.hex = '' # raw serialized hex transaction + self.comment = '' + self.txid = '' + self.btc_txid = '' + self.timestamp = '' + self.chksum = '' + self.fmt_data = '' + self.blockcount = 0 + self.isSigned = False + + def add_output(self,btcaddr,amt): + self.outputs[btcaddr] = (amt,) + + def del_output(self,btcaddr): + del self.outputs[btcaddr] + + def sum_outputs(self): + return sum([self.outputs[k][0] for k in self.outputs]) + + # returns true if comment added or changed + def add_comment(self,infile=None): + if infile: + s = get_data_from_file(infile,'transaction comment') + if is_valid_tx_comment(s): + self.comment = s.decode('utf8').strip() + return True + else: + sys.exit(2) + else: # get comment from user, or edit existing comment + m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.comment)] + if keypress_confirm(m,default_yes=False): + while True: + s = my_raw_input('Comment: ',insert_txt=self.comment.encode('utf8')) + if is_valid_tx_comment(s): + csave = self.comment + self.comment = s.decode('utf8').strip() + return (True,False)[csave == self.comment] + else: + msg('Invalid comment') + return False + + def edit_comment(self): + return self.add_comment(self) + + # https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending + def calculate_size_and_fee(self,fee_estimate): + self.size = len(self.inputs)*180 + len(self.outputs)*34 + 10 + if fee_estimate: + ftype,fee = 'Calculated','{:.8f}'.format(fee_estimate*opt.tx_fee_adj*self.size / 1024) + else: + ftype,fee = 'User-selected',opt.tx_fee + + ufee = None + if not keypress_confirm('{} TX fee: {} BTC. OK?'.format(ftype,fee),default_yes=True): + while True: + ufee = my_raw_input('Enter transaction fee: ') + if convert_to_btc_amt(ufee,return_on_fail=True): + if Decimal(ufee) > g.max_tx_fee: + msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,g.max_tx_fee)) + else: + fee = ufee + break + self.fee = convert_to_btc_amt(fee) + vmsg('Inputs:{} Outputs:{} TX size:{}'.format( + len(self.inputs),len(self.outputs),self.size)) + vmsg('Fee estimate: {} (1024 bytes, {} confs)'.format(fee_estimate,opt.tx_confs)) + m = ('',' (after %sx adjustment)' % opt.tx_fee_adj)[opt.tx_fee_adj != 1 and not ufee] + vmsg('TX fee: {}{}'.format(self.fee,m)) + + def copy_inputs(self,source): + copy_keys = 'txid','vout','amount','comment','mmid','address',\ + 'confirmations','scriptPubKey' + self.inputs = [dict([(k,d[k] if k in d else '') for k in copy_keys]) for d in source] + + def sum_inputs(self): + return sum([i['amount'] for i in self.inputs]) + + def create_raw(self,c): + inputs,outputs = [],{} + def dec2str(d): + tmp = {} + for k in d: + tmp[k] = str(d[k]) if type(d[k]) is Decimal else d[k] + return tmp + inputs = [dec2str(d) for d in self.inputs] + for k in self.outputs: + outputs[k] = str(self.outputs[k][0]) +# mdie(inputs,outputs) + self.hex = c.createrawtransaction(inputs,outputs) + self.txid = make_chksum_6(unhexlify(self.hex)).upper() + +# def make_b2m_map(self,ail_w,ail_f): +# d = dict([(d['address'], (d['mmid'],d['comment'])) +# for d in self.inputs if d['mmid']]) +# d = ail_w.make_reverse_dict(self.outputs.keys()) +# d.update(ail_f.make_reverse_dict(self.outputs.keys())) +# self.b2m_map = d + + def add_mmaddrs_to_outputs(self,ail_w,ail_f): + d = ail_w.make_reverse_dict(self.outputs.keys()) + d.update(ail_f.make_reverse_dict(self.outputs.keys())) + for k in self.outputs: + if k in d: + self.outputs[k] += d[k] + + def add_timestamp(self): + self.timestamp = make_timestamp() + + def add_blockcount(self,c): + self.blockcount = int(c.getblockcount()) + + def format(self): + from mmgen.bitcoin import b58encode + lines = ( + '{} {} {} {}'.format( + self.txid, + self.send_amt, + self.timestamp, + (self.blockcount or 'None') + ), + self.hex, + repr(self.inputs), + repr(self.outputs) + ) + ((b58encode(self.comment.encode('utf8')),) if self.comment else ()) + self.chksum = make_chksum_6(' '.join(lines)) + self.fmt_data = '\n'.join((self.chksum,) + lines)+'\n' + + # return true or false, don't exit + def sign(self,c,tx_num_str,keys=None): + + if keys: + qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k'))) + dmsg('Keys:\n %s' % '\n '.join(keys)) + + sig_data = [dict([(k,d[k]) for k in 'txid','vout','scriptPubKey']) for d in self.inputs] + dmsg('Sig data:\n%s' % pp_format(sig_data)) + dmsg('Raw hex:\n%s' % self.hex) + + msg_r('Signing transaction{}...'.format(tx_num_str)) + sig_tx = c.signrawtransaction(self.hex,sig_data,keys) + + if sig_tx['complete']: + msg('OK') + self.hex = sig_tx['hex'] + self.isSigned = True + return True + else: + msg('failed\nBitcoind returned the following errors:') + pp_msg(sig_tx['errors']) + return False + + def check_signed(self,c): + d = c.decoderawtransaction(self.hex) + self.isSigned = bool(d['vin'][0]['scriptSig']['hex']) + return self.isSigned + + def send(self,c,bogus=False): + if bogus: + self.btc_txid = 'deadbeef' * 8 + m = 'BOGUS transaction NOT sent: %s' + else: + self.btc_txid = c.sendrawtransaction(self.hex) # exits on failure? + m = 'Transaction sent: %s' + msg(m % self.btc_txid) + + def write_txid_to_file(self,ask_write=False,ask_write_default_yes=True): + fn = '%s[%s].%s' % (self.txid,self.send_amt,g.txid_ext) + write_data_to_file(fn,self.btc_txid+'\n','transaction ID', + ask_write=ask_write, + ask_write_default_yes=ask_write_default_yes) + + def write_to_file(self,ask_write=True,ask_write_default_yes=False): + if ask_write == False: + ask_write_default_yes=True + self.format() + fn = '%s[%s].%s' % (self.txid,self.send_amt, + (g.rawtx_ext,g.sigtx_ext)[self.isSigned]) + write_data_to_file(fn,self.fmt_data,'transaction', + ask_write=ask_write, + ask_write_default_yes=ask_write_default_yes) + + def view_with_prompt(self,prompt=''): + prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view' + reply = prompt_and_get_char(prompt,'YyNnVvTt',enter_ok=True) + if reply and reply in 'YyVvTt': + self.view(pager=reply in 'Vv',terse=reply in 'Tt') + + def view(self,pager=False,pause=True,terse=False): + o = self.format_view(terse=terse).encode('utf8') + if pager: do_pager(o) + else: + sys.stdout.write(o) + from mmgen.term import get_char + if pause: + get_char('Press any key to continue: ') + msg('') + + def format_view(self,terse=False): + try: + blockcount = bitcoin_connection().getblockcount() + except: + blockcount = None + + fs = ( + 'TRANSACTION DATA\n\nHeader: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n', + 'Transaction {} - {} BTC - {} UTC\n' + )[bool(terse)] + + out = fs.format(self.txid,self.send_amt,self.timestamp) + + enl = ('\n','')[bool(terse)] + if self.comment: + out += 'Comment: %s\n%s' % (self.comment,enl) + out += 'Inputs:\n' + enl + + nonmm_str = 'non-{pnm} address'.format(pnm=g.proj_name) + + for n,i in enumerate(self.inputs): + if blockcount: + confirmations = i['confirmations'] + blockcount - self.blockcount + days = int(confirmations * g.mins_per_block / (60*24)) + if not i['mmid']: + i['mmid'] = nonmm_str + mmid_fmt = ' ({:>{l}})'.format(i['mmid'],l=34-len(i['address'])) + if terse: + out += ' %s: %-54s %s BTC' % (n+1,i['address'] + mmid_fmt, + normalize_btc_amt(i['amount'])) + else: + for d in ( + (n+1, 'tx,vout:', '%s,%s' % (i['txid'], i['vout'])), + ('', 'address:', i['address'] + mmid_fmt), + ('', 'comment:', i['comment']), + ('', 'amount:', '%s BTC' % normalize_btc_amt(i['amount'])), + ('', 'confirmations:', '%s (around %s days)' % (confirmations,days) if blockcount else '') + ): + if d[2]: out += ('%3s %-8s %s\n' % d) + out += '\n' + + out += 'Outputs:\n' + enl + for n,k in enumerate(self.outputs): + btcaddr = k + v = self.outputs[k] + btc_amt,mmid,comment = (v[0],'Non-MMGen address','') if len(v) == 1 else v + mmid_fmt = ' ({:>{l}})'.format(mmid,l=34-len(btcaddr)) + if terse: + out += ' %s: %-54s %s BTC' % (n+1, btcaddr+mmid_fmt, normalize_btc_amt(btc_amt)) + else: + for d in ( + (n+1, 'address:', btcaddr + mmid_fmt), + ('', 'comment:', comment), + ('', 'amount:', '%s BTC' % normalize_btc_amt(btc_amt)) + ): + if d[2]: out += ('%3s %-8s %s\n' % d) + out += '\n' + + fs = ( + 'Total input: %s BTC\nTotal output: %s BTC\nTX fee: %s BTC\n', + 'In %s BTC - Out %s BTC - Fee %s BTC\n' + )[bool(terse)] + + total_in = self.sum_inputs() + total_out = self.sum_outputs() + out += fs % ( + normalize_btc_amt(total_in), + normalize_btc_amt(total_out), + normalize_btc_amt(total_in-total_out) + ) + + return out + + def parse_tx_file(self,infile,desc): + + self.parse_tx_data(get_lines_from_file(infile,desc)) + + def parse_tx_data(self,tx_data): + + err_str,err_fmt = '','Invalid %s in transaction file' + + if len(tx_data) == 6: + self.chksum,metadata,self.hex,inputs_data,outputs_data,comment = tx_data + elif len(tx_data) == 5: + self.chksum,metadata,self.hex,inputs_data,outputs_data = tx_data + comment = '' + else: + err_str = 'number of lines' + + if not err_str: + if self.chksum != make_chksum_6(' '.join(tx_data[1:])): + err_str = 'checksum' + elif len(metadata.split()) != 4: + err_str = 'metadata' + else: + self.txid,send_amt,self.timestamp,blockcount = metadata.split() + self.send_amt = Decimal(send_amt) + self.blockcount = int(blockcount) + try: unhexlify(self.hex) + except: err_str = 'hex data' + else: + try: self.inputs = eval(inputs_data) + except: err_str = 'inputs data' + else: + try: self.outputs = eval(outputs_data) + except: err_str = 'btc-to-mmgen address map data' + else: + if comment: + from mmgen.bitcoin import b58decode + comment = b58decode(comment) + if comment == False: + err_str = 'encoded comment (not base58)' + else: + if is_valid_tx_comment(comment): + self.comment = comment.decode('utf8') + else: + err_str = 'comment' + + if err_str: + msg(err_fmt % err_str) + sys.exit(2) + + diff --git a/mmgen/util.py b/mmgen/util.py index c6c33c11..d97d0051 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -176,12 +176,25 @@ def split_into_cols(col_wid,s): def capfirst(s): return s if len(s) == 0 else s[0].upper() + s[1:] -def make_timestamp(): - tv = time.gmtime(time.time())[:6] +def decode_timestamp(s): +# with open('/etc/timezone') as f: +# tz_save = f.read().rstrip() + os.environ['TZ'] = 'UTC' + ts = time.strptime(s,'%Y%m%d_%H%M%S') + t = time.mktime(ts) +# os.environ['TZ'] = tz_save + return int(t) + +def make_timestamp(secs=None): + t = int(secs) if secs else time.time() + tv = time.gmtime(t)[:6] return '{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}'.format(*tv) -def make_timestr(): - tv = time.gmtime(time.time())[:6] + +def make_timestr(secs=None): + t = int(secs) if secs else time.time() + tv = time.gmtime(t)[:6] return '{:04d}/{:02d}/{:02d} {:02d}:{:02d}:{:02d}'.format(*tv) + def secs_to_hms(secs): return '{:02d}:{:02d}:{:02d}'.format(secs/3600, (secs/60) % 60, secs % 60) @@ -791,3 +804,15 @@ def bitcoin_connection(): import mmgen.rpc return mmgen.rpc.BitcoinRPCConnection( host,port,cfg[user],cfg[passwd],auth_cookie=auth_cookie) + +def pp_format(d): + import pprint + return pprint.PrettyPrinter(indent=4).pformat(d) + +def pp_die(d): + import pprint + die(1,pprint.PrettyPrinter(indent=4).pformat(d)) + +def pp_msg(d): + import pprint + msg(pprint.PrettyPrinter(indent=4).pformat(d)) diff --git a/scripts/tx-old2new.py b/scripts/tx-old2new.py new file mode 100755 index 00000000..22a639d7 --- /dev/null +++ b/scripts/tx-old2new.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python + +import sys,os +repo_root = os.path.split(os.path.abspath(os.path.dirname(sys.argv[0])))[0] +sys.path = [repo_root] + sys.path + +from mmgen.common import * + +from mmgen.tool import * +from mmgen.tx import * +from mmgen.bitcoin import * +from mmgen.seed import * +from mmgen.term import do_pager + +help_data = { + 'desc': "Convert MMGen transaction file from old format to new format", + 'usage': "", + 'options': """ +-h, --help Print this help message +""" +} + +import mmgen.opts +cmd_args = opts.init(help_data) + +if len(cmd_args) != 1: opts.usage() + +def parse_tx_file(infile): + + err_str,err_fmt = '','Invalid %s in transaction file' + tx_data = get_lines_from_file(infile) + + if len(tx_data) == 5: + metadata,tx_hex,inputs_data,outputs_data,comment = tx_data + elif len(tx_data) == 4: + metadata,tx_hex,inputs_data,outputs_data = tx_data + comment = '' + else: + err_str = 'number of lines' + + if not err_str: + if len(metadata.split()) != 3: + err_str = 'metadata' + else: + try: unhexlify(tx_hex) + except: err_str = 'hex data' + else: + try: inputs_data = eval(inputs_data) + except: err_str = 'inputs data' + else: + try: outputs_data = eval(outputs_data) + except: err_str = 'btc-to-mmgen address map data' + else: + if comment: + from mmgen.bitcoin import b58decode + comment = b58decode(comment) + if comment == False: + err_str = 'encoded comment (not base58)' + else: + if is_valid_tx_comment(comment): + comment = comment.decode('utf8') + else: + err_str = 'comment' + + if err_str: + msg(err_fmt % err_str) + sys.exit(2) + else: + return metadata.split(),tx_hex,inputs_data,outputs_data,comment + +def find_block_by_time(c,timestamp): + secs = decode_timestamp(timestamp) + block_num = c.getblockcount() +# print 'secs:',secs, 'last block:',last_block + top,bot = block_num,0 + m = 'Searching for block' + msg_r(m) + for i in range(40): + msg_r('.') + bhash = c.getblockhash(block_num) + block = c.getblock(bhash) +# print 'block_num:',block_num, 'mediantime:',block['mediantime'], 'target:',secs + cur_secs = block['mediantime'] + if cur_secs > secs: + top = block_num + else: + bot = block_num + block_num = (top + bot) / 2 + if top - bot < 2: + msg('\nFound: %s ' % block_num) + break + + return block_num + +tx = MMGenTX() + +[tx.txid,send_amt,tx.timestamp],tx.hex,inputs,b2m_map,tx.comment = parse_tx_file(cmd_args[0]) +tx.send_amt = Decimal(send_amt) + +c = bitcoin_connection() + +tx.blockcount = find_block_by_time(c,tx.timestamp) + +dec_tx = c.decoderawtransaction(tx.hex) + +tx.copy_inputs(inputs) +tx.outputs = dict([(i['scriptPubKey']['addresses'][0],(i['value'],)) for i in dec_tx['vout']]) +for k in tx.outputs: + if k in b2m_map: + tx.outputs[k] += b2m_map[k] + +#tx.view_with_prompt('View decoded transaction?') +#tx.format() +#print(str(tx)) +# do_pager('OLD:\n' + pp_format(inputs) + '\nNEW:\n' + pp_format(tx.inputs)) +tx.write_to_file(ask_write=False) diff --git a/test/tooltest.py b/test/tooltest.py index 392d780d..a39706c3 100755 --- a/test/tooltest.py +++ b/test/tooltest.py @@ -103,7 +103,7 @@ cfg = { 'tmpdir': 'test/tmp10', 'tmpdir_num': 10, 'refdir': 'test/ref', - 'txfile': 'tx_FFB367[1.234].raw', + 'txfile': 'FFB367[1.234].rawtx', 'addrfile': '98831F3A[1,31-33,500-501,1010-1011].addrs', 'addrfile_chk': '6FEF 6FB9 7B13 5D91 854A 0BD3', }