New object-oriented TX code

This commit is contained in:
The MMGen Project 2016-07-04 21:47:32 +03:00
commit 3f2a866c47
12 changed files with 745 additions and 434 deletions

View file

@ -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'

View file

@ -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)

View file

@ -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()

View file

@ -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] <transaction file>... [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)

View file

@ -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]),

View file

@ -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)

View file

@ -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]

View file

@ -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):

View file

@ -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)

View file

@ -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))

116
scripts/tx-old2new.py Executable file
View file

@ -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': "<tx file>",
'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)

View file

@ -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',
}