From 207632cb40882a16534841faf70ea08093c54e0a Mon Sep 17 00:00:00 2001 From: philemon Date: Mon, 13 Nov 2017 22:42:50 +0300 Subject: [PATCH] txcreate(tx) -> tx.create() (delete txcreate.py) --- mmgen/main_txbump.py | 2 +- mmgen/main_txcreate.py | 7 +- mmgen/main_txdo.py | 5 +- mmgen/obj.py | 7 +- mmgen/protocol.py | 3 +- mmgen/tx.py | 213 +++++++++++++++++++++++++++++++++++- mmgen/txcreate.py | 242 ----------------------------------------- mmgen/util.py | 2 +- 8 files changed, 226 insertions(+), 255 deletions(-) delete mode 100755 mmgen/txcreate.py diff --git a/mmgen/main_txbump.py b/mmgen/main_txbump.py index 6b04b976..61b5ae1f 100755 --- a/mmgen/main_txbump.py +++ b/mmgen/main_txbump.py @@ -80,7 +80,7 @@ rpc_init() tx_file = cmd_args.pop(0) check_infile(tx_file) -from mmgen.txcreate import * +from mmgen.tx import * from mmgen.txsign import * seed_files = get_seed_files(opt,cmd_args) if (cmd_args or opt.send) else None diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index a94479ae..062f8055 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -51,6 +51,9 @@ opts_data = lambda: { cmd_args = opts.init(opts_data) -from mmgen.txcreate import * -tx = txcreate(cmd_args,do_info=opt.info) +rpc_init() + +from mmgen.tx import MMGenTX +tx = MMGenTX() +tx.create(cmd_args,do_info=opt.info) tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False) diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index 07b94316..51b57e5e 100755 --- a/mmgen/main_txdo.py +++ b/mmgen/main_txdo.py @@ -79,7 +79,7 @@ cmd_args = opts.init(opts_data) rpc_init() -from mmgen.txcreate import * +from mmgen.tx import * from mmgen.txsign import * seed_files = get_seed_files(opt,cmd_args) @@ -88,7 +88,8 @@ kal = get_keyaddrlist(opt) kl = get_keylist(opt) if kl and kal: kl.remove_dup_keys(kal) -tx = txcreate(cmd_args,caller='txdo') +tx = MMGenTX(caller='txdo') +tx.create(cmd_args) txsign(tx,seed_files,kl,kal) tx.write_to_file(ask_write=False) diff --git a/mmgen/obj.py b/mmgen/obj.py index 8c15dcec..17e578f6 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -349,10 +349,9 @@ class BTCAmt(Decimal,Hilite,InitErrors): def __neg__(self,other,context=None): return type(self)(Decimal.__neg__(self,other,context)) -class BCHAmt(BTCAmt): - pass -class LTCAmt(BTCAmt): - max_amt = 84000000 +class BCHAmt(BTCAmt): pass +class B2XAmt(BTCAmt): pass +class LTCAmt(BTCAmt): max_amt = 84000000 class CoinAddr(str,Hilite,InitErrors,MMGenObject): color = 'cyan' diff --git a/mmgen/protocol.py b/mmgen/protocol.py index cfcf0e0a..f325dee5 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -70,7 +70,8 @@ class BitcoinProtocol(MMGenObject): sighash_type = 'ALL' block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' forks = [ - (478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','bch') + (478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','bch'), + (None,'','b2x') ] caps = ('rbf','segwit') base_coin = 'BTC' diff --git a/mmgen/tx.py b/mmgen/tx.py index 3ea7af6b..90dd2097 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -26,6 +26,69 @@ from binascii import unhexlify from mmgen.common import * from mmgen.obj import * +pnm = g.proj_name + +wmsg = { + '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 {pnm} 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 {pnm} 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(), + 'non_mmgen_inputs': """ +NOTE: This transaction includes non-{pnm} inputs, which makes the signing +process more complicated. When signing the transaction, keys for non-{pnm} +inputs must be supplied to '{pnl}-txsign' in a file with the '--keys-from-file' +option. +Selected non-{pnm} inputs: {{}} +""".strip().format(pnm=pnm,pnl=pnm.lower()), + 'not_enough_coin': """ +Selected outputs insufficient to fund this transaction ({{}} {} needed) +""".strip().format(g.coin), + 'no_change_output': """ +ERROR: No change address specified. If you wish to create a transaction with +only one output, specify a single output address with no {} amount +""".strip().format(g.coin), +} + +def select_unspent(unspent,prompt): + while True: + reply = my_raw_input(prompt).strip() + if reply: + selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return') + if selected: + if selected[-1] <= len(unspent): + return selected + msg('Unspent output number must be <= %s' % len(unspent)) + +def mmaddr2coinaddr(mmaddr,ad_w,ad_f): + + # assume mmaddr has already been checked + coin_addr = ad_w.mmaddr2coinaddr(mmaddr) + + if not coin_addr: + if ad_f: + coin_addr = ad_f.mmaddr2coinaddr(mmaddr) + if coin_addr: + msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr)) + if not keypress_confirm('Continue anyway?'): + sys.exit(1) + else: + die(2,wmsg['addr_not_found'].format(pnm=pnm,mmgenaddr=mmaddr)) + else: + die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr)) + + return CoinAddr(coin_addr) + def segwit_is_active(exit_on_error=False): d = g.rpch.getblockchaininfo() if d['chain'] == 'regtest': @@ -184,7 +247,7 @@ class MMGenTX(MMGenObject): desc = 'transaction outputs' member_type = 'MMGenTxOutput' - def __init__(self,filename=None,md_only=False): + def __init__(self,filename=None,md_only=False,caller=None): self.inputs = self.MMGenTxInputList() self.outputs = self.MMGenTxOutputList() self.send_amt = g.proto.coin_amt('0') # total amt minus change @@ -198,6 +261,7 @@ class MMGenTX(MMGenObject): self.blockcount = 0 self.chain = None self.coin = None + self.caller = caller if filename: self.parse_tx_file(filename,md_only=md_only) @@ -876,7 +940,7 @@ class MMGenTX(MMGenObject): self.blockcount = int(blockcount) desc = 'transaction hex data' self.hex = HexStr(self.hex,on_fail='raise') - if md_only: return # the following ops will all fail if g.coin doesn't match tx.coin + if md_only: return # the following ops will all fail if g.coin doesn't match self.coin desc = 'coin type in metadata' assert self.coin == g.coin,'invalid coin type' desc = 'inputs data' @@ -891,6 +955,151 @@ class MMGenTX(MMGenObject): if not self.chain and not self.inputs[0].addr.is_for_chain('testnet'): self.chain = 'mainnet' + def get_fee_from_estimate_or_user(self,estimate_fail_msg_shown=[]): + + if opt.tx_fee: + desc = 'User-selected' + start_fee = opt.tx_fee + else: + desc = 'Network-estimated' + ret = g.rpch.estimatefee(opt.tx_confs) + if ret == -1: + if not estimate_fail_msg_shown: + msg('Network fee estimation for {} confirmations failed'.format(opt.tx_confs)) + estimate_fail_msg_shown.append(True) + start_fee = None + else: + start_fee = g.proto.coin_amt(ret) * opt.tx_fee_adj * self.estimate_size() / 1024 + if opt.verbose: + msg('{} fee ({} confs): {} {}/kB'.format(desc,opt.tx_confs,ret,g.coin)) + msg('TX size (estimated): {}'.format(self.estimate_size())) + + return self.get_usr_fee_interactive(start_fee,desc=desc) + + def get_outputs_from_cmdline(self,cmd_args): + from mmgen.addr import AddrList,AddrData + addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext] + cmd_args = set(cmd_args) - set(addrfiles) + + ad_f = AddrData() + for a in addrfiles: + check_infile(a) + ad_f.add(AddrList(a)) + + ad_w = AddrData(source='tw') + + for a in cmd_args: + if ',' in a: + a1,a2 = a.split(',',1) + if is_mmgen_id(a1) or is_coin_addr(a1): + coin_addr = mmaddr2coinaddr(a1,ad_w,ad_f) if is_mmgen_id(a1) else CoinAddr(a1) + self.add_output(coin_addr,g.proto.coin_amt(a2)) + else: + die(2,"%s: invalid subargument in command-line argument '%s'" % (a1,a)) + elif is_mmgen_id(a) or is_coin_addr(a): + if self.get_chg_output_idx() != None: + die(2,'ERROR: More than one change address listed on command line') + coin_addr = mmaddr2coinaddr(a,ad_w,ad_f) if is_mmgen_id(a) else CoinAddr(a) + self.add_output(coin_addr,g.proto.coin_amt('0'),is_chg=True) + else: + die(2,'{}: invalid command-line argument'.format(a)) + + if not self.outputs: + die(2,'At least one output must be specified on the command line') + + if self.get_chg_output_idx() == None: + die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(self.outputs) == 1]) + + self.add_mmaddrs_to_outputs(ad_w,ad_f) + self.check_dup_addrs('outputs') + + if not segwit_is_active() and self.has_segwit_outputs(): + fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain' + rdie(2,fs.format(g.proj_name)) + + def get_inputs_from_user(self,tw): + + while True: + m = 'Enter a range or space-separated list of outputs to spend: ' + sel_nums = select_unspent(tw.unspent,m) + msg('Selected output%s: %s' % (suf(sel_nums,'s'),' '.join(str(i) for i in sel_nums))) + + sel_unspent = tw.MMGenTwOutputList([tw.unspent[i-1] for i in sel_nums]) + + t_inputs = sum(s.amt for s in sel_unspent) + if t_inputs < self.send_amt: + msg(wmsg['not_enough_coin'].format(self.send_amt-t_inputs)) + continue + + non_mmaddrs = [i for i in sel_unspent if i.twmmid.type == 'non-mmgen'] + if non_mmaddrs and self.caller != 'txdo': + msg(wmsg['non_mmgen_inputs'].format(', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs]))))) + if not keypress_confirm('Accept?'): + continue + + self.copy_inputs_from_tw(sel_unspent) # makes self.inputs + + change_amt = self.sum_inputs() - self.send_amt - self.get_fee_from_estimate_or_user() + + if change_amt >= 0: + p = 'Transaction produces {} {} in change'.format(change_amt.hl(),g.coin) + if opt.yes or keypress_confirm(p+'. OK?',default_yes=True): + if opt.yes: msg(p) + return change_amt + else: + msg(wmsg['not_enough_coin'].format(abs(change_amt))) + + def create(self,cmd_args,do_info=False): + + if opt.comment_file: self.add_comment(opt.comment_file) + + if not do_info: self.get_outputs_from_cmdline(cmd_args) + + do_license_msg() + + from mmgen.tw import MMGenTrackingWallet + tw = MMGenTrackingWallet(minconf=opt.minconf) + tw.view_and_sort(self) + tw.display_total() + + if do_info: sys.exit(0) + + self.send_amt = self.sum_outputs() + + msg('Total amount to spend: {}'.format( + ('Unknown','{} {}'.format(self.send_amt.hl(),g.coin))[bool(self.send_amt)] + )) + + change_amt = self.get_inputs_from_user(tw) + + if opt.rbf: self.signal_for_rbf() # only after we have inputs + + chg_idx = self.get_chg_output_idx() + + if change_amt == 0: + msg('Warning: Change address will be deleted as transaction produces no change') + self.del_output(chg_idx) + else: + self.update_output_amt(chg_idx,g.proto.coin_amt(change_amt)) + + if not self.send_amt: + self.send_amt = change_amt + + if not opt.yes: + self.add_comment() # edits an existing comment + self.create_raw() # creates self.hex, self.txid + + self.add_timestamp() + self.add_blockcount() + self.chain = g.chain + + assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee + + qmsg('Transaction successfully created') + + if not opt.yes: + self.view_with_prompt('View decoded transaction?') + class MMGenBumpTX(MMGenTX): min_fee = None diff --git a/mmgen/txcreate.py b/mmgen/txcreate.py deleted file mode 100755 index 628a413b..00000000 --- a/mmgen/txcreate.py +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/env python -# -# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution -# Copyright (C)2013-2017 Philemon -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -txcreate: Create a cryptocoin transaction with MMGen- and/or non-MMGen inputs and outputs -""" - -from mmgen.common import * -from mmgen.tx import * -from mmgen.tw import * - -pnm = g.proj_name - -wmsg = { - '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 {pnm} 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 {pnm} 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(), - 'non_mmgen_inputs': """ -NOTE: This transaction includes non-{pnm} inputs, which makes the signing -process more complicated. When signing the transaction, keys for non-{pnm} -inputs must be supplied to '{pnl}-txsign' in a file with the '--keys-from-file' -option. -Selected non-{pnm} inputs: {{}} -""".strip().format(pnm=pnm,pnl=pnm.lower()), - 'not_enough_coin': """ -Selected outputs insufficient to fund this transaction ({{}} {} needed) -""".strip().format(g.coin), - 'no_change_output': """ -ERROR: No change address specified. If you wish to create a transaction with -only one output, specify a single output address with no {} amount -""".strip().format(g.coin), -} - -def select_unspent(unspent,prompt): - while True: - reply = my_raw_input(prompt).strip() - if reply: - selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return') - if selected: - if selected[-1] <= len(unspent): - return selected - msg('Unspent output number must be <= %s' % len(unspent)) - -def mmaddr2coinaddr(mmaddr,ad_w,ad_f): - - # assume mmaddr has already been checked - coin_addr = ad_w.mmaddr2coinaddr(mmaddr) - - if not coin_addr: - if ad_f: - coin_addr = ad_f.mmaddr2coinaddr(mmaddr) - if coin_addr: - msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr)) - if not keypress_confirm('Continue anyway?'): - sys.exit(1) - else: - die(2,wmsg['addr_not_found'].format(pnm=pnm,mmgenaddr=mmaddr)) - else: - die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr)) - - return CoinAddr(coin_addr) - -def get_fee_from_estimate_or_user(tx,estimate_fail_msg_shown=[]): - - if opt.tx_fee: - desc = 'User-selected' - start_fee = opt.tx_fee - else: - desc = 'Network-estimated' - ret = g.rpch.estimatefee(opt.tx_confs) - if ret == -1: - if not estimate_fail_msg_shown: - msg('Network fee estimation for {} confirmations failed'.format(opt.tx_confs)) - estimate_fail_msg_shown.append(True) - start_fee = None - else: - start_fee = g.proto.coin_amt(ret) * opt.tx_fee_adj * tx.estimate_size() / 1024 - if opt.verbose: - msg('{} fee ({} confs): {} {}/kB'.format(desc,opt.tx_confs,ret,g.coin)) - msg('TX size (estimated): {}'.format(tx.estimate_size())) - - return tx.get_usr_fee_interactive(start_fee,desc=desc) - -def get_outputs_from_cmdline(cmd_args,tx): - from mmgen.addr import AddrList,AddrData - addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext] - cmd_args = set(cmd_args) - set(addrfiles) - - ad_f = AddrData() - for a in addrfiles: - check_infile(a) - ad_f.add(AddrList(a)) - - ad_w = AddrData(source='tw') - - for a in cmd_args: - if ',' in a: - a1,a2 = a.split(',',1) - if is_mmgen_id(a1) or is_coin_addr(a1): - coin_addr = mmaddr2coinaddr(a1,ad_w,ad_f) if is_mmgen_id(a1) else CoinAddr(a1) - tx.add_output(coin_addr,g.proto.coin_amt(a2)) - else: - die(2,"%s: invalid subargument in command-line argument '%s'" % (a1,a)) - elif is_mmgen_id(a) or is_coin_addr(a): - if tx.get_chg_output_idx() != None: - die(2,'ERROR: More than one change address listed on command line') - coin_addr = mmaddr2coinaddr(a,ad_w,ad_f) if is_mmgen_id(a) else CoinAddr(a) - tx.add_output(coin_addr,g.proto.coin_amt('0'),is_chg=True) - else: - die(2,'{}: invalid command-line argument'.format(a)) - - if not tx.outputs: - die(2,'At least one output must be specified on the command line') - - if tx.get_chg_output_idx() == None: - die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(tx.outputs) == 1]) - - tx.add_mmaddrs_to_outputs(ad_w,ad_f) - tx.check_dup_addrs('outputs') - - if not segwit_is_active() and tx.has_segwit_outputs(): - fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain' - rdie(2,fs.format(g.proj_name)) - -def get_inputs_from_user(tw,tx,caller): - - while True: - m = 'Enter a range or space-separated list of outputs to spend: ' - sel_nums = select_unspent(tw.unspent,m) - msg('Selected output%s: %s' % (suf(sel_nums,'s'),' '.join(str(i) for i in sel_nums))) - - sel_unspent = tw.MMGenTwOutputList([tw.unspent[i-1] for i in sel_nums]) - - t_inputs = sum(s.amt for s in sel_unspent) - if t_inputs < tx.send_amt: - msg(wmsg['not_enough_coin'].format(tx.send_amt-t_inputs)) - continue - - non_mmaddrs = [i for i in sel_unspent if i.twmmid.type == 'non-mmgen'] - if non_mmaddrs and caller != 'txdo': - msg(wmsg['non_mmgen_inputs'].format(', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs]))))) - if not keypress_confirm('Accept?'): - continue - - tx.copy_inputs_from_tw(sel_unspent) # makes tx.inputs - - change_amt = tx.sum_inputs() - tx.send_amt - get_fee_from_estimate_or_user(tx) - - if change_amt >= 0: - p = 'Transaction produces {} {} in change'.format(change_amt.hl(),g.coin) - if opt.yes or keypress_confirm(p+'. OK?',default_yes=True): - if opt.yes: msg(p) - return change_amt - else: - msg(wmsg['not_enough_coin'].format(abs(change_amt))) - -def txcreate(cmd_args,do_info=False,caller='txcreate'): - - rpc_init() - - tx = MMGenTX() - - if opt.comment_file: tx.add_comment(opt.comment_file) - - if not do_info: get_outputs_from_cmdline(cmd_args,tx) - - do_license_msg() - - tw = MMGenTrackingWallet(minconf=opt.minconf) - tw.view_and_sort(tx) - tw.display_total() - - if do_info: sys.exit(0) - - tx.send_amt = tx.sum_outputs() - - msg('Total amount to spend: {}'.format( - ('Unknown','{} {}'.format(tx.send_amt.hl(),g.coin))[bool(tx.send_amt)] - )) - - change_amt = get_inputs_from_user(tw,tx,caller) - - if opt.rbf: tx.signal_for_rbf() # only after we have inputs - - chg_idx = tx.get_chg_output_idx() - - if change_amt == 0: - msg('Warning: Change address will be deleted as transaction produces no change') - tx.del_output(chg_idx) - else: - tx.update_output_amt(chg_idx,g.proto.coin_amt(change_amt)) - - if not tx.send_amt: - tx.send_amt = change_amt - - dmsg('tx: %s' % tx) - - if not opt.yes: - tx.add_comment() # edits an existing comment - tx.create_raw() # creates tx.hex, tx.txid - - tx.add_timestamp() - tx.add_blockcount() - tx.chain = g.chain - - assert tx.sum_inputs() - tx.sum_outputs() <= g.proto.max_tx_fee - - qmsg('Transaction successfully created') - - dmsg('TX (final): %s' % tx) - - if not opt.yes: - tx.view_with_prompt('View decoded transaction?') - - return tx diff --git a/mmgen/util.py b/mmgen/util.py index 18b2f101..14c0883a 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -807,7 +807,7 @@ def rpc_init(reinit=False): try: assert block0 == g.proto.block0,'Incorrect Genesis block for {}'.format(g.proto.__name__) for fork in g.proto.forks: - if latest < fork[0]: break + if fork[0] == None or latest < fork[0]: break bhash = conn.getblockhash(fork[0]) assert bhash == fork[1], ( 'Bad block hash at fork block {}. Is this the {} chain?'.format(fork[0],fork[2].upper()))