B2X support, locktime-based coin splitting utility
- full support for B2X fork - `mmgen-split` coin splitting utility for replayable forks - `mmgen-regtest fork` command for testing forking scenarios - `regtest_split` command added to `test/test.py` test suite - timelock support for `txcreate` and `txdo` - nlocktime and nsequence checks after signing and before sending
This commit is contained in:
parent
207632cb40
commit
420d0e9699
18 changed files with 709 additions and 152 deletions
|
|
@ -312,6 +312,7 @@ def set_led(cmd):
|
|||
led_thread.start()
|
||||
|
||||
def get_insert_status():
|
||||
if os.getenv('MMGEN_TEST_SUITE'): return True
|
||||
try: os.stat(os.path.join('/dev/disk/by-label/',part_label))
|
||||
except: return False
|
||||
else: return True
|
||||
|
|
|
|||
24
cmds/mmgen-split
Executable file
24
cmds/mmgen-split
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
mmgen-split: Split funds after a fork using a timelocked transaction
|
||||
"""
|
||||
|
||||
from mmgen.main import launch
|
||||
launch("split")
|
||||
|
|
@ -40,32 +40,41 @@ opts_data = lambda: {
|
|||
|
||||
AVAILABLE COMMANDS
|
||||
|
||||
setup - set up system for regtest operation with MMGen
|
||||
stop - stop the regtest coin daemon
|
||||
bob - switch to Bob's wallet, starting daemon if necessary
|
||||
alice - switch to Alice's wallet, starting daemon if necessary
|
||||
user - show current user
|
||||
generate - mine a block
|
||||
send ADDR AMT - send amount AMT to address ADDR
|
||||
test_daemon - test whether daemon is running
|
||||
get_balances - get balances of Bob and Alice
|
||||
show_mempool - show transaction IDs in mempool
|
||||
setup - set up system for regtest operation with MMGen
|
||||
fork COIN - create a fork of coin COIN
|
||||
stop - stop the regtest coin daemon
|
||||
bob - switch to Bob's wallet, starting daemon if necessary
|
||||
alice - switch to Alice's wallet, starting daemon if necessary
|
||||
user - show current user
|
||||
generate N - mine n blocks (defaults to 1)
|
||||
send ADDR AMT - send amount AMT of miner funds to address ADDR
|
||||
test_daemon - test whether daemon is running
|
||||
get_balances - get balances of Bob and Alice
|
||||
show_mempool - show transaction IDs in mempool
|
||||
cli [arguments] - execute an RPC call with arguments
|
||||
"""
|
||||
}
|
||||
|
||||
cmd_args = opts.init(opts_data)
|
||||
|
||||
cmds = ('setup','stop','generate','test_daemon','create_data_dir','bob','alice','miner','user','send',
|
||||
'wait_for_daemon','wait_for_exit','get_current_user','get_balances','show_mempool')
|
||||
'wait_for_daemon','wait_for_exit','get_current_user','get_balances','show_mempool','cli','fork')
|
||||
|
||||
try:
|
||||
if cmd_args[0] == 'send':
|
||||
assert len(cmd_args) == 3
|
||||
elif cmd_args[0] == 'fork':
|
||||
assert len(cmd_args) == 2
|
||||
elif cmd_args[0] == 'generate':
|
||||
assert len(cmd_args) in (1,2)
|
||||
if len(cmd_args) == 2:
|
||||
cmd_args[1] = int(cmd_args[1])
|
||||
elif cmd_args[0] == 'cli':
|
||||
pass
|
||||
else:
|
||||
assert cmd_args[0] in cmds and len(cmd_args) == 1
|
||||
except:
|
||||
opts.usage()
|
||||
else:
|
||||
args = cmd_args[1:]
|
||||
from mmgen.regtest import *
|
||||
globals()[cmd_args[0]](*args)
|
||||
globals()[cmd_args[0]](*cmd_args[1:])
|
||||
|
|
|
|||
139
mmgen/main_split.py
Executable file
139
mmgen/main_split.py
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||
# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# TODO: check that balances of output addrs are zero?
|
||||
|
||||
"""
|
||||
mmgen-split: Split funds after a replayable chain fork using a timelocked transaction
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from mmgen.common import *
|
||||
|
||||
opts_data = lambda: {
|
||||
'desc': """Split funds in a {pnm} wallet after a chain fork using a
|
||||
timelocked transaction""".format(pnm=g.proj_name),
|
||||
'usage':'[opts] [output addr1] [output addr2]',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
-f, --tx-fees= f The transaction fees for each chain (comma-separated)
|
||||
-c, --other-coin= c The coin symbol of the other chain (default: {oc})
|
||||
-B, --no-blank Don't blank screen before displaying unspent outputs
|
||||
-d, --outdir= d Specify an alternate directory 'd' for output
|
||||
-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
|
||||
-R, --rpc-host2= h Host the other coin daemon is running on (default: none)
|
||||
-L, --locktime= t Lock time (block height or unix seconds)
|
||||
(default: {bh})
|
||||
""".format(oc=g.proto.forks[-1][2].upper(),bh='current block height'),
|
||||
'notes': """\n
|
||||
This command creates two transactions: one (with the timelock) to be broadcast
|
||||
on the long chain and one on the short chain after a replayable chain fork.
|
||||
Only {pnm} addresses may be spent to.
|
||||
|
||||
The command must be run on the longest chain. The user is reponsible for
|
||||
ensuring that the current chain is the longest. The other chain is specified
|
||||
on the command line, or it defaults to the most recent replayable fork of the
|
||||
current chain.
|
||||
|
||||
For the split to have a reasonable chance of succeeding, the long chain should
|
||||
be well ahead of the short one (by more than 20 blocks or so) and transactions
|
||||
should have a good chance of confirming quickly on both chains. For this
|
||||
larger than normal fees may be required. Fees may be specified on the command
|
||||
line, or network fee estimation may be used.
|
||||
|
||||
If the split fails (i.e. the long-chain TX is broadcast and confirmed on the
|
||||
short chain), no funds are lost. A new split attempt can be made with the
|
||||
long-chain transaction's output as an input for the new split transaction.
|
||||
This process can be repeated as necessary until the split succeeds.
|
||||
|
||||
IMPORTANT: Timelock replay protection offers NO PROTECTION against reorg
|
||||
attacks on the majority chain or reorg attacks on the minority chain if the
|
||||
minority chain is ahead of the timelock. If the reorg'd minority chain is
|
||||
behind the timelock, protection is contingent on getting the non-timelocked
|
||||
transaction reconfirmed before the timelock expires. Use at your own risk.
|
||||
""".format(pnm=g.proj_name)
|
||||
}
|
||||
|
||||
cmd_args = opts.init(opts_data,add_opts=['tx_fee','tx_fee_adj','comment_file'])
|
||||
|
||||
opt.other_coin = opt.other_coin.upper() if opt.other_coin else g.proto.forks[-1][2].upper()
|
||||
if opt.other_coin.lower() not in [e[2] for e in g.proto.forks if e[3] == True]:
|
||||
die(1,"'{}': not a replayable fork of {} chain".format(opt.other_coin,g.coin))
|
||||
|
||||
if len(cmd_args) != 2:
|
||||
fs = 'This command requires exactly two {} addresses as arguments'
|
||||
die(1,fs.format(g.proj_name))
|
||||
|
||||
from mmgen.obj import MMGenID
|
||||
try:
|
||||
mmids = [MMGenID(a,on_fail='die') for a in cmd_args]
|
||||
except:
|
||||
die(1,'Command line arguments must be valid MMGen IDs')
|
||||
|
||||
if mmids[0] == mmids[1]:
|
||||
die(2,'Both transactions have the same output! ({})'.format(mmids[0]))
|
||||
|
||||
from mmgen.tx import MMGenSplitTX
|
||||
from mmgen.protocol import CoinProtocol
|
||||
|
||||
if opt.tx_fees:
|
||||
g_coin_save = g.coin
|
||||
for idx,g_coin in ((1,opt.other_coin),(0,g_coin_save)):
|
||||
g.coin = g_coin
|
||||
g.proto = CoinProtocol(g.coin,g.testnet)
|
||||
opt.tx_fee = opt.tx_fees.split(',')[idx]
|
||||
opts.opt_is_tx_fee(opt.tx_fee,'transaction fee') or sys.exit(1)
|
||||
|
||||
rpc_init(reinit=True)
|
||||
|
||||
tx1 = MMGenSplitTX()
|
||||
opt.no_blank = True
|
||||
|
||||
gmsg("Creating timelocked transaction for long chain ({})".format(g.coin))
|
||||
locktime = int(opt.locktime or 0) or g.rpch.getblockcount()
|
||||
tx1.create(mmids[0],locktime)
|
||||
|
||||
tx1.format()
|
||||
tx1.create_fn()
|
||||
|
||||
gmsg("\nCreating transaction for short chain ({})".format(opt.other_coin))
|
||||
|
||||
if opt.rpc_host2: g.rpc_host = opt.rpc_host2
|
||||
if opt.tx_fees: opt.tx_fee = opt.tx_fees.split(',')[1]
|
||||
|
||||
from mmgen.protocol import CoinProtocol
|
||||
g.coin = opt.other_coin
|
||||
g.proto = CoinProtocol(g.coin,g.testnet)
|
||||
reload(sys.modules['mmgen.tx'])
|
||||
|
||||
tx2 = MMGenSplitTX()
|
||||
tx2.inputs = tx1.inputs
|
||||
tx2.inputs.convert_coin()
|
||||
|
||||
tx2.create_split(mmids[1])
|
||||
|
||||
for tx,desc in ((tx1,'Long chain (timelocked)'),(tx2,'Short chain')):
|
||||
tx.desc = desc + ' transaction'
|
||||
tx.write_to_file(ask_write=False,ask_overwrite=not opt.yes,ask_write_default_yes=False)
|
||||
|
|
@ -133,7 +133,7 @@ if not silent:
|
|||
if seed_files or kl or kal:
|
||||
txsign(tx,seed_files,kl,kal)
|
||||
tx.write_to_file(ask_write=False)
|
||||
if tx.send():
|
||||
tx.write_to_file(ask_write=False)
|
||||
tx.send(exit_on_fail=True)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ opts_data = lambda: {
|
|||
per byte (an integer followed by 's'). If omitted, fee
|
||||
will be calculated using {dn}'s 'estimatefee' call
|
||||
-i, --info Display unspent outputs and exit
|
||||
-L, --locktime= t Lock time (block height or unix seconds) (default: 0)
|
||||
-m, --minconf= n Minimum number of confirmations required to spend
|
||||
outputs (default: 1)
|
||||
-q, --quiet Suppress warnings; overwrite files without prompting
|
||||
|
|
@ -55,5 +56,5 @@ rpc_init()
|
|||
|
||||
from mmgen.tx import MMGenTX
|
||||
tx = MMGenTX()
|
||||
tx.create(cmd_args,do_info=opt.info)
|
||||
tx.create(cmd_args,int(opt.locktime or 0),do_info=opt.info)
|
||||
tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ opts_data = lambda: {
|
|||
-K, --key-generator= m Use method 'm' for public key generation
|
||||
Options: {kgs}
|
||||
(default: {kg})
|
||||
-L, --locktime= t Lock time (block height or unix seconds) (default: 0)
|
||||
-m, --minconf=n Minimum number of confirmations required to spend
|
||||
outputs (default: 1)
|
||||
-M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key-
|
||||
|
|
@ -89,9 +90,9 @@ kl = get_keylist(opt)
|
|||
if kl and kal: kl.remove_dup_keys(kal)
|
||||
|
||||
tx = MMGenTX(caller='txdo')
|
||||
tx.create(cmd_args)
|
||||
tx.create(cmd_args,int(opt.locktime or 0))
|
||||
txsign(tx,seed_files,kl,kal)
|
||||
tx.write_to_file(ask_write=False)
|
||||
|
||||
if tx.send():
|
||||
tx.write_to_file(ask_overwrite=False,ask_write=False)
|
||||
tx.send(exit_on_fail=True)
|
||||
tx.write_to_file(ask_overwrite=False,ask_write=False)
|
||||
|
|
|
|||
|
|
@ -64,5 +64,5 @@ if not opt.yes:
|
|||
if tx.add_comment(): # edits an existing comment, returns true if changed
|
||||
tx.write_to_file(ask_write_default_yes=True)
|
||||
|
||||
if tx.send():
|
||||
tx.write_to_file(ask_overwrite=False,ask_write=False)
|
||||
tx.send(exit_on_fail=True)
|
||||
tx.write_to_file(ask_overwrite=False,ask_write=False)
|
||||
|
|
|
|||
|
|
@ -253,7 +253,6 @@ def init(opts_f,add_opts=[],opt_filter=None):
|
|||
setattr(opt,'set_by_user',[])
|
||||
for k in g.global_sets_opt:
|
||||
if k in opt.__dict__ and getattr(opt,k) != None:
|
||||
# _typeconvert_from_dfl(k)
|
||||
setattr(opt,k,set_for_type(getattr(opt,k),getattr(g,k),'--'+k))
|
||||
opt.set_by_user.append(k)
|
||||
else:
|
||||
|
|
@ -277,7 +276,7 @@ def init(opts_f,add_opts=[],opt_filter=None):
|
|||
if g.bob or g.alice:
|
||||
g.testnet = True
|
||||
g.proto = CoinProtocol(g.coin,g.testnet)
|
||||
g.data_dir = os.path.join(g.data_dir_root,'regtest',('alice','bob')[g.bob])
|
||||
g.data_dir = os.path.join(g.data_dir_root,'regtest',g.coin.lower(),('alice','bob')[g.bob])
|
||||
check_or_create_dir(g.data_dir)
|
||||
import regtest as rt
|
||||
g.rpc_host = 'localhost'
|
||||
|
|
@ -303,6 +302,19 @@ def init(opts_f,add_opts=[],opt_filter=None):
|
|||
|
||||
return args
|
||||
|
||||
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 {} amount or satoshis-per-byte specification)".format(
|
||||
val,desc,g.coin.upper()))
|
||||
elif ret != None and ret > g.proto.max_tx_fee:
|
||||
msg("'{}': invalid {} (> max_tx_fee ({} {}))".format(
|
||||
val,desc,g.proto.max_tx_fee,g.coin.upper()))
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_opts(usr_opts): # Returns false if any check fails
|
||||
|
||||
def opt_splits(val,sep,n,desc):
|
||||
|
|
@ -332,19 +344,6 @@ 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 {} amount or satoshis-per-byte specification)".format(
|
||||
val,desc,g.coin.upper()))
|
||||
elif ret != None and ret > g.proto.max_tx_fee:
|
||||
msg("'{}': invalid {} (> max_tx_fee ({} {}))".format(
|
||||
val,desc,g.proto.max_tx_fee,g.coin.upper()))
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
def opt_is_in_list(val,lst,desc):
|
||||
if val not in lst:
|
||||
q,sep = (('',','),("'","','"))[type(lst[0]) == str]
|
||||
|
|
@ -461,6 +460,9 @@ def check_opts(usr_opts): # Returns false if any check fails
|
|||
m = "Regtest (Bob and Alice) mode not set up yet. Run '{}-regtest setup' to initialize."
|
||||
try: os.stat(daemon_dir)
|
||||
except: die(1,m.format(g.proj_name.lower()))
|
||||
elif key == 'locktime':
|
||||
if not opt_is_int(val,desc): return False
|
||||
if not opt_compares(val,'>',0,desc): return False
|
||||
else:
|
||||
if g.debug: Msg("check_opts(): No test for opt '%s'" % key)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ protocol.py: Coin protocol functions, classes and methods
|
|||
import os,hashlib
|
||||
from binascii import unhexlify
|
||||
from mmgen.util import msg,pmsg
|
||||
from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt
|
||||
from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt,B2XAmt
|
||||
from mmgen.globalvars import g
|
||||
|
||||
def hash160(hexnum): # take hex, return hex - OP_HASH160
|
||||
|
|
@ -69,9 +69,9 @@ class BitcoinProtocol(MMGenObject):
|
|||
daemon_data_subdir = ''
|
||||
sighash_type = 'ALL'
|
||||
block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
|
||||
forks = [
|
||||
(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','bch'),
|
||||
(None,'','b2x')
|
||||
forks = [ # height, hash, name, replayable
|
||||
(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','bch',False),
|
||||
(None,'','b2x',True)
|
||||
]
|
||||
caps = ('rbf','segwit')
|
||||
base_coin = 'BTC'
|
||||
|
|
@ -160,7 +160,7 @@ class BitcoinCashProtocol(BitcoinProtocol):
|
|||
mmtypes = ('L','C')
|
||||
sighash_type = 'ALL|FORKID'
|
||||
forks = [
|
||||
(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','btc')
|
||||
(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','btc',False)
|
||||
]
|
||||
caps = ()
|
||||
coin_amt = BCHAmt
|
||||
|
|
@ -178,6 +178,24 @@ class BitcoinCashTestnetProtocol(BitcoinCashProtocol):
|
|||
data_subdir = 'testnet'
|
||||
daemon_data_subdir = 'testnet3'
|
||||
|
||||
class B2XProtocol(BitcoinProtocol):
|
||||
daemon_name = 'bitcoind-2x'
|
||||
daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Bitcoin_2X') if g.platform == 'win' \
|
||||
else os.path.join(g.home_dir,'.bitcoin-2x')
|
||||
rpc_port = 8338
|
||||
coin_amt = B2XAmt
|
||||
max_tx_fee = B2XAmt('0.1')
|
||||
forks = [
|
||||
(None,'','btc',True)
|
||||
]
|
||||
|
||||
class B2XTestnetProtocol(B2XProtocol):
|
||||
addr_ver_num = { 'p2pkh': ('6f','mn'), 'p2sh': ('c4','2') }
|
||||
privkey_pfx = 'ef'
|
||||
data_subdir = 'testnet'
|
||||
daemon_data_subdir = 'testnet5'
|
||||
rpc_port = 18338
|
||||
|
||||
class LitecoinProtocol(BitcoinProtocol):
|
||||
block0 = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2'
|
||||
name = 'litecoin'
|
||||
|
|
@ -210,6 +228,7 @@ class CoinProtocol(MMGenObject):
|
|||
coins = {
|
||||
'btc': (BitcoinProtocol,BitcoinTestnetProtocol),
|
||||
'bch': (BitcoinCashProtocol,BitcoinCashTestnetProtocol),
|
||||
'b2x': (B2XProtocol,B2XTestnetProtocol),
|
||||
'ltc': (LitecoinProtocol,LitecoinTestnetProtocol),
|
||||
# 'eth': (EthereumProtocol,EthereumTestnetProtocol),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,38 +24,40 @@ import os,subprocess,time,shutil
|
|||
from mmgen.common import *
|
||||
PIPE = subprocess.PIPE
|
||||
|
||||
data_dir = os.path.join(g.data_dir_root,'regtest')
|
||||
data_dir = os.path.join(g.data_dir_root,'regtest',g.coin.lower())
|
||||
daemon_dir = os.path.join(data_dir,'regtest')
|
||||
rpc_port = 8552
|
||||
rpc_ports = { 'btc':8552, 'bch':8553, 'b2x':8554, 'ltc':8555 }
|
||||
rpc_port = rpc_ports[g.coin.lower()]
|
||||
rpc_user = 'bobandalice'
|
||||
rpc_password = 'hodltothemoon'
|
||||
init_amt = 500
|
||||
|
||||
tr_wallet = lambda user: os.path.join(daemon_dir,'wallet.dat.'+user)
|
||||
|
||||
common_args = (
|
||||
common_args = lambda: (
|
||||
'-rpcuser={}'.format(rpc_user),
|
||||
'-rpcpassword={}'.format(rpc_password),
|
||||
'-rpcport={}'.format(rpc_port),
|
||||
'-regtest',
|
||||
'-datadir={}'.format(data_dir))
|
||||
|
||||
def start_daemon(user,quiet=False,daemon=True):
|
||||
def start_daemon(user,quiet=False,daemon=True,reindex=False):
|
||||
cmd = (
|
||||
g.proto.daemon_name,
|
||||
'-listen=0',
|
||||
'-keypool=1',
|
||||
'-wallet={}'.format(os.path.basename(tr_wallet(user)))
|
||||
) + common_args
|
||||
) + common_args()
|
||||
if daemon: cmd += ('-daemon',)
|
||||
if reindex: cmd += ('-reindex',)
|
||||
if not g.debug or quiet: vmsg('{}'.format(' '.join(cmd)))
|
||||
p = subprocess.Popen(cmd,stdout=PIPE,stderr=PIPE)
|
||||
err = process_output(p,silent=False)[1]
|
||||
if err:
|
||||
rdie(1,'Error starting the {} daemon:\n{}'.format(g.proto.name.capitalize(),err))
|
||||
|
||||
def start_daemon_mswin(user,quiet=False):
|
||||
def start_daemon_mswin(user,quiet=False,reindex=False):
|
||||
import threading
|
||||
t = threading.Thread(target=start_daemon,args=[user,quiet,False])
|
||||
t = threading.Thread(target=start_daemon,args=[user,quiet,False,reindex])
|
||||
t.daemon = True
|
||||
t.start()
|
||||
if not opt.verbose: Msg_r(' \b') # blocks w/o this...crazy
|
||||
|
|
@ -63,7 +65,7 @@ def start_daemon_mswin(user,quiet=False):
|
|||
def start_cmd(*args,**kwargs):
|
||||
cmd = args
|
||||
if args[0] == 'cli':
|
||||
cmd = (g.proto.name+'-cli',) + common_args + args[1:]
|
||||
cmd = (g.proto.name+'-cli',) + common_args() + args[1:]
|
||||
if g.debug or not 'quiet' in kwargs:
|
||||
vmsg('{}'.format(' '.join(cmd)))
|
||||
ip = op = ep = (PIPE,None)['no_pipe' in kwargs and kwargs['no_pipe']]
|
||||
|
|
@ -113,15 +115,16 @@ def get_balances():
|
|||
msg('{:<16} {:12}'.format('Total balance:',tbal))
|
||||
|
||||
def create_data_dir():
|
||||
try: os.stat(daemon_dir)
|
||||
try: os.stat(os.path.join(data_dir,'regtest')) # don't use daemon_dir, as data_dir may change
|
||||
except: pass
|
||||
else:
|
||||
if keypress_confirm('Delete your existing MMGen regtest setup and create a new one?'):
|
||||
m = "Delete your existing MMGen regtest setup at '{}' and create a new one?"
|
||||
if keypress_confirm(m.format(data_dir)):
|
||||
shutil.rmtree(data_dir)
|
||||
else:
|
||||
die()
|
||||
|
||||
try: os.mkdir(data_dir)
|
||||
try: os.makedirs(data_dir)
|
||||
except: pass
|
||||
|
||||
def process_output(p,silent=False):
|
||||
|
|
@ -133,9 +136,9 @@ def process_output(p,silent=False):
|
|||
vmsg('stderr: [{}]'.format(err.strip()))
|
||||
return out,err
|
||||
|
||||
def start_and_wait(user,silent=False,nonl=False):
|
||||
def start_and_wait(user,silent=False,nonl=False,reindex=False):
|
||||
vmsg('Starting {} regtest daemon'.format(g.proto.name))
|
||||
(start_daemon_mswin,start_daemon)[g.platform=='linux'](user)
|
||||
(start_daemon_mswin,start_daemon)[g.platform=='linux'](user,reindex=reindex)
|
||||
wait_for_daemon('ready',silent=silent,nonl=nonl)
|
||||
|
||||
def stop_and_wait(silent=False,nonl=False,stop_silent=False,ignore_noconnect_error=False):
|
||||
|
|
@ -156,9 +159,53 @@ def show_mempool():
|
|||
msg(pformat(eval(p.stdout.read())))
|
||||
p.wait()
|
||||
|
||||
def setup():
|
||||
try: os.mkdir(data_dir)
|
||||
def cli(*args):
|
||||
p = start_cmd(*(('cli',) + args))
|
||||
from pprint import pformat
|
||||
Msg_r(p.stdout.read())
|
||||
msg_r(p.stderr.read())
|
||||
p.wait()
|
||||
|
||||
def fork(coin):
|
||||
coin = coin.upper()
|
||||
from mmgen.protocol import CoinProtocol
|
||||
forks = CoinProtocol(coin,False).forks
|
||||
if not [f for f in forks if f[2] == g.coin.lower() and f[3] == True]:
|
||||
die(1,"Coin {} is not a replayable fork of coin {}".format(g.coin,coin))
|
||||
|
||||
gmsg('Creating fork from coin {} to coin {}'.format(coin,g.coin))
|
||||
source_data_dir = os.path.join(g.data_dir_root,'regtest',coin.lower())
|
||||
|
||||
try: os.stat(source_data_dir)
|
||||
except: die(1,"Source directory '{}' does not exist!".format(source_data_dir))
|
||||
|
||||
# stop the other daemon
|
||||
global rpc_port,data_dir
|
||||
rpc_port_save,data_dir_save = rpc_port,data_dir
|
||||
rpc_port = rpc_ports[coin.lower()]
|
||||
data_dir = os.path.join(g.data_dir_root,'regtest',coin.lower())
|
||||
if test_daemon() != 'stopped':
|
||||
stop_and_wait(silent=True,stop_silent=True)
|
||||
rpc_port,data_dir = rpc_port_save,data_dir_save
|
||||
|
||||
try: os.makedirs(data_dir)
|
||||
except: pass
|
||||
|
||||
# stop our daemon
|
||||
if test_daemon() != 'stopped':
|
||||
stop_and_wait(silent=True,stop_silent=True)
|
||||
|
||||
create_data_dir()
|
||||
os.rmdir(data_dir)
|
||||
shutil.copytree(source_data_dir,data_dir,symlinks=True)
|
||||
start_and_wait('miner',reindex=True,silent=True)
|
||||
stop_and_wait(silent=True,stop_silent=True)
|
||||
gmsg('Fork {} successfully created'.format(g.coin))
|
||||
|
||||
def setup():
|
||||
try: os.makedirs(data_dir)
|
||||
except: pass
|
||||
|
||||
if test_daemon() != 'stopped':
|
||||
stop_and_wait(silent=True,stop_silent=True)
|
||||
create_data_dir()
|
||||
|
|
@ -192,7 +239,7 @@ def get_current_user_win(quiet=False):
|
|||
return None
|
||||
|
||||
def get_current_user_unix(quiet=False):
|
||||
p = start_cmd('pgrep','-af','{}.*-rpcuser={}.*'.format(g.proto.daemon_name,rpc_user))
|
||||
p = start_cmd('pgrep','-af','{}.*-rpcport={}.*'.format(g.proto.daemon_name,rpc_port))
|
||||
cmdline = p.stdout.read()
|
||||
if not cmdline: return None
|
||||
for k in ('miner','bob','alice'):
|
||||
|
|
@ -214,20 +261,20 @@ def user(user=None,quiet=False):
|
|||
wait_for_daemon('ready')
|
||||
if test_daemon() == 'ready':
|
||||
if user == get_current_user(quiet=True):
|
||||
if not quiet: msg('{} is already the current user'.format(user.capitalize()))
|
||||
if not quiet: msg('{} is already the current user for coin {}'.format(user.capitalize(),g.coin))
|
||||
return True
|
||||
gmsg_r('Switching to user {}'.format(user.capitalize()))
|
||||
gmsg_r('Switching to user {} for coin {}'.format(user.capitalize(),g.coin))
|
||||
stop_and_wait(silent=False,nonl=True,stop_silent=True)
|
||||
time.sleep(0.1) # file lock has race condition - TODO: test for lock file
|
||||
start_and_wait(user,nonl=True)
|
||||
else:
|
||||
gmsg_r('Starting regtest daemon with current user {}'.format(user.capitalize()))
|
||||
gmsg_r('Starting regtest daemon for coin {} with current user {}'.format(g.coin,user.capitalize()))
|
||||
start_and_wait(user,nonl=True)
|
||||
gmsg('done')
|
||||
|
||||
def stop(silent=False,ignore_noconnect_error=True):
|
||||
if test_daemon() != 'stopped' and not silent:
|
||||
gmsg('Stopping {} regtest daemon'.format(g.proto.name))
|
||||
gmsg('Stopping {} regtest daemon for coin {}'.format(g.proto.name,g.coin))
|
||||
p = start_cmd('cli','stop')
|
||||
err = process_output(p)[1]
|
||||
if err:
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class CoinDaemonRPCConnection(object):
|
|||
|
||||
import socket
|
||||
try:
|
||||
socket.create_connection((host,port)).close()
|
||||
socket.create_connection((host,port),timeout=3).close()
|
||||
except:
|
||||
die(1,'Unable to connect to {}:{}'.format(host,port))
|
||||
|
||||
|
|
|
|||
231
mmgen/tx.py
231
mmgen/tx.py
|
|
@ -60,6 +60,25 @@ only one output, specify a single output address with no {} amount
|
|||
""".strip().format(g.coin),
|
||||
}
|
||||
|
||||
def strfmt_locktime(num,terse=False):
|
||||
# Locktime itself is an unsigned 4-byte integer which can be parsed two ways:
|
||||
#
|
||||
# If less than 500 million, locktime is parsed as a block height. The transaction can be
|
||||
# added to any block which has this height or higher.
|
||||
# MMGen note: s/this height or higher/a higher block height/
|
||||
#
|
||||
# If greater than or equal to 500 million, locktime is parsed using the Unix epoch time
|
||||
# format (the number of seconds elapsed since 1970-01-01T00:00 UTC). The transaction can be
|
||||
# added to any block whose block time is greater than the locktime.
|
||||
if num >= 5 * 10**6:
|
||||
return ' '.join(time.strftime('%c',time.gmtime(num)).split()[1:])
|
||||
elif num > 0:
|
||||
return '{}{}'.format(('block height ','')[terse],num)
|
||||
elif num == None:
|
||||
return '(None)'
|
||||
else:
|
||||
die(2,"'{}': invalid locktime value!".format(num))
|
||||
|
||||
def select_unspent(unspent,prompt):
|
||||
while True:
|
||||
reply = my_raw_input(prompt).strip()
|
||||
|
|
@ -258,10 +277,12 @@ class MMGenTX(MMGenObject):
|
|||
self.timestamp = ''
|
||||
self.chksum = ''
|
||||
self.fmt_data = ''
|
||||
self.fn = ''
|
||||
self.blockcount = 0
|
||||
self.chain = None
|
||||
self.coin = None
|
||||
self.caller = caller
|
||||
self.locktime = None
|
||||
|
||||
if filename:
|
||||
self.parse_tx_file(filename,md_only=md_only)
|
||||
|
|
@ -276,7 +297,7 @@ class MMGenTX(MMGenObject):
|
|||
die(2,'Transaction is for {}, but current chain is {}!'.format(self.chain,g.chain))
|
||||
|
||||
def add_output(self,coinaddr,amt,is_chg=None):
|
||||
self.outputs.append(self.MMGenTxOutput(addr=coinaddr,amt=amt,is_chg=is_chg))
|
||||
self.outputs.append(MMGenTX.MMGenTxOutput(addr=coinaddr,amt=amt,is_chg=is_chg))
|
||||
|
||||
def get_chg_output_idx(self):
|
||||
for i in range(len(self.outputs)):
|
||||
|
|
@ -287,7 +308,7 @@ class MMGenTX(MMGenObject):
|
|||
def update_output_amt(self,idx,amt):
|
||||
o = self.outputs[idx].__dict__
|
||||
o['amt'] = amt
|
||||
self.outputs[idx] = self.MMGenTxOutput(**o)
|
||||
self.outputs[idx] = MMGenTX.MMGenTxOutput(**o)
|
||||
|
||||
def del_output(self,idx):
|
||||
self.outputs.pop(idx)
|
||||
|
|
@ -300,7 +321,8 @@ class MMGenTX(MMGenObject):
|
|||
def add_mmaddrs_to_outputs(self,ad_w,ad_f):
|
||||
a = [e.addr for e in self.outputs]
|
||||
d = ad_w.make_reverse_dict(a)
|
||||
d.update(ad_f.make_reverse_dict(a))
|
||||
if ad_f:
|
||||
d.update(ad_f.make_reverse_dict(a))
|
||||
for e in self.outputs:
|
||||
if e.addr and e.addr in d:
|
||||
e.mmid,f = d[e.addr]
|
||||
|
|
@ -316,13 +338,16 @@ class MMGenTX(MMGenObject):
|
|||
die(2,'{}: duplicate address in transaction {}'.format(attr,io_str))
|
||||
old_attr = attr
|
||||
|
||||
def update_txid(self):
|
||||
self.txid = MMGenTxID(make_chksum_6(unhexlify(self.hex)).upper())
|
||||
|
||||
def create_raw(self):
|
||||
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 = g.rpch.createrawtransaction(i,o)
|
||||
self.txid = MMGenTxID(make_chksum_6(unhexlify(self.hex)).upper())
|
||||
self.update_txid()
|
||||
|
||||
# returns true if comment added or changed
|
||||
def add_comment(self,infile=None):
|
||||
|
|
@ -484,8 +509,8 @@ class MMGenTX(MMGenObject):
|
|||
|
||||
def decode_io(self,desc,data):
|
||||
io,il = (
|
||||
(self.MMGenTxOutput,self.MMGenTxOutputList),
|
||||
(self.MMGenTxInput,self.MMGenTxInputList)
|
||||
(MMGenTX.MMGenTxOutput,MMGenTX.MMGenTxOutputList),
|
||||
(MMGenTX.MMGenTxInput,MMGenTX.MMGenTxInputList)
|
||||
)[desc=='inputs']
|
||||
return il([io(**dict([(k,d[k]) for k in io.__dict__
|
||||
if k in d and d[k] not in ('',None)])) for d in data])
|
||||
|
|
@ -519,6 +544,13 @@ class MMGenTX(MMGenObject):
|
|||
def add_timestamp(self):
|
||||
self.timestamp = make_timestamp()
|
||||
|
||||
def get_hex_locktime(self):
|
||||
return int(hexlify(unhexlify(self.hex[-8:])[::-1]),16)
|
||||
|
||||
def set_hex_locktime(self,val):
|
||||
assert type(val) == int,'locktime value not an integer'
|
||||
self.hex = self.hex[:-8] + hexlify(unhexlify('{:08x}'.format(val))[::-1])
|
||||
|
||||
def add_blockcount(self):
|
||||
self.blockcount = int(g.rpch.getblockcount())
|
||||
|
||||
|
|
@ -526,13 +558,14 @@ class MMGenTX(MMGenObject):
|
|||
self.inputs.check_coin_mismatch()
|
||||
self.outputs.check_coin_mismatch()
|
||||
lines = [
|
||||
'{}{} {} {} {} {}'.format(
|
||||
'{}{} {} {} {} {}{}'.format(
|
||||
(g.coin+' ','')[g.coin=='BTC'],
|
||||
self.chain.upper() if self.chain else 'Unknown',
|
||||
self.txid,
|
||||
self.send_amt,
|
||||
self.timestamp,
|
||||
self.blockcount
|
||||
self.blockcount,
|
||||
('',' LT={}'.format(self.locktime))[bool(self.locktime)]
|
||||
),
|
||||
self.hex,
|
||||
repr([e.__dict__ for e in self.inputs]),
|
||||
|
|
@ -624,20 +657,25 @@ class MMGenTX(MMGenObject):
|
|||
ret = self.desc == 'signed transaction'
|
||||
return (red,green)[ret](str(ret)) if color else ret
|
||||
|
||||
# protect against an attack where a malicious, compromised or malfunctioning coin daemon could switch
|
||||
# hex transaction data.
|
||||
# check that a malicious, compromised or malfunctioning coin daemon hasn't altered hex tx data:
|
||||
def check_hex_tx_matches_mmgen_tx(self,deserial_tx):
|
||||
m = 'Fatal error: a malicious or malfunctioning coin daemon or other program has altered your data!'
|
||||
m = 'Fatal error: a malicious or malfunctioning coin daemon or other program may have altered your data!'
|
||||
|
||||
if deserial_tx['lock_time'] != 0:
|
||||
rdie(3,'\nLock time is not zero!\n' + m)
|
||||
lt = deserial_tx['lock_time']
|
||||
if lt != int(self.locktime or 0):
|
||||
m2 = '\nTransaction hex locktime ({}) does not match MMGen transaction locktime ({})\n{}'
|
||||
rdie(3,m2.format(lt,self.locktime,m))
|
||||
|
||||
def check_equal(desc,mmio,hexio):
|
||||
def check_equal(desc,hexio,mmio):
|
||||
if mmio != hexio:
|
||||
msg('\nMMGen {}:\n{}'.format(desc,pformat(mmio)))
|
||||
msg('Hex {}:\n{}'.format(desc,pformat(hexio)))
|
||||
m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n' + m
|
||||
rdie(3,m2.format(desc.capitalize()))
|
||||
m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n'
|
||||
rdie(3,(m2+m).format(desc.capitalize()))
|
||||
|
||||
seq_hex = map(lambda i: int(i['nSeq'],16),deserial_tx['txins'])
|
||||
seq_mmgen = map(lambda i: i.sequence or g.max_int,self.inputs)
|
||||
check_equal('sequence numbers',seq_hex,seq_mmgen)
|
||||
|
||||
d_hex = sorted((i['txid'],i['vout']) for i in deserial_tx['txins'])
|
||||
d_mmgen = sorted((i.txid,i.vout) for i in self.inputs)
|
||||
|
|
@ -649,7 +687,7 @@ class MMGenTX(MMGenObject):
|
|||
|
||||
uh = deserial_tx['unsigned_hex']
|
||||
if str(self.txid) != make_chksum_6(unhexlify(uh)).upper():
|
||||
die(3,'MMGen TxID ({}) does not match hex transaction data!'.format(self.txid))
|
||||
rdie(3,'MMGen TxID ({}) does not match hex transaction data!\n{}'.format(self.txid,m))
|
||||
|
||||
def check_sigs(self,deserial_tx=None): # return False if no sigs, die on error
|
||||
txins = (deserial_tx or DeserializedTX(self.hex))['txins']
|
||||
|
|
@ -706,7 +744,7 @@ class MMGenTX(MMGenObject):
|
|||
if ret:
|
||||
die(1,'Transaction has been replaced'+('',', and the replacement TX is confirmed')[ret==2]+'!')
|
||||
|
||||
def send(self,prompt_user=True):
|
||||
def send(self,prompt_user=True,exit_on_fail=False):
|
||||
|
||||
if not self.marked_signed():
|
||||
die(1,'Transaction is not signed!')
|
||||
|
|
@ -745,10 +783,14 @@ class MMGenTX(MMGenObject):
|
|||
elif 'Illegal use of SIGHASH_FORKID' in errmsg:
|
||||
m = 'The Aug. 1 2017 UAHF is not yet active on this chain.'
|
||||
m += "\nRe-run the script without the --coin=bch option."
|
||||
elif '64: non-final' in errmsg:
|
||||
m2 = "Transaction with locktime '{}' can't be included in this block!"
|
||||
m = m2.format(strfmt_locktime(self.get_hex_locktime()))
|
||||
else:
|
||||
m = errmsg
|
||||
msg(yellow(m))
|
||||
msg(red('Send of MMGen transaction {} failed'.format(self.txid)))
|
||||
if exit_on_fail: sys.exit(1)
|
||||
return False
|
||||
else:
|
||||
if bogus_send:
|
||||
|
|
@ -768,24 +810,28 @@ class MMGenTX(MMGenObject):
|
|||
ask_write=ask_write,
|
||||
ask_write_default_yes=ask_write_default_yes)
|
||||
|
||||
def create_fn(self):
|
||||
tl = self.get_hex_locktime()
|
||||
self.fn = '{}{}[{!s}{}{}].{}'.format(
|
||||
self.txid,
|
||||
('-'+g.coin,'')[g.coin=='BTC'],
|
||||
self.send_amt,
|
||||
('',',{}'.format(self.btc2spb(self.get_fee())))[self.is_rbf()],
|
||||
('',',tl={}'.format(tl))[bool(tl)],
|
||||
self.ext)
|
||||
|
||||
def write_to_file( self,
|
||||
add_desc='',
|
||||
ask_write=True,
|
||||
ask_write_default_yes=False,
|
||||
ask_tty=True,
|
||||
ask_overwrite=True,
|
||||
fn=None):
|
||||
if ask_write == False:
|
||||
ask_write_default_yes=True
|
||||
self.format()
|
||||
if not fn:
|
||||
fn = '{}{}[{}{}].{}'.format(
|
||||
self.txid,
|
||||
('-'+g.coin,'')[g.coin=='BTC'],
|
||||
self.send_amt,
|
||||
('',',{}'.format(self.btc2spb(self.get_fee())))[self.is_rbf()],
|
||||
self.ext)
|
||||
write_data_to_file(fn,self.fmt_data,self.desc+add_desc,
|
||||
ask_overwrite=True):
|
||||
|
||||
if ask_write == False: ask_write_default_yes = True
|
||||
if not self.fmt_data: self.format()
|
||||
if not self.fn: self.create_fn()
|
||||
|
||||
write_data_to_file(self.fn,self.fmt_data,self.desc+add_desc,
|
||||
ask_overwrite=ask_overwrite,
|
||||
ask_write=ask_write,
|
||||
ask_tty=ask_tty,
|
||||
|
|
@ -814,9 +860,6 @@ class MMGenTX(MMGenObject):
|
|||
def is_rbf(self):
|
||||
return self.inputs[0].sequence == g.max_int - 2
|
||||
|
||||
def signal_for_rbf(self):
|
||||
self.inputs[0].sequence = g.max_int - 2
|
||||
|
||||
def format_view(self,terse=False):
|
||||
try:
|
||||
rpc_init()
|
||||
|
|
@ -824,12 +867,6 @@ class MMGenTX(MMGenObject):
|
|||
except:
|
||||
blockcount = None
|
||||
|
||||
hdr_fs = (
|
||||
'TRANSACTION DATA\n\n[ID:{}] [{} {}] [{} UTC] [RBF:{}] [Signed:{}]\n',
|
||||
'Transaction {} {} {} ({} UTC) RBF={} Signed={}\n'
|
||||
)[bool(terse)]
|
||||
nonmm_str = '(non-{pnm} address)'.format(pnm=g.proj_name)
|
||||
|
||||
def get_max_mmwid(io):
|
||||
if io == self.inputs:
|
||||
sel_f = lambda o: len(o.mmid) + 2 # len('()')
|
||||
|
|
@ -837,6 +874,7 @@ class MMGenTX(MMGenObject):
|
|||
sel_f = lambda o: len(o.mmid) + (2,8)[bool(o.is_chg)] # + len(' (chg)')
|
||||
return max(max([sel_f(o) for o in io if o.mmid] or [0]),len(nonmm_str))
|
||||
|
||||
nonmm_str = '(non-{pnm} address)'.format(pnm=g.proj_name)
|
||||
max_mmwid = max(get_max_mmwid(self.inputs),get_max_mmwid(self.outputs))
|
||||
|
||||
def format_io(io):
|
||||
|
|
@ -866,8 +904,18 @@ class MMGenTX(MMGenObject):
|
|||
io_out += '\n'.join([('{:>3} {:<8} {}'.format(*d)) for d in items if d[2]]) + '\n\n'
|
||||
return io_out
|
||||
|
||||
out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),g.coin,self.timestamp,
|
||||
(red('False'),green('True'))[self.is_rbf()],self.marked_signed(color=True))
|
||||
hdr_fs = (
|
||||
'TRANSACTION DATA\n\nID={} ({} {}) UTC={} RBF={} Sig={} Locktime={}\n',
|
||||
'TX {} ({} {}) UTC={} RBF={} Sig={} Locktime={}\n'
|
||||
)[bool(terse)]
|
||||
out = hdr_fs.format(self.txid.hl(),
|
||||
self.send_amt.hl(),
|
||||
g.coin,
|
||||
self.timestamp,
|
||||
(red('False'),
|
||||
green('True'))[self.is_rbf()],
|
||||
self.marked_signed(color=True),
|
||||
(green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)])
|
||||
if self.chain in ('testnet','regtest'):
|
||||
out += green('Chain: {}\n'.format(self.chain.upper()))
|
||||
if self.coin_txid:
|
||||
|
|
@ -925,6 +973,10 @@ class MMGenTX(MMGenObject):
|
|||
metadata,self.hex,inputs_data,outputs_data = tx_data
|
||||
metadata = metadata.split()
|
||||
|
||||
if metadata[-1].find('LT=') == 0:
|
||||
desc = 'locktime'
|
||||
self.locktime = int(metadata.pop()[3:])
|
||||
|
||||
self.coin = metadata.pop(0) if len(metadata) == 6 else 'BTC'
|
||||
|
||||
if len(metadata) == 5:
|
||||
|
|
@ -955,7 +1007,7 @@ class MMGenTX(MMGenObject):
|
|||
if not self.chain and not self.inputs[0].addr.is_for_chain('testnet'):
|
||||
self.chain = 'mainnet'
|
||||
|
||||
def get_fee_from_estimate_or_user(self,estimate_fail_msg_shown=[]):
|
||||
def get_fee_from_user(self,have_estimate_fail=[]):
|
||||
|
||||
if opt.tx_fee:
|
||||
desc = 'User-selected'
|
||||
|
|
@ -964,9 +1016,9 @@ class MMGenTX(MMGenObject):
|
|||
desc = 'Network-estimated'
|
||||
ret = g.rpch.estimatefee(opt.tx_confs)
|
||||
if ret == -1:
|
||||
if not estimate_fail_msg_shown:
|
||||
if not have_estimate_fail:
|
||||
msg('Network fee estimation for {} confirmations failed'.format(opt.tx_confs))
|
||||
estimate_fail_msg_shown.append(True)
|
||||
have_estimate_fail.append(True)
|
||||
start_fee = None
|
||||
else:
|
||||
start_fee = g.proto.coin_amt(ret) * opt.tx_fee_adj * self.estimate_size() / 1024
|
||||
|
|
@ -1039,7 +1091,7 @@ class MMGenTX(MMGenObject):
|
|||
|
||||
self.copy_inputs_from_tw(sel_unspent) # makes self.inputs
|
||||
|
||||
change_amt = self.sum_inputs() - self.send_amt - self.get_fee_from_estimate_or_user()
|
||||
change_amt = self.sum_inputs() - self.send_amt - self.get_fee_from_user()
|
||||
|
||||
if change_amt >= 0:
|
||||
p = 'Transaction produces {} {} in change'.format(change_amt.hl(),g.coin)
|
||||
|
|
@ -1049,7 +1101,8 @@ class MMGenTX(MMGenObject):
|
|||
else:
|
||||
msg(wmsg['not_enough_coin'].format(abs(change_amt)))
|
||||
|
||||
def create(self,cmd_args,do_info=False):
|
||||
def create(self,cmd_args,locktime,do_info=False):
|
||||
assert type(locktime) == int
|
||||
|
||||
if opt.comment_file: self.add_comment(opt.comment_file)
|
||||
|
||||
|
|
@ -1072,7 +1125,9 @@ class MMGenTX(MMGenObject):
|
|||
|
||||
change_amt = self.get_inputs_from_user(tw)
|
||||
|
||||
if opt.rbf: self.signal_for_rbf() # only after we have inputs
|
||||
# only after we have inputs
|
||||
if locktime: self.inputs[0].sequence = g.max_int - 1
|
||||
if opt.rbf: self.inputs[0].sequence = g.max_int - 2
|
||||
|
||||
chg_idx = self.get_chg_output_idx()
|
||||
|
||||
|
|
@ -1089,6 +1144,12 @@ class MMGenTX(MMGenObject):
|
|||
self.add_comment() # edits an existing comment
|
||||
self.create_raw() # creates self.hex, self.txid
|
||||
|
||||
if locktime:
|
||||
msg('Setting nlocktime to {}!'.format(strfmt_locktime(locktime)))
|
||||
self.set_hex_locktime(locktime)
|
||||
self.update_txid()
|
||||
self.locktime = locktime
|
||||
|
||||
self.add_timestamp()
|
||||
self.add_blockcount()
|
||||
self.chain = g.chain
|
||||
|
|
@ -1165,3 +1226,77 @@ class MMGenBumpTX(MMGenTX):
|
|||
msg('{} {c}: {} fee too large. Maximum fee: <{} {c}'.format(ret,desc,output_amt,c=g.coin))
|
||||
return False
|
||||
return ret
|
||||
|
||||
class MMGenSplitTX(MMGenTX):
|
||||
|
||||
def __init__(self):
|
||||
super(type(self),self).__init__()
|
||||
|
||||
def get_fee_from_user(self,have_estimate_fail=[],split_tx=False):
|
||||
|
||||
if split_tx:
|
||||
try:
|
||||
rpc_init(reinit=True)
|
||||
except:
|
||||
ymsg('Connect to {} daemon failed. Network fee estimation unavailable'.format(g.coin))
|
||||
desc = 'User-selected'
|
||||
try:
|
||||
# TODO: check in opts.py
|
||||
start_fee = opt.tx_fees.split(',')[split_tx]
|
||||
except:
|
||||
start_fee = None
|
||||
return self.get_usr_fee_interactive(start_fee,desc=desc)
|
||||
|
||||
return super(type(self),self).get_fee_from_user(have_estimate_fail=have_estimate_fail)
|
||||
|
||||
def get_outputs_from_cmdline(self,mmid):
|
||||
|
||||
from mmgen.addr import AddrData
|
||||
ad_w = AddrData(source='tw')
|
||||
|
||||
# TODO: check that addr is empty
|
||||
|
||||
if is_mmgen_id(mmid):
|
||||
coin_addr = mmaddr2coinaddr(mmid,ad_w,None) if is_mmgen_id(mmid) else CoinAddr(mmid)
|
||||
self.add_output(coin_addr,g.proto.coin_amt('0'),is_chg=True)
|
||||
else:
|
||||
die(2,'{}: invalid command-line argument'.format(mmid))
|
||||
|
||||
self.add_mmaddrs_to_outputs(ad_w,None)
|
||||
|
||||
if not segwit_is_active() and self.has_segwit_outputs():
|
||||
fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain'
|
||||
rdie(2,fs.format(g.proj_name))
|
||||
|
||||
def create_split(self,mmid):
|
||||
|
||||
self.outputs = self.MMGenTxOutputList()
|
||||
self.get_outputs_from_cmdline(mmid)
|
||||
|
||||
while True:
|
||||
change_amt = self.sum_inputs() - self.get_fee_from_user(split_tx=True)
|
||||
if change_amt >= 0:
|
||||
p = 'Transaction produces {} {} in change'.format(change_amt.hl(),g.coin)
|
||||
if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
|
||||
if opt.yes: msg(p)
|
||||
break
|
||||
else:
|
||||
msg(wmsg['not_enough_coin'].format(abs(change_amt)))
|
||||
|
||||
self.update_output_amt(0,change_amt)
|
||||
self.send_amt = change_amt
|
||||
|
||||
if not opt.yes:
|
||||
self.add_comment() # edits an existing comment
|
||||
self.create_raw() # creates self.hex, self.txid
|
||||
|
||||
self.add_timestamp()
|
||||
self.add_blockcount() # TODO
|
||||
self.chain = g.chain
|
||||
|
||||
assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee
|
||||
|
||||
qmsg('Transaction successfully created')
|
||||
|
||||
if not opt.yes:
|
||||
self.view_with_prompt('View decoded transaction?')
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Tested on Linux, MinGW-64
|
||||
# MinGW's bash 3.1.17 doesn't do ${var^^}
|
||||
|
||||
dfl_tests='obj misc btc btc_tn btc_rt bch bch_rt ltc ltc_tn ltc_rt tool gen'
|
||||
dfl_tests='obj misc btc btc_tn btc_rt bch bch_rt b2x b2x_rt ltc ltc_tn ltc_rt tool gen'
|
||||
PROGNAME=$(basename $0)
|
||||
while getopts hinPt OPT
|
||||
do
|
||||
|
|
@ -22,6 +22,8 @@ do
|
|||
echo " btc_rt - bitcoin regtest"
|
||||
echo " bch - bitcoin cash (BCH)"
|
||||
echo " bch_rt - bitcoin cash (BCH) regtest"
|
||||
echo " b2x - bitcoin 2x (B2X)"
|
||||
echo " b2x_rt - bitcoin 2x (B2X) regtest"
|
||||
echo " ltc - litecoin"
|
||||
echo " ltc_tn - litecoin testnet"
|
||||
echo " ltc_rt - litecoin regtest"
|
||||
|
|
@ -51,7 +53,7 @@ set -e
|
|||
REFDIR=test/ref
|
||||
if uname -a | grep -qi mingw; then SUDO='' MINGW=1; else SUDO='sudo' MINGW=''; fi
|
||||
|
||||
function check {
|
||||
check() {
|
||||
[ "$BRANCH" ] || { echo 'No branch specified. Exiting'; exit; }
|
||||
[ "$(git diff $BRANCH)" == "" ] || {
|
||||
echo "Unmerged changes from branch '$BRANCH'. Exiting"
|
||||
|
|
@ -60,7 +62,7 @@ function check {
|
|||
git diff $BRANCH >/dev/null 2>&1 || exit
|
||||
}
|
||||
|
||||
function install {
|
||||
install() {
|
||||
set -x
|
||||
eval "$SUDO rm -rf .test-release"
|
||||
git clone --branch $BRANCH --single-branch . .test-release
|
||||
|
|
@ -74,7 +76,7 @@ function install {
|
|||
[ "$MINGW" ] && ./setup.py build --compiler=mingw32
|
||||
eval "$SUDO ./setup.py install"
|
||||
}
|
||||
function do_test {
|
||||
do_test() {
|
||||
set +x
|
||||
for i in "$@"; do
|
||||
LS='\n'
|
||||
|
|
@ -93,7 +95,7 @@ t_obj=(
|
|||
'test/objtest.py --coin=ltc --testnet=1 -S')
|
||||
f_obj='Data object test complete'
|
||||
|
||||
i_misc='Miscellaneous operations'
|
||||
i_misc='Miscellaneous operations' # includes autosign!
|
||||
s_misc='The bitcoin, bitcoin-abc and litecoin (mainnet) daemons must be running for the following tests'
|
||||
t_misc=(
|
||||
'test/test.py -On misc')
|
||||
|
|
@ -120,7 +122,9 @@ f_btc_tn='You may stop the bitcoin testnet daemon if you wish'
|
|||
|
||||
i_btc_rt='Bitcoin regtest'
|
||||
s_btc_rt="The following tests will test MMGen's regtest (Bob and Alice) mode"
|
||||
t_btc_rt=('test/test.py -On regtest')
|
||||
t_btc_rt=(
|
||||
'test/test.py -On regtest'
|
||||
'test/test.py -On regtest_split')
|
||||
f_btc_rt="Regtest (Bob and Alice) mode tests for BTC completed"
|
||||
|
||||
i_bch='Bitcoin cash (BCH)'
|
||||
|
|
@ -133,6 +137,16 @@ s_bch_rt="The following tests will test MMGen's regtest (Bob and Alice) mode"
|
|||
t_bch_rt=('test/test.py --coin=bch -On regtest')
|
||||
f_bch_rt="Regtest (Bob and Alice) mode tests for BCH completed"
|
||||
|
||||
i_b2x='Bitcoin 2X (B2X)'
|
||||
s_b2x='The bitcoin 2X daemon (BTC1) must both be running for the following tests'
|
||||
t_b2x=('test/test.py -On --coin=b2x dfl_wallet main ref ref_other')
|
||||
f_b2x='You may stop the Bitcoin 2X daemon if you wish'
|
||||
|
||||
i_b2x_rt='Bitcoin 2X (B2X) regtest'
|
||||
s_b2x_rt="The following tests will test MMGen's regtest (Bob and Alice) mode"
|
||||
t_b2x_rt=('test/test.py --coin=b2x -On regtest')
|
||||
f_b2x_rt="Regtest (Bob and Alice) mode tests for B2X completed"
|
||||
|
||||
i_ltc='Litecoin'
|
||||
s_ltc='The litecoin daemon must both be running for the following tests'
|
||||
t_ltc=(
|
||||
|
|
@ -194,12 +208,13 @@ f_gen="gentest tests completed"
|
|||
}
|
||||
[ "$INSTALL_ONLY" ] && exit
|
||||
|
||||
function skip_maybe {
|
||||
skip_maybe() {
|
||||
echo -n "Enter 's' to skip, or ENTER to continue: "; read
|
||||
[ "$REPLY" == 's' ] && return 0
|
||||
return 1
|
||||
}
|
||||
function run_tests {
|
||||
|
||||
run_tests() {
|
||||
for t in $1; do
|
||||
eval echo -e \${GREEN}'###' Running $(echo \$i_$t) tests\$RESET
|
||||
[ "$PAUSE" ] && { eval echo $(echo \$s_$t); skip_maybe && continue; }
|
||||
|
|
@ -209,9 +224,17 @@ function run_tests {
|
|||
done
|
||||
}
|
||||
|
||||
check_args() {
|
||||
for i in $tests; do
|
||||
echo "$dfl_tests" | grep -q "\<$i\>" || { echo "$i: unrecognized argument"; exit; }
|
||||
done
|
||||
}
|
||||
|
||||
tests=$dfl_tests
|
||||
[ "$*" ] && tests="$*"
|
||||
[ "$NO_PAUSE" ] || PAUSE=1
|
||||
|
||||
check_args
|
||||
run_tests "$tests"
|
||||
|
||||
echo -e "${GREEN}All OK$RESET"
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -136,6 +136,7 @@ setup(
|
|||
'mmgen.main_passgen',
|
||||
'mmgen.main_addrimport',
|
||||
'mmgen.main_regtest',
|
||||
'mmgen.main_split',
|
||||
'mmgen.main_txcreate',
|
||||
'mmgen.main_txbump',
|
||||
'mmgen.main_txsign',
|
||||
|
|
@ -158,6 +159,7 @@ setup(
|
|||
'cmds/mmgen-walletchk',
|
||||
'cmds/mmgen-walletconv',
|
||||
'cmds/mmgen-walletgen',
|
||||
'cmds/mmgen-split',
|
||||
'cmds/mmgen-txcreate',
|
||||
'cmds/mmgen-txbump',
|
||||
'cmds/mmgen-txsign',
|
||||
|
|
|
|||
6
test/ref/6A52BC-B2X[106.6789,tl=1320969600].rawtx
Normal file
6
test/ref/6A52BC-B2X[106.6789,tl=1320969600].rawtx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
ff8ec9
|
||||
B2X MAINNET 6A52BC 106.6789 20171113_152611 434 LT=1320969600
|
||||
020000000321efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0000000000feffffff21efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0200000000ffffffff21efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0100000000ffffffff0400e40b54020000001976a914e9721033b4cd4fae9e5510cdc9d7871dcf2be9e488ac30051d21000000001976a914b57ca511b3e5b54a7ec936b0fc6b44f595a7cffe88ac6c206028090000001976a9142794da6d30fc6adced41313ac3bfab900096b44488ac202cb2060000000017a914f12c150f224f045e75799410d6b6653af33568d8878065bc4e
|
||||
[{'confs': 1, 'scriptPubKey': '76a9149b1a59d1411e2678a707b371e8e805bbacdde32288ac', 'addr': '1F97Jd89wwmu4ELadesAdGDzg3d8Y6j5iP', 'vout': 0, 'sequence': 4294967294, 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'mmid': '98831F3A:C:1', 'amt': B2XAmt('100'), 'label': u''}, {'confs': 1, 'scriptPubKey': '76a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac', 'addr': '1GfuYaKHrhdiVybXMGCcjadSgfjvpdt2x9', 'vout': 2, 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'mmid': '98831F3A:L:2', 'amt': B2XAmt('200'), 'label': u''}, {'confs': 1, 'scriptPubKey': 'a914dd36f2365178f16504c5c38492a015be59bff33b87', 'addr': '3Mrh5fQwUwV9U2D1VHBrBW6cwqM4ZBdja9', 'vout': 1, 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'mmid': '98831F3A:S:2', 'amt': B2XAmt('199.9999598'), 'label': u''}]
|
||||
[{'is_chg': True, 'addr': '14cHfz8dixc3GL3HFZEbicjinguTkaL1BJ', 'amt': B2XAmt('393.3209406'), 'mmid': '98831F3A:L:4'}, {'addr': '1NHM5xG2x1972hGvPqi1X1bcQV4bg4B6zK', 'amt': B2XAmt('100')}, {'mmid': '98831F3A:C:5', 'amt': B2XAmt('5.5555'), 'addr': '1HYcdCFPmWakX2g8mP6ksxDDokDyRbeaAb'}, {'mmid': '98831F3A:S:3', 'amt': B2XAmt('1.1234'), 'addr': '3PgDafUUfbG4MYCXjKgzrfZTRFogLKS4fZ'}]
|
||||
TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
c14468
|
||||
B2X TESTNET 6A52BC 106.6789 20171113_152611 434 LT=1320969600
|
||||
020000000321efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0000000000feffffff21efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0200000000ffffffff21efc7591303a97400ed3bb813258762695f40100825255ab0f9d596310c654a0100000000ffffffff0400e40b54020000001976a914e9721033b4cd4fae9e5510cdc9d7871dcf2be9e488ac30051d21000000001976a914b57ca511b3e5b54a7ec936b0fc6b44f595a7cffe88ac6c206028090000001976a9142794da6d30fc6adced41313ac3bfab900096b44488ac202cb2060000000017a914f12c150f224f045e75799410d6b6653af33568d8878065bc4e
|
||||
[{'confs': 1, 'addr': 'muf4bgD8kyD9qLpCMDqYTBSKY3DqTWZR92', 'vout': 0, 'sequence': 4294967294, 'label': u'', 'mmid': '98831F3A:C:1', 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'amt': B2XAmt('100'), 'scriptPubKey': '76a9149b1a59d1411e2678a707b371e8e805bbacdde32288ac'}, {'confs': 1, 'addr': 'mwBrqdQGfj4yH6594qAzZVqmYfLdmB1C7W', 'vout': 2, 'label': u'', 'mmid': '98831F3A:L:2', 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'amt': B2XAmt('200'), 'scriptPubKey': '76a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac'}, {'confs': 1, 'addr': '2NDQu9QLy6PzVfoqZAQoioT5tABZENihnkH', 'vout': 1, 'label': u'', 'mmid': '98831F3A:S:2', 'txid': '4a650c3196d5f9b05a25250810405f6962872513b83bed0074a9031359c7ef21', 'amt': B2XAmt('199.9999598'), 'scriptPubKey': 'a914dd36f2365178f16504c5c38492a015be59bff33b87'}]
|
||||
[{'addr': 'mj8Ey3DcXz3J3SWty8CyYXx3egWAapMVMF', 'mmid': '98831F3A:L:4', 'amt': B2XAmt('393.3209406'), 'is_chg': True}, {'addr': 'n2oJP1M1m2aMookY7QgPLvowGUfJYcFbZ7', 'amt': B2XAmt('100')}, {'mmid': '98831F3A:C:5', 'addr': 'mx4ZvFLNaY21J99kUx58hsRYfjpgPYwpHn', 'amt': B2XAmt('5.5555')}, {'mmid': '98831F3A:S:3', 'addr': '2NFEReQQWH3mQZKq5QTJsUcYidc1r35E2Rg', 'amt': B2XAmt('1.1234')}]
|
||||
TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos
|
||||
210
test/test.py
210
test/test.py
|
|
@ -151,22 +151,25 @@ ref_subdir = '' if g.proto.base_coin == 'BTC' else g.proto.name
|
|||
altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin
|
||||
tn_ext = ('','.testnet')[g.testnet]
|
||||
|
||||
fork = {'bch':'btc','btc':'btc','ltc':'ltc'}[g.coin.lower()]
|
||||
tx_fee = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[g.coin.lower()]
|
||||
txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[g.coin.lower()]
|
||||
coin_sel = g.coin.lower()
|
||||
if g.coin == 'B2X': coin_sel = 'btc'
|
||||
|
||||
rtFundAmt = {'btc':'500','bch':'500','ltc':'5500'}[g.coin.lower()]
|
||||
fork = {'bch':'btc','btc':'btc','ltc':'ltc'}[coin_sel]
|
||||
tx_fee = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[coin_sel]
|
||||
txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[coin_sel]
|
||||
|
||||
rtFundAmt = {'btc':'500','bch':'500','ltc':'5500'}[coin_sel]
|
||||
rtFee = {
|
||||
'btc': ('20s','10s','60s','0.0001','10s','20s'),
|
||||
'bch': ('20s','10s','60s','0.0001','10s','20s'),
|
||||
'ltc': ('1000s','500s','1500s','0.05','400s','1000s')
|
||||
}[g.coin.lower()]
|
||||
}[coin_sel]
|
||||
rtBals = {
|
||||
'btc': ('499.999942','399.9998214','399.9998079','399.9996799','13.00000000','986.99957990','999.99957990'),
|
||||
'bch': ('499.9999416','399.9999124','399.99989','399.9997616','276.22339397','723.77626763','999.99966160'),
|
||||
'ltc': ('5499.9971','5399.994085','5399.993545','5399.987145','13.00000000','10986.93714500','10999.93714500'),
|
||||
}[g.coin.lower()]
|
||||
rtBobOp3 = {'btc':'S:2','bch':'L:3','ltc':'S:2'}[g.coin.lower()]
|
||||
}[coin_sel]
|
||||
rtBobOp3 = {'btc':'S:2','bch':'L:3','ltc':'S:2'}[coin_sel]
|
||||
|
||||
if opt.segwit and 'S' not in g.proto.mmtypes:
|
||||
die(1,'--segwit option incompatible with {}'.format(g.proto.__name__))
|
||||
|
|
@ -201,12 +204,9 @@ cfgs = {
|
|||
},
|
||||
'segwit': get_segwit_val()
|
||||
},
|
||||
'17': {
|
||||
'tmpdir': os.path.join('test','tmp17'),
|
||||
},
|
||||
'18': {
|
||||
'tmpdir': os.path.join('test','tmp18'),
|
||||
},
|
||||
'17': { 'tmpdir': os.path.join('test','tmp17') },
|
||||
'18': { 'tmpdir': os.path.join('test','tmp18') },
|
||||
'19': { 'tmpdir': os.path.join('test','tmp19'), 'wpasswd':'abc' },
|
||||
'1': {
|
||||
'tmpdir': os.path.join('test','tmp1'),
|
||||
'wpasswd': 'Dorian',
|
||||
|
|
@ -452,6 +452,7 @@ cfgs = {
|
|||
'ref_tx_file': {
|
||||
'btc': 'FFB367[1.234]{}.rawtx',
|
||||
'bch': '99BE60-BCH[106.6789]{}.rawtx',
|
||||
'b2x': '6A52BC-B2X[106.6789,tl=1320969600]{}.rawtx',
|
||||
'ltc': '75F455-LTC[106.6789]{}.rawtx',
|
||||
},
|
||||
'ic_wallet': '98831F3A-5482381C-18460FB1[256,1].mmincog',
|
||||
|
|
@ -671,6 +672,30 @@ cmd_group['regtest'] = (
|
|||
('regtest_stop', 'stopping regtest daemon'),
|
||||
)
|
||||
|
||||
cmd_group['regtest_split'] = (
|
||||
('regtest_split_setup', 'regtest forking scenario setup'),
|
||||
('regtest_walletgen_bob', "generating Bob's wallet"),
|
||||
('regtest_addrgen_bob', "generating Bob's addresses"),
|
||||
('regtest_addrimport_bob', "importing Bob's addresses"),
|
||||
('regtest_fund_bob', "funding Bob's wallet"),
|
||||
('regtest_split_fork', 'regtest split fork'),
|
||||
('regtest_split_start_btc', 'start regtest daemon (BTC)'),
|
||||
('regtest_split_start_b2x', 'start regtest daemon (B2X)'),
|
||||
('regtest_split_gen_btc', 'mining a block (BTC)'),
|
||||
('regtest_split_gen_b2x', 'mining 100 blocks (B2X)'),
|
||||
('regtest_split_do_split', 'creating coin splitting transactions'),
|
||||
('regtest_split_sign_b2x', 'signing B2X split transaction'),
|
||||
('regtest_split_sign_btc', 'signing BTC split transaction'),
|
||||
('regtest_split_send_b2x', 'sending B2X split transaction'),
|
||||
('regtest_split_send_btc', 'sending BTC split transaction'),
|
||||
('regtest_split_gen_btc', 'mining a block (BTC)'),
|
||||
('regtest_split_gen_b2x2', 'mining a block (B2X)'),
|
||||
('regtest_split_txdo_timelock_bad_btc', 'sending transaction with bad locktime (BTC)'),
|
||||
('regtest_split_txdo_timelock_good_btc','sending transaction with good locktime (BTC)'),
|
||||
('regtest_split_txdo_timelock_bad_b2x', 'sending transaction with bad locktime (B2X)'),
|
||||
('regtest_split_txdo_timelock_good_b2x','sending transaction with good locktime (B2X)'),
|
||||
)
|
||||
|
||||
cmd_group['misc'] = (
|
||||
('autosign', 'transaction autosigning (BTC,BCH,LTC)'),
|
||||
)
|
||||
|
|
@ -743,6 +768,11 @@ for a,b in cmd_group['regtest']:
|
|||
cmd_list['regtest'].append(a)
|
||||
cmd_data[a] = (17,b,[[[],17]])
|
||||
|
||||
cmd_data['info_regtest_split'] = 'regtest mode with fork and coin split',[17]
|
||||
for a,b in cmd_group['regtest_split']:
|
||||
cmd_list['regtest_split'].append(a)
|
||||
cmd_data[a] = (19,b,[[[],19]])
|
||||
|
||||
cmd_data['info_misc'] = 'miscellaneous operations',[18]
|
||||
for a,b in cmd_group['misc']:
|
||||
cmd_list['misc'].append(a)
|
||||
|
|
@ -935,7 +965,7 @@ def create_fake_unspent_entry(coinaddr,al_id=None,idx=None,lbl=None,non_mmgen=Fa
|
|||
if 'S' not in g.proto.mmtypes: segwit = False
|
||||
if lbl: lbl = ' ' + lbl
|
||||
spk1,spk2 = (('76a914','88ac'),('a914','87'))[segwit and coinaddr.addr_fmt=='p2sh']
|
||||
amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[g.coin.lower()]
|
||||
amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[coin_sel]
|
||||
return {
|
||||
'account': '{}:{}'.format(g.proto.base_coin.lower(),coinaddr) if non_mmgen \
|
||||
else (u'{}:{}{}'.format(al_id,idx,lbl.decode('utf8'))),
|
||||
|
|
@ -1032,7 +1062,7 @@ def make_txcreate_cmdline(tx_data):
|
|||
coinaddr = AddrGenerator(t).to_addr(KeyGenerator().to_pubhex(privkey))
|
||||
|
||||
# total of two outputs must be < 10 BTC (<1000 LTC)
|
||||
mods = {'btc':(6,4),'bch':(6,4),'ltc':(600,400)}[g.coin.lower()]
|
||||
mods = {'btc':(6,4),'bch':(6,4),'ltc':(600,400)}[coin_sel]
|
||||
for k in cfgs:
|
||||
cfgs[k]['amts'] = [None,None]
|
||||
for idx,mod in enumerate(mods):
|
||||
|
|
@ -1501,11 +1531,11 @@ class MMGenTestSuite(object):
|
|||
add = ' #' + tnum if tnum else ''
|
||||
t.written_to_file('Signed transaction' + add, oo=True)
|
||||
|
||||
def txsign(self,name,txfile,wf,pf='',bumpf='',save=True,has_label=False,txdo_handle=None):
|
||||
def txsign(self,name,txfile,wf,pf='',bumpf='',save=True,has_label=False,txdo_handle=None,extra_opts=[]):
|
||||
if txdo_handle:
|
||||
t = txdo_handle
|
||||
else:
|
||||
t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],txfile]+([],[wf])[bool(wf)])
|
||||
t = MMGenExpect(name,'mmgen-txsign', extra_opts + ['-d',cfg['tmpdir'],txfile]+([],[wf])[bool(wf)])
|
||||
t.license()
|
||||
t.tx_view()
|
||||
t.passphrase('MMGen wallet',cfg['wpasswd'])
|
||||
|
|
@ -1522,18 +1552,24 @@ class MMGenTestSuite(object):
|
|||
def txsign_dfl_wallet(self,name,txfile,pf='',save=True,has_label=False):
|
||||
return self.txsign(name,txfile,wf=None,pf=pf,save=save,has_label=has_label)
|
||||
|
||||
def txsend(self,name,sigfile,txdo_handle=None):
|
||||
def txsend(self,name,sigfile,txdo_handle=None,really_send=False,extra_opts=[]):
|
||||
if txdo_handle:
|
||||
t = txdo_handle
|
||||
else:
|
||||
t = MMGenExpect(name,'mmgen-txsend', ['-d',cfg['tmpdir'],sigfile])
|
||||
if really_send: os.environ['MMGEN_BOGUS_SEND'] = ''
|
||||
t = MMGenExpect(name,'mmgen-txsend', extra_opts + ['-d',cfg['tmpdir'],sigfile])
|
||||
if really_send: os.environ['MMGEN_BOGUS_SEND'] = '1'
|
||||
t.license()
|
||||
t.tx_view()
|
||||
t.expect('Add a comment to transaction? (y/N): ','\n')
|
||||
t.expect('Are you sure you want to broadcast this')
|
||||
m = 'YES, I REALLY WANT TO DO THIS'
|
||||
t.expect("'%s' to confirm: " % m,m+'\n')
|
||||
t.expect('BOGUS transaction NOT sent')
|
||||
if really_send:
|
||||
txid = t.expect_getend('Transaction sent: ')
|
||||
assert len(txid) == 64
|
||||
else:
|
||||
t.expect('BOGUS transaction NOT sent')
|
||||
t.written_to_file('Sent transaction')
|
||||
t.ok()
|
||||
|
||||
|
|
@ -1818,7 +1854,7 @@ class MMGenTestSuite(object):
|
|||
cmp_or_die(hincog_offset,int(o))
|
||||
|
||||
# Miscellaneous tests
|
||||
def autosign(self,name):
|
||||
def autosign(self,name): # tests everything except device detection, mount/unmount
|
||||
if g.platform == 'win':
|
||||
msg('Skipping {} (not supported)'.format(name)); return
|
||||
fdata = (('btc',''),('bch',''),('ltc','litecoin'))
|
||||
|
|
@ -2102,8 +2138,8 @@ class MMGenTestSuite(object):
|
|||
def regtest_walletgen_alice(self,name): return self.regtest_walletgen(name,'alice')
|
||||
|
||||
@staticmethod
|
||||
def regtest_user_dir(user):
|
||||
return os.path.join(data_dir,'regtest',user)
|
||||
def regtest_user_dir(user,coin=None):
|
||||
return os.path.join(data_dir,'regtest',coin or g.coin.lower(),user)
|
||||
|
||||
def regtest_user_sid(self,user):
|
||||
return os.path.basename(get_file_with_ext('mmdat',self.regtest_user_dir(user)))[:8]
|
||||
|
|
@ -2183,7 +2219,15 @@ class MMGenTestSuite(object):
|
|||
cmp_or_die(ret,rtBals[6],skip_ok=True)
|
||||
t.ok()
|
||||
|
||||
def regtest_user_txdo(self,name,user,fee,outputs_cl,outputs_prompt,extra_args=[],wf=None,pw='abc',no_send=False,do_label=False):
|
||||
def regtest_user_txdo( self,name,user,fee,
|
||||
outputs_cl,
|
||||
outputs_prompt,
|
||||
extra_args=[],
|
||||
wf=None,
|
||||
pw='abc',
|
||||
no_send=False,
|
||||
do_label=False,
|
||||
bad_locktime=False):
|
||||
os.environ['MMGEN_BOGUS_SEND'] = ''
|
||||
t = MMGenExpect(name,'mmgen-txdo',
|
||||
['-d',cfg['tmpdir'],'-B','--'+user,'--tx-fee='+fee]
|
||||
|
|
@ -2205,11 +2249,14 @@ class MMGenTestSuite(object):
|
|||
t.expect('to continue: ','\n')
|
||||
t.passphrase('MMGen wallet',pw)
|
||||
t.written_to_file('Signed transaction')
|
||||
if not no_send:
|
||||
if no_send:
|
||||
t.read()
|
||||
exit_val = 0
|
||||
else:
|
||||
t.expect('to confirm: ','YES, I REALLY WANT TO DO THIS\n')
|
||||
t.expect('Transaction sent')
|
||||
t.read()
|
||||
t.ok()
|
||||
s,exit_val = (('Transaction sent',0),("can't be included",1))[bad_locktime]
|
||||
t.expect(s)
|
||||
t.ok(exit_val)
|
||||
|
||||
def regtest_bob_split1(self,name):
|
||||
sid = self.regtest_user_sid('bob')
|
||||
|
|
@ -2269,9 +2316,11 @@ class MMGenTestSuite(object):
|
|||
txfile = get_file_with_ext(',{}].sigtx'.format(rtFee[1][:-1]),cfg['tmpdir'],delete=False,no_dot=True)
|
||||
return self.regtest_user_txbump(name,'bob',txfile,rtFee[2],'c')
|
||||
|
||||
def regtest_generate(self,name):
|
||||
t = MMGenExpect(name,'mmgen-regtest',['generate'])
|
||||
t.expect('Mined 1 block')
|
||||
def regtest_generate(self,name,coin=None,num_blocks=1):
|
||||
int(num_blocks)
|
||||
if coin: opt.coin = coin
|
||||
t = MMGenExpect(name,'mmgen-regtest',['generate',str(num_blocks)])
|
||||
t.expect('Mined {} block'.format(num_blocks))
|
||||
t.ok()
|
||||
|
||||
def regtest_get_mempool(self,name):
|
||||
|
|
@ -2389,7 +2438,95 @@ class MMGenTestSuite(object):
|
|||
t = MMGenExpect(name,'mmgen-regtest',['stop'])
|
||||
t.ok()
|
||||
|
||||
# regtest undocumented admin commands
|
||||
def regtest_split_setup(self,name):
|
||||
if g.coin != 'BTC': die(1,'Test valid only for coin BTC')
|
||||
opt.coin = 'BTC'
|
||||
return self.regtest_setup(name)
|
||||
|
||||
def regtest_split_fork(self,name):
|
||||
opt.coin = 'B2X'
|
||||
t = MMGenExpect(name,'mmgen-regtest',['fork','btc'])
|
||||
t.expect('Creating fork from coin')
|
||||
t.expect('successfully created')
|
||||
t.ok()
|
||||
|
||||
def regtest_split_start(self,name,coin):
|
||||
opt.coin = coin
|
||||
t = MMGenExpect(name,'mmgen-regtest',['bob'])
|
||||
t.expect('Starting')
|
||||
t.expect('done')
|
||||
t.ok()
|
||||
|
||||
def regtest_split_start_btc(self,name): self.regtest_split_start(name,coin='BTC')
|
||||
def regtest_split_start_b2x(self,name): self.regtest_split_start(name,coin='B2X')
|
||||
def regtest_split_gen_btc(self,name): self.regtest_generate(name,coin='BTC')
|
||||
def regtest_split_gen_b2x(self,name): self.regtest_generate(name,coin='B2X',num_blocks=100)
|
||||
def regtest_split_gen_b2x2(self,name): self.regtest_generate(name,coin='B2X')
|
||||
|
||||
def regtest_split_do_split(self,name):
|
||||
opt.coin = 'B2X'
|
||||
sid = self.regtest_user_sid('bob')
|
||||
t = MMGenExpect(name,'mmgen-split',[
|
||||
'--bob',
|
||||
'--outdir='+cfg['tmpdir'],
|
||||
'--tx-fees=0.0001,0.0003',
|
||||
sid+':S:1',sid+':S:2'])
|
||||
t.expect(r"'q'=quit view, .*?:.",'q', regex=True)
|
||||
t.expect('outputs to spend: ','1\n')
|
||||
|
||||
for tx in ('timelocked','split'):
|
||||
for q in ('fee','change'): t.expect('OK? (Y/n): ','y')
|
||||
t.expect('Add a comment to transaction? (y/N): ','n')
|
||||
t.expect('View decoded transaction\? .*?: ','t',regex=True)
|
||||
t.expect('to continue: ','\n')
|
||||
|
||||
t.written_to_file('Long chain (timelocked) transaction')
|
||||
t.written_to_file('Short chain transaction')
|
||||
t.ok()
|
||||
|
||||
def regtest_split_sign(self,name,coin,ext):
|
||||
wf = get_file_with_ext('mmdat',self.regtest_user_dir('bob',coin=coin.lower()))
|
||||
txfile = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
|
||||
opt.coin = coin
|
||||
self.txsign(name,txfile,wf,extra_opts=['--bob'])
|
||||
|
||||
def regtest_split_sign_b2x(self,name):
|
||||
return self.regtest_split_sign(name,coin='B2X',ext='533].rawtx')
|
||||
|
||||
def regtest_split_sign_btc(self,name):
|
||||
return self.regtest_split_sign(name,coin='BTC',ext='9997].rawtx')
|
||||
|
||||
def regtest_split_send(self,name,coin,ext):
|
||||
opt.coin = coin
|
||||
txfile = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
|
||||
self.txsend(name,txfile,really_send=True,extra_opts=['--bob'])
|
||||
|
||||
def regtest_split_send_b2x(self,name):
|
||||
return self.regtest_split_send(name,coin='B2X',ext='533].sigtx')
|
||||
|
||||
def regtest_split_send_btc(self,name):
|
||||
return self.regtest_split_send(name,coin='BTC',ext='9997].sigtx')
|
||||
|
||||
def regtest_split_txdo_timelock(self,name,coin,locktime,bad_locktime):
|
||||
opt.coin = coin
|
||||
sid = self.regtest_user_sid('bob')
|
||||
self.regtest_user_txdo(
|
||||
name,'bob','0.0001',[sid+':S:5'],'1',pw='abc',
|
||||
extra_args=['--locktime='+str(locktime)],
|
||||
bad_locktime=bad_locktime)
|
||||
|
||||
def regtest_split_txdo_timelock_bad_btc(self,name):
|
||||
self.regtest_split_txdo_timelock(name,'BTC',locktime=8888,bad_locktime=True)
|
||||
def regtest_split_txdo_timelock_good_btc(self,name):
|
||||
self.regtest_split_txdo_timelock(name,'BTC',locktime=1321009871,bad_locktime=False)
|
||||
def regtest_split_txdo_timelock_bad_b2x(self,name):
|
||||
self.regtest_split_txdo_timelock(name,'B2X',locktime=8888,bad_locktime=True)
|
||||
def regtest_split_txdo_timelock_good_b2x(self,name):
|
||||
self.regtest_split_txdo_timelock(name,'B2X',locktime=1321009871,bad_locktime=False)
|
||||
|
||||
# def regtest_user_txdo(self,name,user,fee,outputs_cl,outputs_prompt,extra_args=[],wf=None,pw='abc',no_send=False,do_label=False):
|
||||
|
||||
# undocumented admin commands
|
||||
ref_tx_setup = regtest_setup
|
||||
ref_tx_generate = regtest_generate
|
||||
|
||||
|
|
@ -2420,7 +2557,12 @@ class MMGenTestSuite(object):
|
|||
outputs_cl = [sid+':{}:3,1.1234'.format(g.proto.mmtypes[-1]), sid+':C:5,5.5555',sid+':L:4',addr+',100']
|
||||
pw = cfg['wpasswd']
|
||||
# create tx in cwd
|
||||
t = MMGenExpect(name,'mmgen-txcreate',['-B','--bob','--tx-fee='+rtFee[0]] + outputs_cl)
|
||||
t = MMGenExpect(name,'mmgen-txcreate',[
|
||||
'-B',
|
||||
'--bob',
|
||||
'--tx-fee='+rtFee[0],
|
||||
'--locktime=1320969600'
|
||||
] + outputs_cl)
|
||||
# [os.path.join(ref_dir,cfg['ref_wallet'])])
|
||||
t.expect(r"'q'=quit view, .*?:.",'M',regex=True) # sort by mmid
|
||||
t.expect(r"'q'=quit view, .*?:.",'q',regex=True)
|
||||
|
|
@ -2441,7 +2583,7 @@ class MMGenTestSuite(object):
|
|||
with open(fn) as f:
|
||||
lines = f.read().splitlines()
|
||||
|
||||
from mmgen.obj import BTCAmt,LTCAmt,BCHAmt
|
||||
from mmgen.obj import BTCAmt,LTCAmt,BCHAmt,B2XAmt
|
||||
tx = {}
|
||||
for k,i in (('in',3),('out',4)):
|
||||
tx[k] = eval(lines[i])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue