[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:
parent
8556de2101
commit
93c99755a7
16 changed files with 553 additions and 151 deletions
|
|
@ -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
25
mmgen-txbump
Executable 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")
|
||||
|
|
@ -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
133
mmgen/main_txbump.py
Executable 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
247
mmgen/tx.py
247
mmgen/tx.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -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',
|
||||
|
|
|
|||
43
test/test.py
43
test/test.py
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue