[tx]: BIP 125 replace-by-fee (RBF) support:

* create replaceable transactions with 'mmgen-txcreate' and 'mmgen-txdo'
        using the '--rbf' option
      * create replacement transactions from existing MMGen transactions
        using the new, optionally scriptable 'mmgen-txbump' command
      * Bitcoind must be started with the '-walletrbf' option to enable RBF
        functionality

[tx]: Command scriptability:
      * New '--yes' option makes 'txbump' and 'txsign' fully non-interactive
        and 'txcreate' and 'txsend' mostly non-interactive

[tx]: Satoshis-per-byte format:
      * Tx fees, both on the command line and at the interactive prompt, may be
        specified either as absolute BTC amounts or in satoshis-per-byte format
        (an integer followed by the letter 's')

[tx]: Fees
	  * Completely reworked fee-handling code with better fee checking
	  * default tx fee eliminated, max_tx_fee configurable in mmgen.cfg

Bugfixes and usability improvements:
      *	'mmgen-tool listaddresses' now list addresses from multiple seeds
        correctly
      * Improved user interaction with all 'mmgen-tx*' commands
This commit is contained in:
The MMGen Project 2017-05-17 15:37:30 +03:00
commit 93c99755a7
Signed by: mmgen
GPG key ID: 62DBE9E5212F05BE
16 changed files with 553 additions and 151 deletions

View file

@ -43,8 +43,8 @@
# A value of 0 disables user entropy, but this is not recommended:
# usr_randchars 30
# Set the default transaction fee in BTC:
# tx_fee 0.0003
# Set the maximum transaction fee in BTC:
# max_tx_fee 0.01
# Set the transaction fee adjustment factor. Auto-calculated fees are
# multiplied by this value:

25
mmgen-txbump Executable file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env python
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2016 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/>.
"""
mmgen-txbump: Increase the fee on a replaceable (RBF) MMGen transaction, and
optionally sign and send it.
"""
from mmgen.main import launch
launch("txbump")

View file

@ -36,8 +36,8 @@ class g(object):
sys.exit(ev)
# Variables - these might be altered at runtime:
version = '0.9.0'
release_date = 'December 2016'
version = '0.9.O+'
release_date = 'May 2017'
proj_name = 'MMGen'
proj_url = 'https://github.com/mmgen/mmgen'
@ -51,13 +51,14 @@ class g(object):
hash_preset = '3'
usr_randchars = 30
tx_fee = BTCAmt('0.0003')
tx_fee_adj = 1.0
tx_confs = 3
max_tx_fee = BTCAmt('0.01')
tx_fee_adj = 1.0
tx_confs = 3
satoshi = BTCAmt('0.00000001') # One bitcoin equals 100,000,000 satoshis
seed_len = 256
http_timeout = 60
max_int = 0xffffffff
# Constants - some of these might be overriden, but they don't change thereafter
@ -111,8 +112,8 @@ class g(object):
)
cfg_file_opts = (
'color','debug','hash_preset','http_timeout','no_license','rpc_host','rpc_port',
'quiet','tx_fee','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password',
'bitcoin_data_dir','force_256_color'
'quiet','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password',
'bitcoin_data_dir','force_256_color','max_tx_fee'
)
env_opts = (
'MMGEN_BOGUS_WALLET_DATA',
@ -132,7 +133,7 @@ class g(object):
# Global var sets user opt:
global_sets_opt = ['minconf','seed_len','hash_preset','usr_randchars','debug',
'quiet','tx_confs','tx_fee_adj','tx_fee','key_generator']
'quiet','tx_confs','tx_fee_adj','key_generator']
keyconv_exec = 'keyconv'

133
mmgen/main_txbump.py Executable file
View file

@ -0,0 +1,133 @@
#!/usr/bin/env python
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2016 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/>.
"""
mmgen-txbump: Increase the fee on a replaceable (replace-by-fee) MMGen
transaction, and optionally sign and send it
"""
from mmgen.txcreate import *
from mmgen.txsign import *
opts_data = {
'desc': 'Increase the fee on a replaceable (RBF) {g.proj_name} transaction, creating a new transaction, and optionally sign and send the new transaction'.format(g=g),
'usage': '[opts] <{g.proj_name} TX file> [seed source] ...'.format(g=g),
'sets': ( ('yes', True, 'quiet', True), ),
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-b, --brain-params=l,p Use seed length 'l' and hash preset 'p' for
brainwallet input
-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, as a decimal BTC amount or in
satoshis per byte (an integer followed by 's')
-H, --hidden-incog-input-params=f,o Read hidden incognito data from file
'f' at offset 'o' (comma-separated)
-i, --in-fmt= f Input is from wallet format 'f' (see FMT CODES below)
-l, --seed-len= l Specify wallet seed length of 'l' bits. This option
is required only for brainwallet and incognito inputs
with non-standard (< {g.seed_len}-bit) seed lengths.
-k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
-K, --key-generator= m Use method 'm' for public key generation
Options: {kgs}
(default: {kg})
-M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key-
address file (output of '{pnl}-keygen'). Permits
online signing without an {pnm} seed source. The
key-address file is also used to verify {pnm}-to-BTC
mappings, so the user should record its checksum.
-o, --output-to-reduce=o Deduct the fee from output 'o' (an integer, or 'c'
for the transaction's change output, if present)
-O, --old-incog-fmt Specify old-format incognito input
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
for password hashing (default: '{g.hash_preset}')
-P, --passwd-file= f Get {pnm} wallet or bitcoind passphrase from file 'f'
-q, --quiet Suppress warnings; overwrite files without prompting
-s, --send Sign and send the transaction (the default if seed
data is provided)
-v, --verbose Produce more verbose output
-y, --yes Answer 'yes' to prompts, suppress non-essential output
-z, --show-hash-presets Show information on available hash presets
""".format(g=g,pnm=pnm,pnl=pnm.lower(),
kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
kg=g.key_generator),
'notes': '\n' + fee_notes + txsign_notes
}
cmd_args = opts.init(opts_data)
c = bitcoin_connection()
tx_file = cmd_args.pop(0)
check_infile(tx_file)
seed_files = get_seed_files(opt,cmd_args) if (cmd_args or opt.send) else None
kal = get_keyaddrlist(opt)
kl = get_keylist(opt)
tx = MMGenBumpTX(filename=tx_file,send=(seed_files or kl or kal))
do_license_msg()
silent = opt.yes and opt.tx_fee != None and opt.output_to_reduce != None
if not silent:
msg(green('ORIGINAL TRANSACTION'))
msg(tx.format_view(terse=True))
tx.set_min_fee()
if not [o.amt for o in tx.outputs if o.amt >= tx.min_fee]:
die(1,'Transaction cannot be bumped.' +
'\nAll outputs have less than the minimum fee ({} BTC)'.format(tx.min_fee))
msg('Creating new transaction')
op_idx = tx.choose_output()
if not silent:
msg('Minimum fee for new transaction: {} BTC'.format(tx.min_fee))
fee = tx.get_usr_fee_interactive(tx_fee=opt.tx_fee,desc='User-selected')
tx.update_output_amt(op_idx,tx.sum_inputs()-tx.sum_outputs(exclude=op_idx)-fee)
d = tx.get_fee()
assert d == fee and d <= g.max_tx_fee
if not opt.yes:
tx.add_comment() # edits an existing comment
tx.create_raw(c) # creates tx.hex, tx.txid
tx.add_timestamp()
tx.add_blockcount(c)
qmsg('Fee successfully increased')
if not silent:
msg(green('\nREPLACEMENT TRANSACTION:'))
msg_r(tx.format_view(terse=True))
if seed_files or kl or kal:
txsign(opt,c,tx,seed_files,kl,kal)
tx.write_to_file(ask_write=False)
if tx.send(c):
tx.write_to_file(ask_write=False)
else:
tx.write_to_file(ask_write=not opt.yes,ask_write_default_yes=False,ask_overwrite=not opt.yes)

View file

@ -26,24 +26,30 @@ from mmgen.txcreate import *
opts_data = {
'desc': 'Create a transaction with outputs to specified Bitcoin or {g.proj_name} addresses'.format(g=g),
'usage': '[opts] <addr,amt> ... [change addr] [addr file] ...',
'sets': ( ('yes', True, 'quiet', True), ),
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-a, --tx-fee-adj= f Adjust transaction fee by factor 'f' (see below)
-B, --no-blank Don't blank screen before displaying unspent outputs
-c, --comment-file= f Source the transaction's comment from file 'f'
-C, --tx-confs= c Desired number of confirmations (default: {g.tx_confs})
-d, --outdir= d Specify an alternate directory 'd' for output
-f, --tx-fee= f Transaction fee (default: {g.tx_fee} BTC (but see below))
-i, --info Display unspent outputs and exit
-m, --minconf= n Minimum number of confirmations required to spend outputs (default: 1)
-q, --quiet Suppress warnings; overwrite files without prompting
-v, --verbose Produce more verbose output
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-a, --tx-fee-adj= f Adjust transaction fee by factor 'f' (see below)
-B, --no-blank Don't blank screen before displaying unspent outputs
-c, --comment-file=f Source the transaction's comment from file 'f'
-C, --tx-confs= c Desired number of confirmations (default: {g.tx_confs})
-d, --outdir= d Specify an alternate directory 'd' for output
-f, --tx-fee= f Transaction fee, as a decimal BTC amount or in satoshis
per byte (an integer followed by 's'). If omitted, fee
will be calculated using bitcoind's 'estimatefee' call
-i, --info Display unspent outputs and exit
-m, --minconf= n Minimum number of confirmations required to spend
outputs (default: 1)
-q, --quiet Suppress warnings; overwrite files without prompting
-r, --rbf Make transaction BIP 125 replaceable (replace-by-fee)
-v, --verbose Produce more verbose output
-y, --yes Answer 'yes' to prompts, suppress non-essential output
""".format(g=g),
'notes': '\n' + txcreate_notes
'notes': '\n' + txcreate_notes + fee_notes
}
cmd_args = opts.init(opts_data)
do_license_msg()
tx = txcreate(opt,cmd_args,do_info=opt.info)
tx.write_to_file(ask_write_default_yes=False)
tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False)

View file

@ -26,6 +26,7 @@ from mmgen.txsign import *
opts_data = {
'desc': 'Create, sign and send an {g.proj_name} transaction'.format(g=g),
'usage': '[opts] <addr,amt> ... [change addr] [addr file] ... [seed source] ...',
'sets': ( ('yes', True, 'quiet', True), ),
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
@ -37,7 +38,10 @@ opts_data = {
-C, --tx-confs= c Desired number of confirmations (default: {g.tx_confs})
-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 (but see below))
-f, --tx-fee= f Transaction fee, as a decimal BTC amount or in
satoshis per byte (an integer followed by 's').
If omitted, bitcoind's 'estimatefee' will be used
to calculate the fee.
-H, --hidden-incog-input-params=f,o Read hidden incognito data from file
'f' at offset 'o' (comma-separated)
-i, --in-fmt= f Input is from wallet format 'f' (see FMT CODES below)
@ -59,13 +63,15 @@ opts_data = {
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
for password hashing (default: '{g.hash_preset}')
-P, --passwd-file= f Get {pnm} wallet passphrase from file 'f'
-r, --rbf Make transaction BIP 125 (replace-by-fee) replaceable
-q, --quiet Suppress warnings; overwrite files without prompting
-v, --verbose Produce more verbose output
-y, --yes Answer 'yes' to prompts, suppress non-essential output
-z, --show-hash-presets Show information on available hash presets
""".format(g=g,pnm=pnm,pnl=pnm.lower(),
kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
kg=g.key_generator),
'notes': '\n' + txcreate_notes + txsign_notes
'notes': '\n' + txcreate_notes + fee_notes + txsign_notes
}
cmd_args = opts.init(opts_data)
@ -81,5 +87,5 @@ tx = txcreate(opt,cmd_args,caller='txdo')
txsign(opt,c,tx,seed_files,kl,kal)
tx.write_to_file(ask_write=False)
if tx.send(opt,c):
tx.write_to_file(ask_write=False)
if tx.send(c):
tx.write_to_file(ask_overwrite=False,ask_write=False)

View file

@ -27,11 +27,13 @@ opts_data = {
'desc': 'Send a Bitcoin transaction signed by {pnm}-txsign'.format(
pnm=g.proj_name.lower()),
'usage': '[opts] <signed transaction file>',
'sets': ( ('yes', True, 'quiet', True), ),
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-d, --outdir= d Specify an alternate directory 'd' for output
-q, --quiet Suppress warnings; overwrite files without prompting
-y, --yes Answer 'yes' to prompts, suppress non-essential output
"""
}
@ -44,12 +46,19 @@ else: opts.usage()
do_license_msg()
tx = MMGenTX(infile)
c = bitcoin_connection()
if not tx.check_signed(c):
die(1,'Transaction has no signature!')
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)
if tx.send(opt,c):
tx.write_to_file(ask_write=False)
if not tx.check_signed(c):
die(1,'Transaction is not signed!')
if tx.btc_txid:
msg('Warning: transaction has already been sent!')
qmsg("Signed transaction file '%s' is valid" % infile)
if not opt.yes:
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)
if tx.send(c):
tx.write_to_file(ask_overwrite=False,ask_write=False)

View file

@ -26,11 +26,12 @@ from mmgen.txsign import *
opts_data = {
'desc': 'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnm.lower()),
'usage': '[opts] <transaction file>... [seed source]...',
'sets': ( ('yes', True, 'quiet', True), ),
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-b, --brain-params=l,p Use seed length 'l' and hash preset 'p' for brainwallet
input
-b, --brain-params=l,p Use seed length 'l' and hash preset 'p' for
brainwallet input
-d, --outdir= d Specify an alternate directory 'd' for output
-D, --tx-id Display transaction ID and exit
-e, --echo-passphrase Print passphrase to screen when typing it
@ -57,6 +58,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
-y, --yes Answer 'yes' to prompts, suppress non-essential output
""".format(
g=g,pnm=pnm,pnl=pnm.lower(),
kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
@ -98,8 +100,12 @@ for tx_num,tx_file in enumerate(tx_files,1):
if opt.info or opt.terse_info:
tx.view(pause=False,terse=opt.terse_info); continue
tx.view_with_prompt('View data for transaction%s?' % tx_num_str)
if not opt.yes:
tx.view_with_prompt('View data for transaction%s?' % tx_num_str)
txsign(opt,c,tx,seed_files,kl,kal,tx_num_str)
tx.add_comment() # edits an existing comment
tx.write_to_file(ask_write_default_yes=True,add_desc=tx_num_str)
if not opt.yes:
tx.add_comment() # edits an existing comment
tx.write_to_file(ask_write=not opt.yes,ask_write_default_yes=True,add_desc=tx_num_str)

View file

@ -256,7 +256,7 @@ class Hilite(object):
if trunc_ok and len(s) > width: s = s[:width]
if app:
return cls.colorize(a+s+b,color=color) + \
cls.colorize(app.ljust(width-len(a+s+b)),color=appcolor)
cls.colorize(app.ljust(width-len(a+s+b)),color=appcolor)
else:
return cls.colorize((a+s+b).ljust(width),color=color)

View file

@ -278,6 +278,18 @@ def check_opts(usr_opts): # Returns false if any check fails
return False
return True
def opt_is_tx_fee(val,desc):
from mmgen.tx import MMGenTX
ret = MMGenTX().convert_fee_spec(val,224,on_fail='return')
if ret == False:
msg("'{}': invalid {} (not a BTC amount or satoshis-per-byte specification)".format(
val,desc))
elif ret != None and ret > g.max_tx_fee:
msg("'{}': invalid {} (greater than max_tx_fee ({} BTC))".format(val,desc,g.max_tx_fee))
else:
return True
return False
def opt_is_in_list(val,lst,desc):
if val not in lst:
q,sep = (('',','),("'","','"))[type(lst[0]) == str]
@ -375,6 +387,11 @@ def check_opts(usr_opts): # Returns false if any check fails
if not opt_is_int(val,desc): return False
if not opt_compares(val,'>=',g.min_urandchars,desc): return False
if not opt_compares(val,'<=',g.max_urandchars,desc): return False
elif key == 'tx_fee':
if not opt_is_tx_fee(val,desc): return False
elif key == 'tx_confs':
if not opt_is_int(val,desc): return False
if not opt_compares(val,'>=',1,desc): return False
elif key == 'key_generator':
if not opt_compares(val,'<=',len(g.key_generators),desc): return False
if not opt_compares(val,'>',0,desc): return False

View file

@ -111,10 +111,12 @@ class BitcoinRPCConnection(object):
dmsg(' RPC GETRESPONSE data ==> %s\n' % r.__dict__)
if r.status != 200:
msgred('RPC Error: {} {}'.format(r.status,r.reason))
msg_r(yellow('Bitcoind RPC Error: '))
msg(red('{} {}'.format(r.status,r.reason)))
e1 = r.read()
try:
e2 = json.loads(e1)['error']['message']
e3 = json.loads(e1)['error']
e2 = '{} (code {})'.format(e3['message'],e3['code'])
except:
e2 = str(e1)
return die_maybe(r,1,e2)

View file

@ -46,7 +46,7 @@ class MMGenTxInputOldFmt(MMGenListItem): # for converting old tx files only
attrs_priv = 'tr',
class MMGenTxInput(MMGenListItem):
attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif'
attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif','sequence'
label = MMGenListItemAttr('label','MMGenAddrLabel')
class MMGenTxOutput(MMGenListItem):
@ -60,16 +60,11 @@ class MMGenTX(MMGenObject):
txid_ext = 'txid'
desc = 'transaction'
max_fee = BTCAmt('0.01')
def __init__(self,filename=None):
self.inputs = []
self.inputs_enc = []
self.outputs = []
self.outputs_enc = []
self.change_addr = ''
self.size = 0 # size of raw serialized tx
self.fee = BTCAmt('0')
self.send_amt = BTCAmt('0') # total amt minus change
self.hex = '' # raw serialized hex transaction
self.label = MMGenTXLabel('')
@ -84,17 +79,27 @@ class MMGenTX(MMGenObject):
self.mark_signed()
self.parse_tx_file(filename)
def add_output(self,btcaddr,amt): # 'txid','vout','amount','label','mmid','address'
self.outputs.append(MMGenTxOutput(addr=btcaddr,amt=amt))
def add_output(self,btcaddr,amt,is_chg=None):
self.outputs.append(MMGenTxOutput(addr=btcaddr,amt=amt,is_chg=is_chg))
def del_output(self,btcaddr):
def get_chg_output_idx(self):
for i in range(len(self.outputs)):
if self.outputs[i].addr == btcaddr:
self.outputs.pop(i); return
raise ValueError
if self.outputs[i].is_chg == True:
return i
return None
def sum_outputs(self):
return BTCAmt(sum([e.amt for e in self.outputs]))
def update_output_amt(self,idx,amt):
o = self.outputs[idx].__dict__
o['amt'] = amt
self.outputs[idx] = MMGenTxOutput(**o)
def del_output(self,idx):
self.outputs.pop(idx)
def sum_outputs(self,exclude=None):
olist = self.outputs if exclude == None else \
self.outputs[:exclude] + self.outputs[exclude+1:]
return BTCAmt(sum([e.amt for e in olist]))
def add_mmaddrs_to_outputs(self,ad_w,ad_f):
a = [e.addr for e in self.outputs]
@ -113,6 +118,8 @@ class MMGenTX(MMGenObject):
#
def create_raw(self,c):
i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs]
if self.inputs[0].sequence:
i[0]['sequence'] = self.inputs[0].sequence
o = dict([(e.addr,e.amt) for e in self.outputs])
self.hex = c.createrawtransaction(i,o)
self.txid = MMGenTxID(make_chksum_6(unhexlify(self.hex)).upper())
@ -137,31 +144,70 @@ class MMGenTX(MMGenObject):
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',fee_estimate*opt.tx_fee_adj*self.size / 1024
else:
ftype,fee = 'User-selected',opt.tx_fee
# https://bitcoin.stackexchange.com/questions/1195/
# how-to-calculate-transaction-size-before-sending
# 180: uncompressed, 148: compressed
def get_size(self):
if not self.inputs or not self.outputs: return None
return len(self.inputs)*180 + len(self.outputs)*34 + 10
ufee = None
if not keypress_confirm('{} TX fee is {} BTC. OK?'.format(ftype,fee.hl()),default_yes=True):
while True:
ufee = my_raw_input('Enter transaction fee: ')
if BTCAmt(ufee,on_fail='return'):
ufee = BTCAmt(ufee)
if ufee > self.max_fee:
msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,self.max_fee))
else:
fee = ufee
break
self.fee = 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 get_fee(self):
return self.sum_inputs() - self.sum_outputs()
def btc2spb(self,btc_fee):
return int(btc_fee/g.satoshi/self.get_size())
def get_relay_fee(self):
assert self.get_size()
kb_fee = BTCAmt(bitcoin_connection().getinfo()['relayfee'])
vmsg('Relay fee: {} BTC/kB'.format(kb_fee))
return kb_fee * self.get_size() / 1024
def convert_fee_spec(self,tx_fee,tx_size,on_fail='throw'):
if BTCAmt(tx_fee,on_fail='silent'):
return BTCAmt(tx_fee)
elif len(tx_fee) >= 2 and tx_fee[-1] == 's' and is_int(tx_fee[:-1]) and int(tx_fee[:-1]) >= 1:
if tx_size:
return BTCAmt(int(tx_fee[:-1]) * tx_size * g.satoshi)
else:
return None
else:
if on_fail == 'return':
return False
elif on_fail == 'throw':
assert False, "'{}': invalid tx-fee argument".format(tx_fee)
def get_usr_fee(self,tx_fee,desc='Missing description'):
btc_fee = self.convert_fee_spec(tx_fee,self.get_size(),on_fail='return')
if btc_fee == None:
msg("'{}': cannot convert satoshis-per-byte to BTC because transaction size is unknown".format(tx_fee))
assert False # because we shouldn't be calling this if tx size is unknown
elif btc_fee == False:
msg("'{}': invalid TX fee (not a BTC amount or satoshis-per-byte specification)".format(tx_fee))
return False
elif btc_fee > g.max_tx_fee:
msg('{} BTC: {} fee too large (maximum fee: {} BTC)'.format(btc_fee,desc,g.max_tx_fee))
return False
elif btc_fee < self.get_relay_fee():
msg('{} BTC: {} fee too small (below relay fee of {} BTC)'.format(str(btc_fee),desc,str(self.get_relay_fee())))
return False
else:
return btc_fee
def get_usr_fee_interactive(self,tx_fee=None,desc='Starting'):
btc_fee = None
while True:
if tx_fee:
btc_fee = self.get_usr_fee(tx_fee,desc)
if btc_fee:
m = ('',' (after {}x adjustment)'.format(opt.tx_fee_adj))[opt.tx_fee_adj != 1]
p = '{} TX fee{}: {} BTC ({} satoshis per byte)'.format(desc,m,
btc_fee.hl(),pink(str(self.btc2spb(btc_fee))))
if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
if opt.yes: msg(p)
return btc_fee
tx_fee = my_raw_input('Enter transaction fee: ')
desc = 'User-selected'
# inputs methods
def list_wifs(self,desc,mmaddrs_only=False):
@ -248,27 +294,36 @@ class MMGenTX(MMGenObject):
msg('OK')
self.hex = sig_tx['hex']
self.mark_signed()
vmsg('Signed transaction size: {}'.format(len(self.hex)/2))
return True
else:
msg('failed\nBitcoind returned the following errors:')
pp_msg(sig_tx['errors'])
return False
def mark_raw(self):
self.desc = 'transaction'
self.ext = self.raw_ext
def mark_signed(self):
self.desc = 'signed transaction'
self.ext = self.sig_ext
def is_signed(self,color=False):
ret = self.desc == 'signed transaction'
return (red,green)[ret](str(ret)) if color else ret
def check_signed(self,c):
d = c.decoderawtransaction(self.hex)
ret = bool(d['vin'][0]['scriptSig']['hex'])
if ret: self.mark_signed()
return ret
def send(self,opt,c,prompt_user=True):
def send(self,c,prompt_user=True):
if prompt_user:
m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)]
m2 = 'broadcast this transaction to the network'
m3 = ('YES, I REALLY WANT TO DO THIS','YES')[bool(opt.quiet)]
m3 = ('YES, I REALLY WANT TO DO THIS','YES')[bool(opt.quiet or opt.yes)]
confirm_or_exit(m1,m2,m3)
msg('Sending transaction')
@ -276,7 +331,7 @@ class MMGenTX(MMGenObject):
ret = 'deadbeef' * 8
m = 'BOGUS transaction NOT sent: %s'
else:
ret = c.sendrawtransaction(self.hex) # exits on failure?
ret = c.sendrawtransaction(self.hex) # exits on failure
m = 'Transaction sent: %s'
if ret:
@ -288,6 +343,7 @@ class MMGenTX(MMGenObject):
self.add_blockcount(c)
return True
# rpc implementation exits on failure, so we won't get here
msg('Sending of transaction {} failed'.format(self.txid))
return False
@ -297,12 +353,14 @@ class MMGenTX(MMGenObject):
ask_write=ask_write,
ask_write_default_yes=ask_write_default_yes)
def write_to_file(self,add_desc='',ask_write=True,ask_write_default_yes=False,ask_tty=True):
def write_to_file(self,add_desc='',ask_write=True,ask_write_default_yes=False,ask_tty=True,ask_overwrite=True):
if ask_write == False:
ask_write_default_yes=True
self.format()
fn = '%s[%s].%s' % (self.txid,self.send_amt,self.ext)
spbs = ('',',{}'.format(self.btc2spb(self.get_fee())))[self.is_rbf()]
fn = '{}[{}{}].{}'.format(self.txid,self.send_amt,spbs,self.ext)
write_data_to_file(fn,self.fmt_data,self.desc+add_desc,
ask_overwrite=ask_overwrite,
ask_write=ask_write,
ask_tty=ask_tty,
ask_write_default_yes=ask_write_default_yes)
@ -314,7 +372,7 @@ class MMGenTX(MMGenObject):
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')
o = self.format_view(terse=terse)
if pager: do_pager(o)
else:
sys.stdout.write(o)
@ -323,6 +381,21 @@ class MMGenTX(MMGenObject):
get_char('Press any key to continue: ')
msg('')
# def is_rbf_fromhex(self,color=False):
# try:
# dec_tx = bitcoin_connection().decoderawtransaction(self.hex)
# except:
# return yellow('Unknown') if color else None
# rbf = bool(dec_tx['vin'][0]['sequence'] == g.max_int - 2)
# return (red,green)[rbf](str(rbf)) if color else rbf
def is_rbf(self,color=False):
ret = None < self.inputs[0].sequence <= g.max_int - 2
return (red,green)[ret](str(ret)) if color else ret
def signal_for_rbf(self):
self.inputs[0].sequence = g.max_int - 2
def format_view(self,terse=False):
try:
blockcount = bitcoin_connection().getblockcount()
@ -330,11 +403,12 @@ class MMGenTX(MMGenObject):
blockcount = None
hdr_fs = (
'TRANSACTION DATA\n\nHeader: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n',
'Transaction {} - {} BTC - {} UTC\n'
'TRANSACTION DATA\n\nHeader: [ID:{}] [{} BTC] [{} UTC] [RBF:{}] [Signed:{}]\n',
'Transaction {} {} BTC ({} UTC) RBF={} Signed={}\n'
)[bool(terse)]
out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),self.timestamp)
out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),self.timestamp,
self.is_rbf(color=True),self.is_signed(color=True))
enl = ('\n','')[bool(terse)]
if self.btc_txid: out += 'Bitcoin TxID: {}\n'.format(self.btc_txid.hl())
@ -386,8 +460,8 @@ class MMGenTX(MMGenObject):
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'
'Total input: %s BTC\nTotal output: %s BTC\nTX fee: %s BTC (%s satoshis per byte)\n',
'In %s BTC - Out %s BTC - Fee %s BTC (%s satoshis/byte)\n'
)[bool(terse)]
total_in = self.sum_inputs()
@ -395,10 +469,16 @@ class MMGenTX(MMGenObject):
out += fs % (
total_in.hl(),
total_out.hl(),
(total_in-total_out).hl()
(total_in-total_out).hl(),
pink(str(self.btc2spb(total_in-total_out))),
)
if opt.verbose:
ts = len(self.hex)/2 if self.hex else 'unknown'
out += 'Transaction size: estimated - {}, actual - {}\n'.format(self.get_size(),ts)
return out
# only tx label may contain non-ascii chars
# encode() is necessary for test suite with PopenSpawn
return out.encode('utf8')
def parse_tx_file(self,infile):
@ -453,3 +533,68 @@ class MMGenTX(MMGenObject):
try: self.outputs = self.decode_io('outputs',eval(outputs_data))
except: do_err('btc-to-mmgen address map data')
class MMGenBumpTX(MMGenTX):
min_fee = None
bump_output_idx = None
def __init__(self,filename,send=False):
super(type(self),self).__init__(filename)
if not self.is_rbf():
die(1,"Transaction '{}' is not replaceable (RBF)".format(self.txid))
# If sending, require tx to have been signed and broadcast
if send:
if not self.is_signed():
die(1,"File '{}' is not a signed {} transaction file".format(filename,g.proj_name))
if not self.btc_txid:
die(1,"Transaction '{}' was not broadcast to the network".format(self.txid,g.proj_name))
self.btc_txid = ''
self.mark_raw()
def choose_output(self):
chg_idx = self.get_chg_output_idx()
init_reply = opt.output_to_reduce
while True:
if init_reply == None:
reply = my_raw_input('Which output do you wish to deduct the fee from? ')
else:
reply,init_reply = init_reply,None
if chg_idx == None and not is_int(reply):
msg("Output must be an integer")
elif chg_idx != None and not is_int(reply) and reply != 'c':
msg("Output must be an integer, or 'c' for the change output")
else:
idx = chg_idx if reply == 'c' else (int(reply) - 1)
if idx < 0 or idx >= len(self.outputs):
msg('Output must be in the range 1-{}'.format(len(self.outputs)))
else:
o_amt = self.outputs[idx].amt
cs = ('',' (change output)')[chg_idx == idx]
p = 'Fee will be deducted from output {}{} ({} BTC)'.format(idx+1,cs,o_amt)
if o_amt < self.min_fee:
msg('Minimum fee ({} BTC) is greater than output amount ({} BTC)'.format(
self.min_fee,o_amt))
elif opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
if opt.yes: msg(p)
self.bump_output_idx = idx
return idx
def set_min_fee(self):
self.min_fee = self.sum_inputs() - self.sum_outputs() + self.get_relay_fee()
def get_usr_fee(self,tx_fee,desc):
ret = super(type(self),self).get_usr_fee(tx_fee,desc)
if ret < self.min_fee:
msg('{} BTC: {} fee too small. Minimum fee: {} BTC ({} satoshis per byte)'.format(
ret,desc,self.min_fee,self.btc2spb(self.min_fee)))
return False
output_amt = self.outputs[self.bump_output_idx].amt
if ret >= output_amt:
msg('{} BTC: {} fee too large. Maximum fee: <{} BTC'.format(ret,desc,output_amt))
return False
return ret

View file

@ -31,12 +31,13 @@ txcreate_notes = """
The transaction's outputs are specified on the command line, while its inputs
are chosen from a list of the user's unpent outputs via an interactive menu.
If the transaction fee is not specified by the user, it will be calculated
using bitcoind's "estimatefee" function for the default (or user-specified)
number of confirmations. If "estimatefee" fails, the global default fee of
{g.tx_fee} BTC will be used.
If the transaction fee is not specified on the command line (see FEE
SPECIFICATION below), it will be calculated dynamically using bitcoind's
"estimatefee" function for the default (or user-specified) number of
confirmations. If "estimatefee" fails, the user will be prompted for a fee.
Dynamic fees will be multiplied by the value of '--tx-fee-adj', if specified.
Dynamic ("estimatefee") fees will be multiplied by the value of '--tx-fee-adj',
if specified.
Ages of transactions are approximate based on an average block discovery
interval of {g.mins_per_block} minutes.
@ -48,6 +49,13 @@ To send the value of all inputs (minus TX fee) to a single output, specify
one address with no amount on the command line.
""".format(g=g,pnm=pnm)
fee_notes = """
FEE SPECIFICATION: Transaction fees, both on the command line and at the
interactive prompt, may be specified as either absolute BTC amounts, using a
plain decimal number, or as satoshis per byte, using an integer followed by
the letter 's'.
"""
wmsg = {
'addr_in_addrfile_only': """
Warning: output address {mmgenaddr} is not in the tracking wallet, which means
@ -72,11 +80,15 @@ option.
Selected non-{pnm} inputs: %s
""".strip().format(pnm=pnm,pnl=pnm.lower()),
'not_enough_btc': """
Not enough BTC in the inputs for this transaction (%s BTC)
Selected outputs insufficient to fund this transaction (%s BTC needed)
""".strip(),
'throwaway_change': """
ERROR: This transaction produces change (%s BTC); however, no change address
was specified.
""".strip(),
'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 BTC amount
""".strip(),
}
@ -109,26 +121,25 @@ def mmaddr2baddr(c,mmaddr,ad_w,ad_f):
return BTCAddr(btc_addr)
def get_fee_estimate(c):
if 'tx_fee' in opt.set_by_user: # TODO
return None
def get_fee_from_estimate_or_usr(tx,c,estimate_fail_msg_shown=[]):
if opt.tx_fee:
desc = 'User-selected'
start_fee = opt.tx_fee
else:
desc = 'Network-estimated'
ret = c.estimatefee(opt.tx_confs)
if ret != -1:
return BTCAmt(ret)
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:
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')
start_fee = BTCAmt(ret) * opt.tx_fee_adj * tx.get_size() / 1024
if opt.verbose:
msg('{} fee ({} confs): {} BTC/kB'.format(desc,opt.tx_confs,ret))
msg('TX size (estimated): {}'.format(tx.get_size()))
return tx.get_usr_fee_interactive(start_fee,desc=desc)
def txcreate(opt,cmd_args,do_info=False,caller='txcreate'):
@ -153,29 +164,25 @@ def txcreate(opt,cmd_args,do_info=False,caller='txcreate'):
for a in cmd_args:
if ',' in a:
a1,a2 = a.split(',',1)
if is_btc_addr(a1):
btc_addr = BTCAddr(a1)
elif is_mmgen_id(a1):
btc_addr = mmaddr2baddr(c,a1,ad_w,ad_f)
if is_mmgen_id(a1) or is_btc_addr(a1):
btc_addr = mmaddr2baddr(c,a1,ad_w,ad_f) if is_mmgen_id(a1) else BTCAddr(a1)
tx.add_output(btc_addr,BTCAmt(a2))
else:
die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
tx.add_output(btc_addr,BTCAmt(a2))
elif is_mmgen_id(a) or is_btc_addr(a):
if tx.change_addr:
die(2,'ERROR: More than one change address specified: %s, %s' %
(change_addr, a))
tx.change_addr = mmaddr2baddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else BTCAddr(a)
tx.add_output(tx.change_addr,BTCAmt('0'))
if tx.get_chg_output_idx() != None:
die(2,'ERROR: More than one change address listed on command line')
btc_addr = mmaddr2baddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else BTCAddr(a)
tx.add_output(btc_addr,BTCAmt('0'),is_chg=True)
else:
die(2,'%s: unrecognized argument' % a)
if not tx.outputs:
die(2,'At least one output must be specified on the command line')
if opt.tx_fee > tx.max_fee:
die(2,'Transaction fee too large: %s > %s' % (opt.tx_fee,tx.max_fee))
if tx.get_chg_output_idx() == None:
die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(tx.outputs) == 1])
fee_estimate = get_fee_estimate(c)
tw = MMGenTrackingWallet(minconf=opt.minconf)
tw.view_and_sort()
@ -194,8 +201,14 @@ def txcreate(opt,cmd_args,do_info=False,caller='txcreate'):
('s','')[len(sel_nums)==1],
' '.join(str(i) for i in sel_nums)
))
sel_unspent = [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_btc'] % (tx.send_amt - t_inputs))
continue
non_mmaddrs = [i for i in sel_unspent if i.mmid == None]
if non_mmaddrs and caller != 'txdo':
msg(wmsg['non_mmgen_inputs'] % ', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs]))))
@ -204,42 +217,44 @@ def txcreate(opt,cmd_args,do_info=False,caller='txcreate'):
tx.copy_inputs_from_tw(sel_unspent) # makes tx.inputs
tx.calculate_size_and_fee(fee_estimate) # sets tx.size, tx.fee
if opt.rbf: tx.signal_for_rbf() # only after we have inputs
change_amt = tx.sum_inputs() - tx.send_amt - tx.fee
change_amt = tx.sum_inputs() - tx.send_amt - get_fee_from_estimate_or_usr(tx,c)
if change_amt >= 0:
prompt = 'Transaction produces %s BTC in change. OK?' % change_amt.hl()
if keypress_confirm(prompt,default_yes=True):
p = 'Transaction produces %s BTC in change' % change_amt.hl()
if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
if opt.yes: msg(p)
break
else:
msg(wmsg['not_enough_btc'] % change_amt)
msg(wmsg['not_enough_btc'] % abs(change_amt))
chg_idx = tx.get_chg_output_idx()
if change_amt > 0:
change_amt = BTCAmt(change_amt)
if not tx.change_addr:
die(2,wmsg['throwaway_change'] % change_amt)
tx.del_output(tx.change_addr)
tx.add_output(BTCAddr(tx.change_addr),change_amt)
elif tx.change_addr:
msg('Warning: Change address will be unused as transaction produces no change')
tx.del_output(tx.change_addr)
tx.update_output_amt(chg_idx,BTCAmt(change_amt))
else:
msg('Warning: Change address will be deleted as transaction produces no change')
tx.del_output(chg_idx)
if not tx.send_amt:
tx.send_amt = change_amt
dmsg('tx: %s' % tx)
tx.add_comment() # edits an existing comment
tx.create_raw(c) # creates tx.hex, tx.txid
if not opt.yes:
tx.add_comment() # edits an existing comment
tx.create_raw(c) # creates tx.hex, tx.txid
tx.add_mmaddrs_to_outputs(ad_w,ad_f)
tx.add_timestamp()
tx.add_blockcount(c)
assert tx.get_fee() <= g.max_tx_fee
qmsg('Transaction successfully created')
dmsg('TX (final): %s' % tx)
tx.view_with_prompt('View decoded transaction?')
if not opt.yes:
tx.view_with_prompt('View decoded transaction?')
return tx

View file

@ -657,7 +657,7 @@ def do_pager(text):
def do_license_msg(immed=False):
if opt.quiet or g.no_license: return
if opt.quiet or g.no_license or opt.yes: return
import mmgen.license as gpl

View file

@ -100,6 +100,7 @@ setup(
'mmgen.main_addrgen',
'mmgen.main_addrimport',
'mmgen.main_txcreate',
'mmgen.main_txbump',
'mmgen.main_txsign',
'mmgen.main_txsend',
'mmgen.main_txdo',
@ -119,6 +120,7 @@ setup(
'mmgen-walletconv',
'mmgen-walletgen',
'mmgen-txcreate',
'mmgen-txbump',
'mmgen-txsign',
'mmgen-txsend',
'mmgen-txdo',

View file

@ -200,6 +200,7 @@ cfgs = {
'mmdat': 'walletgen',
'addrs': 'addrgen',
'rawtx': 'txcreate',
'txbump': 'txbump',
'sigtx': 'txsign',
'mmwords': 'export_mnemonic',
'mmseed': 'export_seed',
@ -246,6 +247,7 @@ cfgs = {
'addrs': 'addrgen4',
'rawtx': 'txcreate4',
'sigtx': 'txsign4',
'txdo': 'txdo4',
},
'bw_filename': 'brainwallet.mmbrain',
'bw_params': '192,1',
@ -403,7 +405,8 @@ cmd_group['main'] = OrderedDict([
['addrgen', (1,'address generation', [[['mmdat',pwfile],1]],1)],
['addrimport', (1,'address import', [[['addrs'],1]],1)],
['txcreate', (1,'transaction creation', [[['addrs'],1]],1)],
['txsign', (1,'transaction signing', [[['mmdat','rawtx',pwfile],1]],1)],
['txbump', (1,'transaction fee bumping (no send)',[[['rawtx'],1]],1)],
['txsign', (1,'transaction signing', [[['mmdat','rawtx',pwfile,'txbump'],1]],1)],
['txsend', (1,'transaction sending', [[['sigtx'],1]])],
# txdo must go after txsign
['txdo', (1,'online transaction', [[['sigtx','mmdat'],1]])],
@ -444,6 +447,7 @@ cmd_group['main'] = OrderedDict([
['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4],[['addrs','akeys.mmenc'],14]])],
['txsign4', (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet, brainwallet, key-address file and non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4],[['akeys.mmenc'],14]])],
['txdo4', (4,'tx creation,signing and sending with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4],[['addrs','akeys.mmenc'],14],[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4],[['akeys.mmenc'],14]])], # must go after txsign4
['txbump4', (4,'tx fee bump + send with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['akeys.mmenc'],14],[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['akeys.mmenc'],14],[['mmbrain','sigtx','mmdat','txdo'],4]])], # must go after txsign4
])
cmd_group['tool'] = OrderedDict([
@ -1415,7 +1419,7 @@ class MMGenTestSuite(object):
bwd_msg = 'MMGEN_BOGUS_WALLET_DATA=%s' % unspent_data_file
if opt.print_cmdline: msg(bwd_msg)
if opt.log: log_fd.write(bwd_msg + ' ')
t = MMGenExpect(name,'mmgen-'+('txcreate','txdo')[bool(txdo_args)],['-f','0.0001'] + add_args + cmd_args + txdo_args)
t = MMGenExpect(name,'mmgen-'+('txcreate','txdo')[bool(txdo_args)],['--rbf','-f',tx_fee] + add_args + cmd_args + txdo_args)
if ia: return
t.license()
@ -1462,6 +1466,31 @@ class MMGenTestSuite(object):
def txcreate(self,name,addrfile):
self.txcreate_common(name,sources=['1'])
def txbump(self,name,txfile,prepend_args=[],seed_args=[]):
args = prepend_args + ['-q','-d',cfg['tmpdir'],txfile] + seed_args
t = MMGenExpect(name,'mmgen-txbump',args)
if seed_args:
t.hash_preset('key-address data','1')
t.passphrase('key-address data',cfgs['14']['kapasswd'])
t.expect('Check key-to-address validity? (y/N): ','y')
t.expect('Which output do you wish to deduct the fee from? ','1\n')
# Fee must be > tx_fee + network relay fee (currently 0.00001)
t.expect('OK? (Y/n): ','\n')
t.expect('Enter transaction fee: ','124s\n')
t.expect('OK? (Y/n): ','\n')
if seed_args: # sign and send
t.expect('Edit transaction comment? (y/N): ','\n')
for cnum,desc in ('1','incognito data'),('3','MMGen wallet'),('4','MMGen wallet'):
t.passphrase(('%s' % desc),cfgs[cnum]['wpasswd'])
t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
else:
t.expect('Add a comment to transaction? (y/N): ','\n')
t.expect('Save transaction? (y/N): ','y')
t.written_to_file('Transaction')
os.unlink(txfile) # our tx file replaces the original
os.system('touch ' + os.path.join(cfg['tmpdir'],'txbump'))
ok()
def txdo(self,name,addrfile,wallet):
t = self.txcreate_common(name,sources=['1'],txdo_args=[wallet])
self.txsign(name,'','',pf='',save=True,has_label=False,txdo_handle=t)
@ -1478,7 +1507,7 @@ class MMGenTestSuite(object):
add = ' #' + tnum if tnum else ''
t.written_to_file('Signed transaction' + add, oo=True)
def txsign(self,name,txfile,wf,pf='',save=True,has_label=False,txdo_handle=None):
def txsign(self,name,txfile,wf,pf='',bumpf='',save=True,has_label=False,txdo_handle=None):
add_args = ([],['-q','-P',pf])[ia]
if ia:
m = '\nAnswer the interactive prompts as follows:\n ENTER, ENTER, ENTER'
@ -1516,7 +1545,7 @@ class MMGenTestSuite(object):
m = 'YES, I REALLY WANT TO DO THIS'
t.expect("'%s' to confirm: " % m,m+'\n')
t.expect('BOGUS transaction NOT sent')
t.written_to_file('Transaction ID')
t.written_to_file('Sent transaction')
ok()
def walletconv_export(self,name,wf,desc,uargs=[],out_fmt='w',pf=None,out_pw=False):
@ -1719,8 +1748,14 @@ class MMGenTestSuite(object):
non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn)
add_args = ['-d',cfg['tmpdir'],'-i','brain','-b'+cfg['bw_params'],'-p1','-k',non_mm_fn,'-M',f12]
t = self.txcreate_common(name,sources=['1','2','3','4','14'],non_mmgen_input='4',do_label=1,txdo_args=[f7,f8,f9,f10],add_args=add_args)
os.system('rm -f %s/*.sigtx' % cfg['tmpdir'])
self.txsign4(name,f7,f8,f9,f10,f11,f12,txdo_handle=t)
self.txsend(name,'',txdo_handle=t)
os.system('touch ' + os.path.join(cfg['tmpdir'],'txdo'))
def txbump4(self,name,f1,f2,f3,f4,f5,f6,f7,f8,f9): # f7:txfile,f9:'txdo'
non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn)
self.txbump(name,f7,prepend_args=['-p1','-k',non_mm_fn,'-M',f1],seed_args=[f2,f3,f4,f5,f6,f8])
def txsign4(self,name,f1,f2,f3,f4,f5,f6,txdo_handle=None):
if txdo_handle: