Browse Source

txcreate(tx) -> tx.create() (delete txcreate.py)

philemon 7 years ago
parent
commit
207632cb40
8 changed files with 226 additions and 255 deletions
  1. 1 1
      mmgen/main_txbump.py
  2. 5 2
      mmgen/main_txcreate.py
  3. 3 2
      mmgen/main_txdo.py
  4. 3 4
      mmgen/obj.py
  5. 2 1
      mmgen/protocol.py
  6. 211 2
      mmgen/tx.py
  7. 0 242
      mmgen/txcreate.py
  8. 1 1
      mmgen/util.py

+ 1 - 1
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

+ 5 - 2
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)

+ 3 - 2
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)
 

+ 3 - 4
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'

+ 2 - 1
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'

+ 211 - 2
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

+ 0 - 242
mmgen/txcreate.py

@@ -1,242 +0,0 @@
-#!/usr/bin/env python
-#
-# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
-# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
-#
-# 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 <http://www.gnu.org/licenses/>.
-
-"""
-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

+ 1 - 1
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()))