123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- """
- mmgen-txcreate: Create a Bitcoin transaction from MMGen- or non-MMGen inputs
- to MMGen- or non-MMGen outputs
- """
- import sys
- from decimal import Decimal
- import mmgen.config as g
- from mmgen.Opts import *
- from mmgen.license import *
- from mmgen.tx import *
- help_data = {
- 'prog_name': g.prog_name,
- 'desc': "Create a BTC transaction with outputs to specified addresses",
- 'usage': "[opts] <addr,amt> ... [change addr] [addr file] ...",
- 'options': """
- -h, --help Print this help message
- -c, --comment-file= f Source the transaction's comment from file 'f'
- -d, --outdir= d Specify an alternate directory 'd' for output
- -e, --echo-passphrase Print passphrase to screen when typing it
- -f, --tx-fee= f Transaction fee (default: {g.tx_fee} BTC)
- -i, --info Display unspent outputs and exit
- -q, --quiet Suppress warnings; overwrite files without
- prompting
- -v, --verbose Produce more verbose output
- """.format(g=g),
- 'notes': """
- Transaction inputs are chosen from a list of the user's unpent outputs
- via an interactive menu.
- Ages of transactions are approximate based on an average block creation
- interval of {g.mins_per_block} minutes.
- Addresses on the command line can be Bitcoin addresses or {pnm} addresses
- of the form <seed ID>:<number>.
- To send all inputs (minus TX fee) to a single output, specify one address
- with no amount on the command line.
- """.format(g=g,pnm=g.proj_name)
- }
- wmsg = {
- 'too_many_acct_addresses': """
- ERROR: More than one address found for account: "%s".
- Your "wallet.dat" file appears to have been altered by a non-{pnm} program.
- Please restore your tracking wallet from a backup or create a new one and
- re-import your addresses.
- """.strip().format(pnm=g.proj_name),
- 'addr_in_addrfile_only': """
- Warning: output address {mmgenaddr} is not in the tracking wallet, which means
- its balance will not be tracked. You're strongly advised to import the address
- into your tracking wallet before broadcasting this transaction.
- """.strip(),
- 'addr_not_found': """
- No data for MMgen address {mmgenaddr} could be found in either the tracking
- wallet or the supplied address file. Please import this address into your
- tracking wallet, or supply an address file for it on the command line.
- """.strip(),
- 'addr_not_found_no_addrfile': """
- No data for MMgen address {mmgenaddr} could be found in the tracking wallet.
- Please import this address into your tracking wallet or supply an address file
- for it on the command line.
- """.strip(),
- 'no_spendable_outputs': """
- No spendable outputs found! Import addresses with balances into your
- watch-only wallet using '{pnm}-addrimport' and then re-run this program.
- """.strip().format(pnm=g.proj_name.lower()),
- 'mixed_inputs': """
- NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs, which
- makes the signing process more complicated. When signing the transaction, keys
- for the non-{pnm} inputs must be supplied to '{pnl}-txsign' in a file with the
- '--keys-from-file' option.
- Selected mmgen inputs: %s
- """.strip().format(pnm=g.proj_name,pnl=g.proj_name.lower()),
- 'not_enough_btc': """
- Not enough BTC in the inputs for this transaction (%s BTC)
- """.strip(),
- 'throwaway_change': """
- ERROR: This transaction produces change (%s BTC); however, no change address
- was specified.
- """.strip(),
- }
- def format_unspent_outputs_for_printing(out,sort_info,total):
- pfs = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s"
- pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID",
- "Amount (BTC)","Conf.","Age (days)", "Comment")]
- for n,i in enumerate(out):
- addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address
- tx = " " * 63 + "=" \
- if i.skip == "txid" and "grouped" in sort_info else str(i.txid)
- s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr,
- i.mmid,i.amt,i.confirmations,i.days,i.comment)
- pout.append(s.rstrip())
- return \
- "Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n".format(
- make_timestr(), " ".join(sort_info), "\n".join(pout), total
- )
- def sort_and_view(unspent,opts):
- def s_amt(i): return i.amount
- def s_txid(i): return "%s %03s" % (i.txid,i.vout)
- def s_addr(i): return i.address
- def s_age(i): return i.confirmations
- def s_mmgen(i):
- if i.mmid:
- return "{}:{:>0{w}}".format(
- *i.mmid.split(":"), w=g.mmgen_idx_max_digits)
- else: return "G" + i.comment
- sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True
- unspent.sort(key=s_age,reverse=reverse)
- total = trim_exponent(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"
- options_msg = """
- Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
- Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
- """.strip()
- prompt = \
- "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
- mmid_w = max(len(i.mmid) for i in unspent)
- from copy import deepcopy
- from mmgen.term import get_terminal_size
- write_to_file_msg = ""
- msg("")
- while True:
- cols = get_terminal_size()[0]
- if cols < g.min_screen_width:
- msg("%s-txcreate requires a screen at least %s characters wide" %
- (g.proj_name.lower(),g.min_screen_width))
- sys.exit(2)
- addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46)
- acct_w = min(max_acct_len, max(24,int(addr_w-10)))
- btaddr_w = addr_w - acct_w - 1
- tx_w = max(11,min(64, cols-addr_w-32))
- txdots = "..." if tx_w < 64 else ""
- fs = " %-4s %-" + str(tx_w) + "s %-2s %-" + str(addr_w) + "s %-13s %-s"
- table_hdr = fs % ("Num","TX id Vout","","Address","Amount (BTC)",
- "Age(d)" if show_days else "Conf.")
- unsp = deepcopy(unspent)
- for i in unsp: i.skip = ""
- if group and (sort == "address" or sort == "txid"):
- for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
- if sort == "address" and a.address == b.address: b.skip = "addr"
- elif sort == "txid" and a.txid == b.txid: b.skip = "txid"
- for i in unsp:
- amt = str(trim_exponent(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))
- i.age = i.days if show_days else i.confirmations
- if i.skip == "addr":
- i.addr = "|" + "." * 33
- else:
- if show_mmaddr:
- dots = ".." if btaddr_w < len(i.address) else ""
- i.addr = "%s%s %s" % (
- i.address[:btaddr_w-len(dots)],
- dots, (
- ("{:<{w}} ".format(i.mmid,w=mmid_w) if i.mmid else "")
- + i.comment)[:acct_w]
- )
- else:
- i.addr = i.address
- i.tx = " " * (tx_w-4) + "|..." if i.skip == "txid" \
- else i.txid[:tx_w-len(txdots)]+txdots
- sort_info = ["reverse"] if reverse else []
- sort_info.append(sort if sort else "unsorted")
- if group and (sort == "address" or sort == "txid"):
- sort_info.append("grouped")
- out = [hdr_fmt % (" ".join(sort_info), total), table_hdr]
- out += [fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,i.age)
- for n,i in enumerate(unsp)]
- msg("\n".join(out) +"\n\n" + write_to_file_msg + options_msg)
- write_to_file_msg = ""
- skip_prompt = False
- while True:
- reply = get_char(prompt, immed_chars="atDdAMrgmeqpvw")
- if reply == 'a': unspent.sort(key=s_amt); sort = "amount"
- elif reply == 't': unspent.sort(key=s_txid); sort = "txid"
- elif reply == 'D': show_days = not show_days
- elif reply == 'd': unspent.sort(key=s_addr); sort = "address"
- elif reply == 'A': unspent.sort(key=s_age); sort = "age"
- elif reply == 'M':
- unspent.sort(key=s_mmgen); sort = "mmgen"
- show_mmaddr = True
- elif reply == 'r':
- unspent.reverse()
- reverse = not reverse
- elif reply == 'g': group = not group
- elif reply == 'm': show_mmaddr = not show_mmaddr
- elif reply == 'e': pass
- elif reply == 'q': pass
- elif reply == 'p':
- d = format_unspent_outputs_for_printing(unsp,sort_info,total)
- of = "listunspent[%s].out" % ",".join(sort_info)
- write_to_file(of, d, opts,"",False,False)
- write_to_file_msg = "Data written to '%s'\n\n" % of
- elif reply == 'v':
- do_pager("\n".join(out))
- continue
- elif reply == 'w':
- data = format_unspent_outputs_for_printing(unsp,sort_info,total)
- do_pager(data)
- continue
- else:
- msg("\nInvalid input")
- continue
- break
- msg("\n")
- if reply == 'q': break
- return tuple(unspent)
- def select_outputs(unspent,prompt):
- while True:
- reply = my_raw_input(prompt).strip()
- if not reply: continue
- selected = parse_addr_idxs(reply,sep=None)
- if not selected: continue
- if selected[-1] > len(unspent):
- msg("Inputs must be less than %s" % len(unspent))
- continue
- return selected
- def get_acct_data_from_wallet(c,acct_data):
-
- vmsg_r("Getting account data from wallet...")
- accts,i = c.listaccounts(minconf=0,includeWatchonly=True),0
- for acct in accts:
- ma,comment = parse_mmgen_label(acct)
- if ma:
- i += 1
- addrlist = c.getaddressesbyaccount(acct)
- if len(addrlist) != 1:
- msg(wmsg['too_many_acct_addresses'] % acct)
- sys.exit(2)
- seed_id,idx = ma.split(":")
- if seed_id not in acct_data:
- acct_data[seed_id] = {}
- acct_data[seed_id][idx] = (addrlist[0],comment)
- vmsg("%s %s addresses found, %s accounts total" % (i,g.proj_name,len(accts)))
- def mmaddr2btcaddr_unspent(unspent,mmaddr):
- vmsg_r("Searching for {g.proj_name} address {m} in wallet...".format(g=g,m=mmaddr))
- m = [u for u in unspent if u.mmid == mmaddr]
- if len(m) == 0:
- vmsg("not found")
- return "",""
- elif len(m) > 1:
- msg(wmsg['too_many_acct_addresses'] % acct); sys.exit(2)
- else:
- vmsg("success (%s)" % m[0].address)
- return m[0].address, m[0].comment
- sys.exit()
- def mmaddr2btcaddr(c,mmaddr,acct_data,ail):
-
- if not acct_data: get_acct_data_from_wallet(c,acct_data)
- btcaddr = mmaddr2btcaddr_addrdata(mmaddr,acct_data,"wallet")[0]
- if not btcaddr:
- if ail:
- sid,idx = mmaddr.split(":")
- btcaddr = ail.addrinfo(sid).btcaddr(int(idx))
- if btcaddr:
- msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr))
- if not keypress_confirm("Continue anyway?"):
- sys.exit(1)
- else:
- msg(wmsg['addr_not_found'].format(mmgenaddr=mmaddr))
- sys.exit(2)
- else:
- msg(wmsg['addr_not_found_no_addrfile'].format(mmgenaddr=mmaddr))
- sys.exit(2)
- return btcaddr
- opts,cmd_args = parse_opts(sys.argv,help_data)
- if g.debug: show_opts_and_cmd_args(opts,cmd_args)
- if 'comment_file' in opts:
- comment = get_tx_comment_from_file(opts['comment_file'])
- c = connect_to_bitcoind()
- if not 'info' in opts:
- do_license_msg(immed=True)
- tx_out,acct_data,change_addr = {},{},""
- from mmgen.addr import AddrInfo,AddrInfoList
- ail = AddrInfoList()
- addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext]
- cmd_args = set(cmd_args) - set(addrfiles)
- for a in addrfiles:
- check_infile(a)
- ail.add(AddrInfo(a))
- for a in cmd_args:
- if "," in a:
- a1,a2 = split2(a,",")
- if is_btc_addr(a1):
- btcaddr = a1
- elif is_mmgen_addr(a1):
- btcaddr = mmaddr2btcaddr(c,a1,acct_data,ail)
- else:
- msg("%s: unrecognized subargument in argument '%s'" % (a1,a))
- sys.exit(2)
- ret = normalize_btc_amt(a2)
- if ret:
- tx_out[btcaddr] = ret
- else:
- msg("%s: invalid amount in argument '%s'" % (a2,a))
- sys.exit(2)
- elif is_mmgen_addr(a) or is_btc_addr(a):
- if change_addr:
- msg("ERROR: More than one change address specified: %s, %s" %
- (change_addr, a))
- sys.exit(2)
- change_addr = a if is_btc_addr(a) else \
- mmaddr2btcaddr(c,a,acct_data,ail)
- tx_out[change_addr] = 0
- else:
- msg("%s: unrecognized argument" % a)
- sys.exit(2)
- if not tx_out:
- msg("At least one output must be specified on the command line")
- sys.exit(2)
- tx_fee = opts['tx_fee'] if 'tx_fee' in opts else g.tx_fee
- tx_fee = normalize_btc_amt(tx_fee)
- if tx_fee > g.max_tx_fee:
- msg("Transaction fee too large: %s > %s" % (tx_fee,g.max_tx_fee))
- sys.exit(2)
- if g.debug: show_opts_and_cmd_args(opts,cmd_args)
- if g.bogus_wallet_data:
- us = eval(get_data_from_file(g.bogus_wallet_data))
- else:
- us = c.listunspent()
- if not us: msg(wmsg['no_spendable_outputs']); sys.exit(2)
- for o in us:
- o.mmid,o.comment = parse_mmgen_label(o.account)
- del o.account
- unspent = sort_and_view(us,opts)
- total = trim_exponent(sum([i.amount for i in unspent]))
- msg("Total unspent: %s BTC (%s outputs)" % (total, len(unspent)))
- if 'info' in opts: sys.exit(0)
- send_amt = sum([tx_out[i] for i in tx_out.keys()])
- msg("Total amount to spend: %s%s" % (
- (send_amt or "Unknown")," BTC" if send_amt else ""))
- while True:
- sel_nums = select_outputs(unspent,
- "Enter a range or space-separated list of outputs to spend: ")
- msg("Selected output%s: %s" %
- (("" if len(sel_nums) == 1 else "s"), " ".join(str(i) for i in sel_nums))
- )
- sel_unspent = [unspent[i-1] for i in sel_nums]
- mmaddrs = set([i.mmid for i in sel_unspent])
- mmaddrs.discard("")
- if mmaddrs and len(mmaddrs) < len(sel_unspent):
- msg(wmsg['mixed_inputs'] % ", ".join(sorted(mmaddrs)))
- if not keypress_confirm("Accept?"):
- continue
- total_in = trim_exponent(sum([i.amount for i in sel_unspent]))
- change = trim_exponent(total_in - (send_amt + tx_fee))
- if change >= 0:
- prompt = "Transaction produces %s BTC in change. OK?" % change
- if keypress_confirm(prompt,default_yes=True):
- break
- else:
- msg(wmsg['not_enough_btc'] % change)
- if change > 0 and not change_addr:
- msg(wmsg['throwaway_change'] % change)
- sys.exit(2)
- if change_addr in tx_out and not change:
- msg("Warning: Change address will be unused as transaction produces no change")
- del tx_out[change_addr]
- for k,v in tx_out.items(): tx_out[k] = float(v)
- if change > 0: tx_out[change_addr] = float(change)
- tx_in = [{"txid":i.txid, "vout":i.vout} for i in sel_unspent]
- if g.debug:
- print "tx_in:", repr(tx_in)
- print "tx_out:", repr(tx_out)
- if 'comment_file' in opts:
- 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()
- sel_unspent = [i.__dict__ for i in sel_unspent]
- def make_b2m_map(inputs_data,tx_out):
- m = [(d['address'],(d['mmid'],d['comment'])) for d in inputs_data if d['mmid']]
- d = ail.make_reverse_dict(tx_out.keys())
- d.update(m)
- return d
- b2m_map = make_b2m_map(sel_unspent,tx_out)
- prompt_and_view_tx_data(c,"View decoded transaction?",
- sel_unspent,tx_hex,b2m_map,comment,metadata)
- if keypress_confirm("Save transaction?",default_yes=False):
- 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_to_file(outfile,data,opts,"transaction",False,True)
- else:
- msg("Transaction not saved")
|