diff --git a/MANIFEST.in b/MANIFEST.in
index ce214517..bfca67ce 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,6 +2,7 @@ include README.md SIGNING_KEY.pub LICENSE INSTALL
include doc/wiki/using-mmgen/*
include test/*.py
include test/ref/*
+include test/ref/litecoin/*
include scripts/bitcoind-walletunlock.py
include scripts/compute-file-chksum.py
diff --git a/README.md b/README.md
index f885c32e..d1b575fb 100644
--- a/README.md
+++ b/README.md
@@ -129,3 +129,4 @@ Donate: 15TLdmi5NYLdqmtCqczUs5pBPkJDXRs83w
[9]: https://cloud.githubusercontent.com/assets/6071028/20677261/6ccab1bc-b58a-11e6-8ab6-094f88befef2.jpg
[r]: https://github.com/mmgen/mmgen/wiki/Recovering-Your-Keys-Without-the-MMGen-Software
[x]: https://github.com/mmgen/mmgen/wiki/Getting-Started-with-MMGen#a_bch
+[z]: https://user-images.githubusercontent.com/6071028/31656274-a35a1252-b31a-11e7-93b7-3d666f50f70f.png
diff --git a/mmgen-addrgen b/cmds/mmgen-addrgen
similarity index 100%
rename from mmgen-addrgen
rename to cmds/mmgen-addrgen
diff --git a/mmgen-addrimport b/cmds/mmgen-addrimport
similarity index 100%
rename from mmgen-addrimport
rename to cmds/mmgen-addrimport
diff --git a/mmgen-keygen b/cmds/mmgen-keygen
similarity index 100%
rename from mmgen-keygen
rename to cmds/mmgen-keygen
diff --git a/mmgen-passchg b/cmds/mmgen-passchg
similarity index 100%
rename from mmgen-passchg
rename to cmds/mmgen-passchg
diff --git a/mmgen-passgen b/cmds/mmgen-passgen
similarity index 93%
rename from mmgen-passgen
rename to cmds/mmgen-passgen
index 0216710e..d5bf204f 100755
--- a/mmgen-passgen
+++ b/cmds/mmgen-passgen
@@ -17,7 +17,7 @@
# this program. If not, see .
"""
-mmgen-passgen: Generate a series or range of passwords from an MMGen
+mmgen-passgen: Generate a range or series of passwords from an MMGen
deterministic wallet
"""
diff --git a/mmgen-regtest b/cmds/mmgen-regtest
similarity index 100%
rename from mmgen-regtest
rename to cmds/mmgen-regtest
diff --git a/mmgen-tool b/cmds/mmgen-tool
similarity index 100%
rename from mmgen-tool
rename to cmds/mmgen-tool
diff --git a/mmgen-txbump b/cmds/mmgen-txbump
similarity index 100%
rename from mmgen-txbump
rename to cmds/mmgen-txbump
diff --git a/mmgen-txcreate b/cmds/mmgen-txcreate
similarity index 100%
rename from mmgen-txcreate
rename to cmds/mmgen-txcreate
diff --git a/mmgen-txdo b/cmds/mmgen-txdo
similarity index 100%
rename from mmgen-txdo
rename to cmds/mmgen-txdo
diff --git a/mmgen-txsend b/cmds/mmgen-txsend
similarity index 100%
rename from mmgen-txsend
rename to cmds/mmgen-txsend
diff --git a/mmgen-txsign b/cmds/mmgen-txsign
similarity index 100%
rename from mmgen-txsign
rename to cmds/mmgen-txsign
diff --git a/mmgen-walletchk b/cmds/mmgen-walletchk
similarity index 100%
rename from mmgen-walletchk
rename to cmds/mmgen-walletchk
diff --git a/mmgen-walletconv b/cmds/mmgen-walletconv
similarity index 100%
rename from mmgen-walletconv
rename to cmds/mmgen-walletconv
diff --git a/mmgen-walletgen b/cmds/mmgen-walletgen
similarity index 100%
rename from mmgen-walletgen
rename to cmds/mmgen-walletgen
diff --git a/data_files/mmgen.cfg b/data_files/mmgen.cfg
index 5bcc757d..f319edc1 100644
--- a/data_files/mmgen.cfg
+++ b/data_files/mmgen.cfg
@@ -23,16 +23,16 @@
# Uncomment to use testnet instead of mainnet:
# testnet true
-# Set the RPC host (the host bitcoind is running on):
+# Set the RPC host (the host the coin daemon is running on):
# rpc_host localhost
# Set the RPC host's port number
# rpc_port 8332
-# Uncomment to override 'rpcuser' in bitcoin.conf
+# Uncomment to override 'rpcuser' from coin daemon config file
# rpc_user myusername
-# Uncomment to override 'rpcpassword' in bitcoin.conf
+# Uncomment to override 'rpcpassword' from coin daemon config file
# rpc_password mypassword
# Uncomment to set the coin daemon datadir
@@ -46,8 +46,14 @@
# A value of 0 disables user entropy, but this is not recommended:
# usr_randchars 30
-# Set the maximum transaction fee in BTC:
-# max_tx_fee 0.01
+# Set the maximum transaction fee for BTC:
+# btc_max_tx_fee 0.01
+
+# Set the maximum transaction fee for BCH:
+# bch_max_tx_fee 0.1
+
+# Set the maximum transaction fee for LTC:
+# ltc_max_tx_fee 0.3
# Set the transaction fee adjustment factor. Auto-calculated fees are
# multiplied by this value:
diff --git a/mmgen/addr.py b/mmgen/addr.py
index fc3a76e4..ff0ec8cb 100755
--- a/mmgen/addr.py
+++ b/mmgen/addr.py
@@ -41,7 +41,7 @@ class AddrGeneratorP2PKH(AddrGenerator):
def to_addr(self,pubhex):
from mmgen.protocol import hash160
assert type(pubhex) == PubKey
- return CoinAddr(g.proto.hexaddr2addr(hash160(pubhex)))
+ return CoinAddr(g.proto.hexaddr2addr(hash160(pubhex),p2sh=False))
def to_segwit_redeem_script(self,pubhex):
raise NotImplementedError
@@ -160,10 +160,9 @@ class AddrListIDStr(unicode,Hilite):
if fmt_str:
ret = fmt_str.format(s)
- elif addrlist.al_id.mmtype == 'L':
- ret = '{}[{}]'.format(addrlist.al_id.sid,s)
else:
- ret = '{}-{}[{}]'.format(addrlist.al_id.sid,addrlist.al_id.mmtype,s)
+ bc,mt = g.proto.base_coin,addrlist.al_id.mmtype
+ ret = '{}{}{}[{}]'.format(addrlist.al_id.sid,('-'+bc,'')[bc=='BTC'],('-'+mt,'')[mt=='L'],s)
return unicode.__new__(cls,ret)
@@ -175,7 +174,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
# This file is editable.
# Everything following a hash symbol '#' is a comment and ignored by {pnm}.
# A text label of {n} characters or less may be added to the right of each
-# address, and it will be appended to the bitcoind wallet label upon import.
+# address, and it will be appended to the tracking wallet label upon import.
# The label may contain any printable ASCII symbol.
""".strip().format(n=TwComment.max_len,pnm=pnm),
'record_chksum': """
@@ -187,7 +186,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
""".strip().format(pnm=pnm)
}
entry_type = AddrListEntry
- main_key = 'addr'
+ main_attr = 'addr'
data_desc = 'address'
file_desc = 'addresses'
gen_desc = 'address'
@@ -197,7 +196,6 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
gen_keys = False
has_keys = False
ext = 'addrs'
- dfl_mmtype = MMGenAddrType('L')
cook_hash_rounds = 10 # not too many rounds, so hand decoding can still be feasible
chksum_rec_f = lambda foo,e: (str(e.idx), e.addr)
@@ -205,7 +203,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
addrlist='',keylist='',mmtype=None,do_chksum=True,chksum_only=False):
self.update_msgs()
- mmtype = mmtype or self.dfl_mmtype
+ mmtype = mmtype or MMGenAddrType.dfl_mmtype
assert mmtype in MMGenAddrType.mmtypes
if seed and addr_idxs: # data from seed + idxs
@@ -296,21 +294,16 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
self.al_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15))
return out
- def is_mainnet(self):
- return self.data[0].addr.is_mainnet()
-
- def is_for_current_chain(self):
- return self.data[0].addr.is_for_current_chain()
-
def check_format(self,addr): return True # format is checked when added to list entry object
def cook_seed(self,seed):
- if self.al_id.mmtype == 'L':
+ is_btcfork = g.proto.base_coin == 'BTC'
+ if is_btcfork and self.al_id.mmtype == 'L':
return seed
else:
from mmgen.crypto import sha256_rounds
import hmac
- key = self.al_id.mmtype.name
+ key = (g.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
cseed = hmac.new(seed,key,sha256).digest()
dmsg('Seed: {}\nKey: {}\nCseed: {}\nCseed len: {}'.format(hexlify(seed),key,hexlify(cseed),len(cseed)))
return sha256_rounds(cseed,self.cook_hash_rounds)
@@ -406,10 +399,10 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
if type(self) == PasswordList:
out.append(u'{} {} {}:{} {{'.format(
self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len))
- elif self.al_id.mmtype == 'L':
- out.append('{} {{'.format(self.al_id.sid))
else:
- out.append('{} {} {{'.format(self.al_id.sid,self.al_id.mmtype.name.upper()))
+ bc,mt = g.proto.base_coin,self.al_id.mmtype
+ lbl = ':'.join(([bc],[])[bc=='BTC']+([mt.name.upper()],[])[mt=='L'])
+ out.append('{} {}{{'.format(self.al_id.sid,('',lbl+' ')[bool(lbl)]))
fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx))
for e in self.data:
@@ -447,16 +440,16 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
if len(d) != 3: d.append('')
- a = le(**{'idx':int(d[0]),self.main_key:d[1],'label':d[2]})
+ a = le(**{'idx':int(d[0]),self.main_attr:d[1],'label':d[2]})
if self.has_keys:
l = lines.pop(0)
d = l.split(None,2)
if d[0] != 'wif:':
- return "Invalid key line in file: '%s'" % l
+ return "Invalid key line in file: '{}'".format(l)
if not is_wif(d[1]):
- return "'%s': invalid Bitcoin key" % d[1]
+ return "'{}': invalid {} key".format(d[1],g.proto.name.capitalize())
a.sec = PrivKey(wif=d[1])
@@ -498,6 +491,38 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
if not is_mmgen_seed_id(sid):
return do_error("'%s': invalid Seed ID" % ls[0])
+ def parse_addrfile_label(lbl): # we must maintain backwards compat, so parse is tricky
+ al_base_coin,al_mmtype = None,None
+ lbl = lbl.split(':',1)
+ if len(lbl) == 2:
+ al_base_coin = lbl[0]
+ al_mmtype = lbl[1].lower()
+ else:
+ if lbl[0].lower() in MMGenAddrType.get_names():
+ al_mmtype = lbl[0].lower()
+ else:
+ al_base_coin = lbl[0]
+
+ # this block fails if al_mmtype is invalid for g.coin
+ if not al_mmtype:
+ mmtype = MMGenAddrType('L')
+ else:
+ try:
+ mmtype = MMGenAddrType(al_mmtype)
+ except:
+ return do_error(u"'{}': invalid address type in address file. Must be one of: {}".format(
+ mmtype.upper(),' '.join([i['name'].upper() for i in MMGenAddrType.mmtypes.values()])))
+
+ from mmgen.protocol import CoinProtocol
+ base_coin = CoinProtocol(al_base_coin or 'BTC',testnet=False).base_coin
+ if not base_coin:
+ die(2,"'{}': unknown base coin in address file label!".format(al_base_coin))
+ return base_coin,mmtype
+
+ def check_coin_mismatch(base_coin): # die if addrfile coin doesn't match g.coin
+ if not base_coin == g.proto.base_coin:
+ die(2,'{} address file format, but base coin is {}!'.format(base_coin,g.proto.base_coin))
+
if type(self) == PasswordList and len(ls) == 2:
ss = ls.pop().split(':')
if len(ss) != 2:
@@ -507,14 +532,11 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
self.pw_id_str = MMGenPWIDString(ls.pop())
mmtype = MMGenPasswordType('P')
elif len(ls) == 1:
- mmtype = ls.pop().lower()
- try:
- mmtype = MMGenAddrType(mmtype)
- except:
- return do_error(u"'{}': invalid address type in address file. Must be one of: {}".format(
- mmtype.upper(),' '.join([i['name'].upper() for i in MMGenAddrType.mmtypes.values()])))
+ base_coin,mmtype = parse_addrfile_label(ls[0])
+ check_coin_mismatch(base_coin)
elif len(ls) == 0:
- mmtype = MMGenAddrType('L')
+ base_coin,mmtype = 'BTC',MMGenAddrType('L')
+ check_coin_mismatch(base_coin)
else:
return do_error(u"Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0]))
@@ -572,7 +594,7 @@ Record this checksum: it will be used to verify the password file in the future
""".strip()
}
entry_type = PasswordListEntry
- main_key = 'passwd'
+ main_attr = 'passwd'
data_desc = 'password'
file_desc = 'passwords'
gen_desc = 'password'
@@ -666,14 +688,14 @@ Record this checksum: it will be used to verify the password file in the future
def cook_seed(self,seed):
from mmgen.crypto import sha256_rounds
- # Changing either pw_fmt, pw_len or id_str will cause a different, unrelated set of
- # passwords to be generated: this is what we want
+ # Changing either pw_fmt, pw_len or cook_str will cause a different,
+ # unrelated set of passwords to be generated: this is what we want.
# NB: In original implementation, pw_id_str was 'baseN', not 'bN'
- fid_str = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8'))
- dmsg(u'Full ID string: {}'.format(fid_str.decode('utf8')))
- # Original implementation was 'cseed = seed + fid_str'; hmac was not used
+ cook_str = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8'))
+ dmsg(u'Full ID string: {}'.format(cook_str.decode('utf8')))
+ # Original implementation was 'cseed = seed + cook_str'; hmac was not used
import hmac
- cseed = hmac.new(seed,fid_str,sha256).digest()
+ cseed = hmac.new(seed,cook_str,sha256).digest()
dmsg('Seed: {}\nCooked seed: {}\nCooked seed len: {}'.format(hexlify(seed),hexlify(cseed),len(cseed)))
return sha256_rounds(cseed,self.cook_hash_rounds)
@@ -712,10 +734,9 @@ re-import your addresses.
def add_tw_data(self):
vmsg('Getting address data from tracking wallet')
- c = rpc_connection()
- accts = c.listaccounts(0,True)
+ accts = g.rpch.listaccounts(0,True)
data,i = {},0
- alists = c.getaddressesbyaccount([[k] for k in accts],batch=True)
+ alists = g.rpch.getaddressesbyaccount([[k] for k in accts],batch=True)
for acct,addrlist in zip(accts,alists):
l = TwLabel(acct,on_fail='silent')
if l and l.mmid.type == 'mmgen':
diff --git a/mmgen/common.py b/mmgen/common.py
index be39fb4b..1ed29bd7 100755
--- a/mmgen/common.py
+++ b/mmgen/common.py
@@ -26,16 +26,79 @@ import mmgen.opts as opts
from mmgen.opts import opt
from mmgen.util import *
-pw_note = """
+def help_notes(k):
+ from mmgen.seed import SeedSource
+ return {
+ 'passwd': """
For passphrases all combinations of whitespace are equal and leading and
trailing space is ignored. This permits reading passphrase or brainwallet
data from a multi-line file with free spacing and indentation.
-""".strip()
-
-bw_note = """
+""".strip(),
+ 'brainwallet': """
BRAINWALLET NOTE:
To thwart dictionary attacks, it's recommended to use a strong hash preset
with brainwallets. For a brainwallet passphrase to generate the correct
seed, the same seed length and hash preset parameters must always be used.
-""".strip()
+""".strip(),
+ 'txcreate': """
+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 on the command line (see FEE
+SPECIFICATION below), it will be calculated dynamically using {dn}'s
+"estimatefee" function for the default (or user-specified) number of
+confirmations. If "estimatefee" fails, the user will be prompted for a fee.
+
+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 one per {g.proto.secs_per_block} seconds.
+
+All addresses on the command line can be either {pnu} addresses or {pnm}
+addresses of the form :.
+
+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=g.proj_name,
+ dn=g.proto.daemon_name,
+ pnu=g.proto.name.capitalize()),
+ 'fee': """
+FEE SPECIFICATION: Transaction fees, both on the command line and at the
+interactive prompt, may be specified as either absolute {} amounts, using
+a plain decimal number, or as satoshis per byte, using an integer followed by
+the letter 's'.
+""".format(g.coin),
+ 'txsign': """
+Transactions may contain both {pnm} or non-{pnm} input addresses.
+
+To sign non-{pnm} inputs, a {dn} wallet dump or flat key list is used
+as the key source ('--keys-from-file' option).
+
+To sign {pnm} inputs, key data is generated from a seed as with the
+{pnl}-addrgen and {pnl}-keygen commands. Alternatively, a key-address file
+may be used (--mmgen-keys-from-file option).
+
+Multiple wallets or other seed files can be listed on the command line in
+any order. If the seeds required to sign the transaction's inputs are not
+found in these files (or in the default wallet), the user will be prompted
+for seed data interactively.
+
+To prevent an attacker from crafting transactions with bogus {pnm}-to-{pnu}
+address mappings, all outputs to {pnm} addresses are verified with a seed
+source. Therefore, seed files or a key-address file for all {pnm} outputs
+must also be supplied on the command line if the data can't be found in the
+default wallet.
+
+Seed source files must have the canonical extensions listed in the 'FileExt'
+column below:
+
+ {n_fmt}
+""".format( dn=g.proto.daemon_name,
+ n_fmt='\n '.join(SeedSource.format_fmt_codes().splitlines()),
+ pnm=g.proj_name,
+ pnu=g.proto.name.capitalize(),
+ pnl=g.proj_name.lower())
+}[k]
diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py
index 3131dc93..d6aec36e 100755
--- a/mmgen/globalvars.py
+++ b/mmgen/globalvars.py
@@ -21,7 +21,6 @@ globalvars.py: Constants and configuration options for the MMGen suite
"""
import sys,os
-from mmgen.obj import BTCAmt
# Global vars are set to dfl values in class g.
# They're overridden in this order:
@@ -36,7 +35,8 @@ class g(object):
def die(ev=0,s=''):
if s: sys.stderr.write(s+'\n')
sys.exit(ev)
- # Variables - these might be altered at runtime:
+
+ # Constants:
version = '0.9.499'
release_date = 'October 2017'
@@ -47,27 +47,24 @@ class g(object):
author = 'Philemon'
email = ''
Cdates = '2013-2017'
- keywords = 'Bitcoin, cryptocurrency, wallet, cold storage, offline, online, spending, open-source, command-line, Python, Linux, Bitcoin Core, bitcoind, hd, deterministic, hierarchical, secure, anonymous, Electrum, seed, mnemonic, brainwallet, Scrypt, utility, script, scriptable, blockchain, raw, transaction, permissionless, console, terminal, curses, ansi, color, tmux, remote, client, daemon, RPC, json, entropy, xterm, rxvt, PowerShell, MSYS, MinGW, mswin, Armbian, Raspbian, Raspberry Pi, Orange Pi'
+ keywords = 'Bitcoin, BTC, cryptocurrency, wallet, cold storage, offline, online, spending, open-source, command-line, Python, Linux, Bitcoin Core, bitcoind, hd, deterministic, hierarchical, secure, anonymous, Electrum, seed, mnemonic, brainwallet, Scrypt, utility, script, scriptable, blockchain, raw, transaction, permissionless, console, terminal, curses, ansi, color, tmux, remote, client, daemon, RPC, json, entropy, xterm, rxvt, PowerShell, MSYS, MinGW, mswin, Armbian, Raspbian, Raspberry Pi, Orange Pi, BCash, BCH, Litecoin, LTC, altcoin'
+ max_int = 0xffffffff
+ stdin_tty = bool(sys.stdin.isatty() or os.getenv('MMGEN_TEST_SUITE'))
+ http_timeout = 60
- coin = 'BTC'
- coins = 'BTC','BCH'
+ # Variables - these might be altered at runtime:
user_entropy = ''
hash_preset = '3'
usr_randchars = 30
- stdin_tty = bool(sys.stdin.isatty() or os.getenv('MMGEN_TEST_SUITE'))
- 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 in opts.py, but they don't change thereafter
+ # Constant vars - some of these might be overriden in opts.py, but they don't change thereafter
+ coin = 'BTC'
debug = False
quiet = False
no_license = False
@@ -76,13 +73,14 @@ class g(object):
force_256_color = False
testnet = False
regtest = False
- chain = None # set by first call to rpc_connection()
+ chain = None # set by first call to rpc_init()
chains = 'mainnet','testnet','regtest'
- bitcoind_version = None # set by first call to rpc_connection()
+ daemon_version = None # set by first call to rpc_init()
rpc_host = ''
rpc_port = 0
rpc_user = ''
rpc_password = ''
+ rpch = None # global RPC handle
bob = False
alice = False
@@ -128,7 +126,8 @@ class g(object):
cfg_file_opts = (
'color','debug','hash_preset','http_timeout','no_license','rpc_host','rpc_port',
'quiet','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password',
- 'daemon_data_dir','force_256_color','max_tx_fee','regtest'
+ 'daemon_data_dir','force_256_color','regtest',
+ 'btc_max_tx_fee','ltc_max_tx_fee','bch_max_tx_fee'
)
env_opts = (
'MMGEN_BOGUS_WALLET_DATA',
@@ -151,7 +150,6 @@ class g(object):
global_sets_opt = ['minconf','seed_len','hash_preset','usr_randchars','debug',
'quiet','tx_confs','tx_fee_adj','key_generator']
- mins_per_block = 9
passwd_max_tries = 5
max_urandchars = 80
diff --git a/mmgen/main_addrgen.py b/mmgen/main_addrgen.py
index 13be495c..9827ff8d 100755
--- a/mmgen/main_addrgen.py
+++ b/mmgen/main_addrgen.py
@@ -98,15 +98,18 @@ ADDRESS TYPES:
NOTES FOR ALL GENERATOR COMMANDS
-{pwn}
+{n_pw}
-{bwn}
+{n_bw}
FMT CODES:
- {f}
+ {n_fmt}
""".format(
- n_secp=note_secp256k1,n_addrkey=note_addrkey,pwn=pw_note,bwn=bw_note,
- f='\n '.join(SeedSource.format_fmt_codes().splitlines()),
+ n_secp=note_secp256k1,
+ n_addrkey=note_addrkey,
+ n_pw=help_notes('passwd'),
+ n_bw=help_notes('brainwallet'),
+ n_fmt='\n '.join(SeedSource.format_fmt_codes().splitlines()),
n_at='\n '.join(["'{}','{:<12} - {}".format(k,v['name']+"'",v['desc']) for k,v in MAT.mmtypes.items()]),
o=opts
)
diff --git a/mmgen/main_addrimport.py b/mmgen/main_addrimport.py
index 44edfbd0..068bafc8 100755
--- a/mmgen/main_addrimport.py
+++ b/mmgen/main_addrimport.py
@@ -17,7 +17,7 @@
# along with this program. If not, see .
"""
-mmgen-addrimport: Import addresses into a MMGen bitcoind tracking wallet
+mmgen-addrimport: Import addresses into a MMGen coin daemon tracking wallet
"""
import time
@@ -26,7 +26,25 @@ from mmgen.common import *
from mmgen.addr import AddrList,KeyAddrList
from mmgen.obj import TwLabel
-# In batch mode, bitcoind just rescans each address separately anyway, so make
+ai_msgs = lambda k: {
+ 'rescan': """
+WARNING: You've chosen the '--rescan' option. Rescanning the blockchain is
+necessary only if an address you're importing is already on the blockchain,
+has a balance and is not in your tracking wallet. Note that the rescanning
+process is very slow (>30 min. for each imported address on a low-powered
+computer).
+ """.strip() if opt.rescan else """
+WARNING: If any of the addresses you're importing is already on the blockchain,
+has a balance and is not in your tracking wallet, you must exit the program now
+and rerun it using the '--rescan' option.
+""".strip(),
+ 'bad_args': """
+You must specify an {pnm} address file, a single address with the '--address'
+option, or a list of non-{pnm} addresses with the '--addrlist' option
+""".strip().format(pnm=g.proj_name)
+}[k]
+
+# In batch mode, daemon just rescans each address separately anyway, so make
# --batch and --rescan incompatible.
opts_data = lambda: {
@@ -36,14 +54,13 @@ opts_data = lambda: {
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
--a, --address=a Import the single Bitcoin address 'a'
+-a, --address=a Import the single coin address 'a'
-b, --batch Import all addresses in one RPC call.
--l, --addrlist Address source is a flat list of (non-MMGen) Bitcoin addresses
+-l, --addrlist Address source is a flat list of non-MMGen coin addresses
-k, --keyaddr-file Address source is a key-address file
-q, --quiet Suppress warnings
-r, --rescan Rescan the blockchain. Required if address to import is
on the blockchain and has a balance. Rescanning is slow.
--t, --test Simulate operation; don't actually import addresses
""",
'notes': """\n
This command can also be used to update the comment fields of addresses already
@@ -63,109 +80,91 @@ def import_mmgen_list(infile):
rdie(2,'Segwit is not active on this chain. Cannot import Segwit addresses')
return al
+rpc_init()
+
if len(cmd_args) == 1:
infile = cmd_args[0]
check_infile(infile)
if opt.addrlist:
- lines = get_lines_from_file(
- infile,'non-{pnm} addresses'.format(pnm=g.proj_name),trim_comments=True)
- al = AddrList(addrlist=lines)
+ al = AddrList(addrlist=get_lines_from_file(
+ infile,
+ 'non-{pnm} addresses'.format(pnm=g.proj_name),
+ trim_comments=True))
else:
al = import_mmgen_list(infile)
elif len(cmd_args) == 0 and opt.address:
al = AddrList(addrlist=[opt.address])
infile = 'command line'
else:
- die(1,"""
-You must specify an {pnm} address file, a single address, or a list of
-non-{pnm} addresses with the '--addrlist' option)
-""".strip().format(pnm=g.proj_name))
+ die(1,ai_msgs('bad_args'))
m = ' from Seed ID {}'.format(al.al_id.sid) if hasattr(al.al_id,'sid') else ''
qmsg('OK. {} addresses{}'.format(al.num_addrs,m))
-if not opt.test:
- c = rpc_connection()
-
-m = """
-WARNING: You've chosen the '--rescan' option. Rescanning the blockchain is
-necessary only if an address you're importing is already on the blockchain,
-has a balance and is not in your tracking wallet. Note that the rescanning
-process is very slow (>30 min. for each imported address on a low-powered
-computer).
- """.strip() if opt.rescan else """
-WARNING: If any of the addresses you're importing is already on the blockchain,
-has a balance and is not in your tracking wallet, you must exit the program now
-and rerun it using the '--rescan' option.
-""".strip()
-
-if not opt.quiet: confirm_or_exit(m, 'continue', expect='YES')
+if not opt.quiet: confirm_or_exit(ai_msgs('rescan'),'continue',expect='YES')
err_flag = False
def import_address(addr,label,rescan):
try:
- if not opt.test:
- c.importaddress(addr,label,rescan,timeout=(False,3600)[rescan])
+ g.rpch.importaddress(addr,label,rescan,timeout=(False,3600)[rescan])
except:
global err_flag
err_flag = True
w_n_of_m = len(str(al.num_addrs)) * 2 + 2
-w_mmid = '' if opt.addrlist or opt.address else len(str(max(al.idxs()))) + 12
+w_mmid = 1 if opt.addrlist or opt.address else len(str(max(al.idxs()))) + 13
+msg_fmt = '{{:{}}} {{:34}} {{:{}}}'.format(w_n_of_m,w_mmid)
-if opt.rescan:
- import threading
- msg_fmt = '\r%s %-{}s %-34s %s'.format(w_n_of_m)
-else:
- msg_fmt = '\r%-{}s %-34s %s'.format(w_n_of_m, w_mmid)
+if opt.rescan: import threading
-msg("Importing {} address{} from {}{}".format(
- len(al.data), suf(al.data,'es'), infile,
- ('',' (batch mode)')[bool(opt.batch)]
- ))
+msg('Importing {} address{} from {}{}'.format(
+ len(al.data),
+ suf(al.data,'es'),
+ infile,
+ ('',' (batch mode)')[bool(opt.batch)]))
-if not al.is_for_current_chain():
- die(2,"Address{} not compatible with {} chain!".format((' list','')[bool(opt.address)],g.chain))
+if not al.data[0].addr.is_for_chain(g.chain):
+ die(2,'Address{} not compatible with {} chain!'.format((' list','')[bool(opt.address)],g.chain))
-arg_list = []
for n,e in enumerate(al.data):
if e.idx:
label = '{}:{}'.format(al.al_id,e.idx)
if e.label: label += ' ' + e.label
m = label
else:
- label = 'btc:{}'.format(e.addr)
+ label = '{}:{}'.format(g.proto.base_coin.lower(),e.addr)
m = 'non-'+g.proj_name
label = TwLabel(label)
if opt.batch:
+ if n == 0: arg_list = []
arg_list.append((e.addr,label,False))
- elif opt.rescan:
+ continue
+
+ msg_data = ('{}/{}:'.format(n+1,al.num_addrs),e.addr,'({})'.format(m))
+
+ if opt.rescan:
t = threading.Thread(target=import_address,args=[e.addr,label,True])
t.daemon = True
t.start()
-
start = int(time.time())
-
while True:
if t.is_alive():
- elapsed = int(time.time() - start)
- count = '%s/%s:' % (n+1, al.num_addrs)
- msg_r(msg_fmt % (secs_to_hms(elapsed),count,e.addr,'(%s)' % m))
- time.sleep(1)
+ elapsed = int(time.time()-start)
+ msg_r(('\r{} '+msg_fmt).format(secs_to_hms(elapsed),*msg_data))
+ time.sleep(0.5)
else:
if err_flag: die(2,'\nImport failed')
msg('\nOK')
break
else:
import_address(e.addr,label,False)
- count = '%s/%s:' % (n+1, al.num_addrs)
- msg_r(msg_fmt % (count, e.addr, '(%s)' % m))
+ msg_r('\r'+msg_fmt.format(*msg_data))
if err_flag: die(2,'\nImport failed')
msg(' - OK')
if opt.batch:
- ret = c.importaddress(arg_list,batch=True)
- msg('OK: %s addresses imported' % len(ret))
+ ret = g.rpch.importaddress(arg_list,batch=True)
+ msg('OK: {} addresses imported'.format(len(ret)))
diff --git a/mmgen/main_passgen.py b/mmgen/main_passgen.py
index 7c7897b1..8d089c55 100755
--- a/mmgen/main_passgen.py
+++ b/mmgen/main_passgen.py
@@ -98,18 +98,19 @@ EXAMPLE:
NOTES FOR ALL GENERATOR COMMANDS
-{pwn}
+{n_pw}
-{bwn}
+{n_bw}
FMT CODES:
- {f}
+ {n_fmt}
""".format(
- f='\n '.join(SeedSource.format_fmt_codes().splitlines()),
o=opts,g=g,d58=dfl_len['b58'],d32=dfl_len['b32'],
ml=MMGenPWIDString.max_len,
- pwn=pw_note,bwn=bw_note,
- fs="', '".join(MMGenPWIDString.forbidden)
+ fs="', '".join(MMGenPWIDString.forbidden),
+ n_pw=help_notes('passwd'),
+ n_bw=help_notes('brainwallet'),
+ n_fmt='\n '.join(SeedSource.format_fmt_codes().splitlines())
)
}
diff --git a/mmgen/main_regtest.py b/mmgen/main_regtest.py
index 823ae5b0..e3bd887a 100755
--- a/mmgen/main_regtest.py
+++ b/mmgen/main_regtest.py
@@ -17,13 +17,14 @@
# along with this program. If not, see .
"""
-mmgen-regtest: Bitcoind regression test mode setup and operations for the MMGen
+mmgen-regtest: Coin daemon regression test mode setup and operations for the MMGen
suite
"""
from mmgen.common import *
+
opts_data = lambda: {
- 'desc': 'Bitcoind regression test mode setup and operations for the {} suite'.format(g.proj_name),
+ 'desc': 'Coin daemon regression test mode setup and operations for the {} suite'.format(g.proj_name),
'usage': '[opts] ',
'sets': ( ('yes', True, 'quiet', True), ),
'options': """
@@ -40,7 +41,7 @@ opts_data = lambda: {
AVAILABLE COMMANDS
setup - set up system for regtest operation with MMGen
- stop - stop the regtest bitcoind
+ 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
diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py
index 884c677f..76614fde 100755
--- a/mmgen/main_tool.py
+++ b/mmgen/main_tool.py
@@ -17,7 +17,7 @@
# along with this program. If not, see .
"""
-mmgen-tool: Perform various MMGen- and Bitcoin-related operations.
+mmgen-tool: Perform various MMGen- and cryptocoin-related operations.
Part of the MMGen suite
"""
@@ -29,24 +29,24 @@ supported commands), use '-' as the first argument.
""".strip()
cmd_help = """
-Bitcoin address/key operations (compressed public keys supported):
- addr2hexaddr - convert Bitcoin address from base58 to hex format
+Cryptocoin address/key operations (compressed public keys supported):
+ addr2hexaddr - convert coin address from base58 to hex format
hex2wif - convert a private key from hex to WIF format
- hexaddr2addr - convert Bitcoin address from hex to base58 format
- privhex2addr - generate Bitcoin address from private key in hex format
+ hexaddr2addr - convert coin address from hex to base58 format
+ privhex2addr - generate coin address from private key in hex format
privhex2pubhex - generate a hex public key from a hex private key
pubhex2addr - convert a hex pubkey to an address
pubhex2redeem_script - convert a hex pubkey to a witness redeem script
wif2redeem_script - convert a WIF private key to a witness redeem script
wif2segwit_pair - generate both a Segwit redeem script and address from WIF
- pubkey2addr - convert Bitcoin public key to address
+ pubkey2addr - convert coin public key to address
randpair - generate a random private key/address pair
randwif - generate a random private key in WIF format
- wif2addr - generate a Bitcoin address from a key in WIF format
+ wif2addr - generate a coin address from a key in WIF format
wif2hex - convert a private key from WIF to hex format
-Wallet/TX operations (bitcoind must be running):
- getbalance - like 'bitcoin-cli getbalance' but shows confirmed/unconfirmed,
+Wallet/TX operations (coin daemon must be running):
+ getbalance - like '{pn}-cli getbalance' but shows confirmed/unconfirmed,
spendable/unspendable balances for individual {pnm} wallets
listaddress - list the specified {pnm} address and its balance
listaddresses - list {pnm} addresses and their balances
@@ -104,10 +104,10 @@ Mnemonic operations (choose 'electrum' (default), 'tirosh' or 'all'
computed using a different algorithm and are NOT Electrum-compatible!
{sm}
-""".format(pnm=g.proj_name,sm='\n '.join(stdin_msg.split('\n')))
+"""
opts_data = lambda: {
- 'desc': 'Perform various {pnm}- and Bitcoin-related operations'.format(pnm=g.proj_name),
+ 'desc': 'Perform various {pnm}- and cryptocoin-related operations'.format(pnm=g.proj_name),
'usage': '[opts] ',
'options': """
-d, --outdir= d Specify an alternate directory 'd' for output
@@ -122,9 +122,14 @@ opts_data = lambda: {
'notes': """
COMMANDS
-{}
-Type '{} help for help on a particular command
-""".format(cmd_help,g.prog_name)
+{ch}
+Type '{pn} help for help on a particular command
+""".format( pn=g.prog_name,
+ ch=cmd_help.format(
+ pn=g.proto.name,
+ pnm=g.proj_name,
+ sm='\n '.join(stdin_msg.split('\n')))
+ )
}
cmd_args = opts.init(opts_data,add_opts=['hidden_incog_input_params','in_fmt'])
diff --git a/mmgen/main_txbump.py b/mmgen/main_txbump.py
index e4edd8ec..6b04b976 100755
--- a/mmgen/main_txbump.py
+++ b/mmgen/main_txbump.py
@@ -21,8 +21,8 @@ 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 *
+from mmgen.common import *
+from mmgen.seed import SeedSource
opts_data = lambda: {
'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),
@@ -58,29 +58,33 @@ opts_data = lambda: {
-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'
+-P, --passwd-file= f Get {pnm} wallet or {dn} 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(),
+""".format(g=g,pnm=g.proj_name,pnl=g.proj_name.lower(),dn=g.proto.daemon_name,
kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
kg=g.key_generator,
cu=g.coin
),
- 'notes': '\n' + fee_notes.format(g.coin) + txsign_notes
+ 'notes': '\n' + help_notes('fee') + help_notes('txsign')
}
cmd_args = opts.init(opts_data)
-c = rpc_connection()
+rpc_init()
tx_file = cmd_args.pop(0)
check_infile(tx_file)
+from mmgen.txcreate import *
+from mmgen.txsign import *
+
seed_files = get_seed_files(opt,cmd_args) if (cmd_args or opt.send) else None
+
kal = get_keyaddrlist(opt)
kl = get_keylist(opt)
@@ -112,13 +116,13 @@ 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
+assert d == fee and d <= g.proto.max_tx_fee
if not opt.yes:
tx.add_comment() # edits an existing comment
-tx.create_raw(c) # creates tx.hex, tx.txid
+tx.create_raw() # creates tx.hex, tx.txid
tx.add_timestamp()
-tx.add_blockcount(c)
+tx.add_blockcount()
qmsg('Fee successfully increased')
@@ -127,9 +131,9 @@ if not silent:
msg_r(tx.format_view(terse=True))
if seed_files or kl or kal:
- txsign(opt,c,tx,seed_files,kl,kal)
+ txsign(tx,seed_files,kl,kal)
tx.write_to_file(ask_write=False)
- if tx.send(c):
+ if tx.send():
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)
diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py
index f15b21a9..a94479ae 100755
--- a/mmgen/main_txcreate.py
+++ b/mmgen/main_txcreate.py
@@ -17,14 +17,14 @@
# along with this program. If not, see .
"""
-mmgen-txcreate: Create a Bitcoin transaction to and from MMGen- or non-MMGen
+mmgen-txcreate: Create a cryptocoin transaction with MMGen- and/or non-MMGen
inputs and outputs
"""
-from mmgen.txcreate import *
+from mmgen.common import *
opts_data = lambda: {
- 'desc': 'Create a transaction with outputs to specified Bitcoin or {g.proj_name} addresses'.format(g=g),
+ 'desc': 'Create a transaction with outputs to specified coin or {g.proj_name} addresses'.format(g=g),
'usage': '[opts] ... [change addr] [addr file] ...',
'sets': ( ('yes', True, 'quiet', True), ),
'options': """
@@ -37,7 +37,7 @@ opts_data = lambda: {
-d, --outdir= d Specify an alternate directory 'd' for output
-f, --tx-fee= f Transaction fee, as a decimal {cu} amount or in satoshis
per byte (an integer followed by 's'). If omitted, fee
- will be calculated using bitcoind's 'estimatefee' call
+ will be calculated using {dn}'s 'estimatefee' call
-i, --info Display unspent outputs and exit
-m, --minconf= n Minimum number of confirmations required to spend
outputs (default: 1)
@@ -45,11 +45,12 @@ opts_data = lambda: {
-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,cu=g.coin),
- 'notes': '\n' + txcreate_notes + fee_notes.format(g.coin)
+""".format(g=g,cu=g.coin,dn=g.proto.daemon_name),
+ 'notes': '\n' + help_notes('txcreate') + help_notes('fee')
}
cmd_args = opts.init(opts_data)
-do_license_msg()
+
+from mmgen.txcreate import *
tx = txcreate(cmd_args,do_info=opt.info)
tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False)
diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py
index 1ea0a9a9..07b94316 100755
--- a/mmgen/main_txdo.py
+++ b/mmgen/main_txdo.py
@@ -20,8 +20,8 @@
mmgen-txdo: Create, sign and broadcast an online MMGen transaction
"""
-from mmgen.txcreate import *
-from mmgen.txsign import *
+from mmgen.common import *
+from mmgen.seed import SeedSource
opts_data = lambda: {
'desc': 'Create, sign and send an {g.proj_name} transaction'.format(g=g),
@@ -40,7 +40,7 @@ opts_data = lambda: {
-e, --echo-passphrase Print passphrase to screen when typing it
-f, --tx-fee= f Transaction fee, as a decimal {cu} amount or in
satoshis per byte (an integer followed by 's').
- If omitted, bitcoind's 'estimatefee' will be used
+ If omitted, {dn}'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)
@@ -68,27 +68,29 @@ opts_data = lambda: {
-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(),
+""".format(g=g,pnm=g.proj_name,pnl=g.proj_name.lower(),dn=g.proto.daemon_name,
kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
kg=g.key_generator,
- cu=g.coin
- ),
- 'notes': '\n' + txcreate_notes + fee_notes.format(g.coin) + txsign_notes
+ cu=g.coin),
+ 'notes': '\n' + help_notes('txcreate') + help_notes('fee') + help_notes('txsign')
}
cmd_args = opts.init(opts_data)
+rpc_init()
+
+from mmgen.txcreate import *
+from mmgen.txsign import *
+
seed_files = get_seed_files(opt,cmd_args)
-c = rpc_connection()
-do_license_msg()
kal = get_keyaddrlist(opt)
kl = get_keylist(opt)
if kl and kal: kl.remove_dup_keys(kal)
tx = txcreate(cmd_args,caller='txdo')
-txsign(opt,c,tx,seed_files,kl,kal)
+txsign(tx,seed_files,kl,kal)
tx.write_to_file(ask_write=False)
-if tx.send(c):
+if tx.send():
tx.write_to_file(ask_overwrite=False,ask_write=False)
diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py
index ef139331..fb30305a 100755
--- a/mmgen/main_txsend.py
+++ b/mmgen/main_txsend.py
@@ -21,11 +21,9 @@ mmgen-txsend: Broadcast a transaction signed by 'mmgen-txsign' to the network
"""
from mmgen.common import *
-from mmgen.tx import *
opts_data = lambda: {
- 'desc': 'Send a Bitcoin transaction signed by {pnm}-txsign'.format(
- pnm=g.proj_name.lower()),
+ 'desc': 'Send a cryptocoin transaction signed by {pnm}-txsign'.format(pnm=g.proj_name.lower()),
'usage': '[opts] ',
'sets': ( ('yes', True, 'quiet', True), ),
'options': """
@@ -40,22 +38,25 @@ opts_data = lambda: {
cmd_args = opts.init(opts_data)
+rpc_init()
+
if len(cmd_args) == 1:
infile = cmd_args[0]; check_infile(infile)
else: opts.usage()
if not opt.status: do_license_msg()
-c = rpc_connection()
+from mmgen.tx import *
+
tx = MMGenTX(infile) # sig check performed here
vmsg("Signed transaction file '%s' is valid" % infile)
-if not tx.marked_signed(c):
+if not tx.marked_signed():
die(1,'Transaction is not signed!')
if opt.status:
if tx.coin_txid: qmsg('{} txid: {}'.format(g.coin,tx.coin_txid.hl()))
- tx.get_status(c,status=True)
+ tx.get_status(status=True)
sys.exit(0)
if not opt.yes:
@@ -63,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(c):
+if tx.send():
tx.write_to_file(ask_overwrite=False,ask_write=False)
diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py
index af5321b3..56f0bd8c 100755
--- a/mmgen/main_txsign.py
+++ b/mmgen/main_txsign.py
@@ -20,11 +20,12 @@
mmgen-txsign: Sign a transaction generated by 'mmgen-txcreate'
"""
-from mmgen.txsign import *
+from mmgen.common import *
+from mmgen.seed import SeedSource
-# -w, --use-wallet-dat (keys from running bitcoind) removed: use bitcoin-cli walletdump instead
+# -w, --use-wallet-dat (keys from running coin daemon) removed: use walletdump rpc instead
opts_data = lambda: {
- 'desc': 'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnm.lower()),
+ 'desc': 'Sign cryptocoin transactions generated by {pnl}-txcreate'.format(pnl=g.proj_name.lower()),
'usage': '[opts] ... [seed source]...',
'sets': ( ('yes', True, 'quiet', True), ),
'options': """
@@ -53,19 +54,19 @@ opts_data = lambda: {
online signing without an {pnm} seed source. The
key-address file is also used to verify {pnm}-to-{cu}
mappings, so the user should record its checksum.
--P, --passwd-file= f Get {pnm} wallet or bitcoind passphrase from file 'f'
+-P, --passwd-file= f Get {pnm} wallet or {dn} passphrase from file 'f'
-q, --quiet Suppress warnings; overwrite files without prompting
-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(),
+ g=g,pnm=g.proj_name,pnl=g.proj_name.lower(),dn=g.proto.daemon_name,
kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
kg=g.key_generator,
cu=g.coin
),
- 'notes': '\n' + txsign_notes
+ 'notes': '\n' + help_notes('txsign')
}
infiles = opts.init(opts_data,add_opts=['b16'])
@@ -73,11 +74,13 @@ infiles = opts.init(opts_data,add_opts=['b16'])
if not infiles: opts.usage()
for i in infiles: check_infile(i)
-c = rpc_connection()
+rpc_init()
if not opt.info and not opt.terse_info:
do_license_msg(immed=True)
+from mmgen.txsign import *
+
tx_files = get_tx_files(opt,infiles)
seed_files = get_seed_files(opt,infiles)
@@ -105,7 +108,7 @@ for tx_num,tx_file in enumerate(tx_files,1):
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)
+ txsign(tx,seed_files,kl,kal,tx_num_str)
if not opt.yes:
tx.add_comment() # edits an existing comment
diff --git a/mmgen/main_wallet.py b/mmgen/main_wallet.py
index 8c739de1..3a809a0e 100755
--- a/mmgen/main_wallet.py
+++ b/mmgen/main_wallet.py
@@ -30,8 +30,8 @@ usage = '[opts] [infile]'
nargs = 1
iaction = 'convert'
oaction = 'convert'
-
invoked_as = 'passchg' if g.prog_name == 'mmgen-passchg' else g.prog_name.partition('-wallet')[2]
+bw_note = True
# full: defhHiJkKlLmoOpPqrSvz-
if invoked_as == 'gen':
@@ -51,7 +51,7 @@ elif invoked_as == 'passchg':
desc = 'Change the passphrase, hash preset or label of an {pnm} wallet'
opt_filter = 'efhdiHkKOlLmpPqrSvz-'
iaction = 'input'
- bw_note = ''
+ bw_note = False
else:
die(1,"'%s': unrecognized invocation" % g.prog_name)
@@ -97,14 +97,14 @@ opts_data = lambda: {
),
'notes': """
-{pwn}{bwn}
+{n_pw}{n_bw}
FMT CODES:
{f}
""".format(
f='\n '.join(SeedSource.format_fmt_codes().splitlines()),
- pwn=pw_note,
- bwn=('','\n\n' + bw_note)[bool(bw_note)]
+ n_pw=help_notes('passwd'),
+ n_bw=('','\n\n' + help_notes('brainwallet'))[bw_note]
)
}
diff --git a/mmgen/obj.py b/mmgen/obj.py
index 8816d939..8c15dcec 100755
--- a/mmgen/obj.py
+++ b/mmgen/obj.py
@@ -285,6 +285,8 @@ class BTCAmt(Decimal,Hilite,InitErrors):
color = 'yellow'
max_prec = 8
max_amt = 21000000
+ min_coin_unit = Decimal('0.00000001')
+
def __new__(cls,num,on_fail='die'):
if type(num) == cls: return num
cls.arg_chk(cls,on_fail)
@@ -297,8 +299,8 @@ class BTCAmt(Decimal,Hilite,InitErrors):
assert me >= 0,'coin amount cannot be negative'
return me
except Exception as e:
- m = "{!r}: value cannot be converted to BTCAmt ({})"
- return cls.init_fail(m.format(num,e[0]),on_fail)
+ m = "{!r}: value cannot be converted to {} ({})"
+ return cls.init_fail(m.format(num,cls.__name__,e[0]),on_fail)
@classmethod
def fmtc(cls):
@@ -347,24 +349,29 @@ class BTCAmt(Decimal,Hilite,InitErrors):
def __neg__(self,other,context=None):
return type(self)(Decimal.__neg__(self,other,context))
+class BCHAmt(BTCAmt):
+ pass
+class LTCAmt(BTCAmt):
+ max_amt = 84000000
+
class CoinAddr(str,Hilite,InitErrors,MMGenObject):
color = 'cyan'
width = 35 # max len of testnet p2sh addr
def __new__(cls,s,on_fail='die'):
if type(s) == cls: return s
cls.arg_chk(cls,on_fail)
+ from mmgen.globalvars import g
try:
assert set(s) <= set(ascii_letters+digits),'contains non-ascii characters'
me = str.__new__(cls,s)
- from mmgen.globalvars import g
va = g.proto.verify_addr(s,return_dict=True)
assert va,'failed verification'
me.addr_fmt = va['format']
me.hex = va['hex']
return me
except Exception as e:
- m = "{!r}: value cannot be converted to Bitcoin address ({})"
- return cls.init_fail(m.format(s,e[0]),on_fail)
+ m = "{!r}: value cannot be converted to {} address ({})"
+ return cls.init_fail(m.format(s,g.proto.__name__,e[0]),on_fail)
@classmethod
def fmtc(cls,s,**kwargs):
@@ -376,22 +383,17 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject):
s = s[:kwargs['width']-2] + '..'
return Hilite.fmtc(s,**kwargs)
- def is_for_current_chain(self):
+ def is_for_chain(self,chain):
from mmgen.globalvars import g
- assert g.chain,'global chain variable unset'
- return self[0] in g.proto.get_chain_protocol(g.chain).addr_pfxs
-
- def is_mainnet(self):
- from mmgen.globalvars import g
- return self[0] in g.proto.get_chain_protocol('mainnet').addr_pfxs
-
- def is_testnet(self):
- from mmgen.globalvars import g
- return self[0] in g.proto.get_chain_protocol('testnet').addr_pfxs
+ vn = g.proto.get_protocol_by_chain(chain).addr_ver_num
+ if self.addr_fmt == 'p2sh' and 'p2sh2' in vn:
+ return self[0] in vn['p2sh'][1] or self[0] in vn['p2sh2'][1]
+ else:
+ return self[0] in vn[self.addr_fmt][1]
def is_in_tracking_wallet(self):
- from mmgen.rpc import rpc_connection
- d = rpc_connection().validateaddress(self)
+ from mmgen.rpc import rpc_init
+ d = rpc_init().validateaddress(self)
return d['iswatchonly'] and 'account' in d
class SeedID(str,Hilite,InitErrors):
@@ -452,7 +454,9 @@ class TwMMGenID(str,Hilite,InitErrors,MMGenObject):
sort_key,idtype = ret.sort_key,'mmgen'
except Exception as e:
try:
- assert s[:4] == 'btc:',"not a string beginning with the prefix 'btc:'"
+ from mmgen.globalvars import g
+ assert s.split(':',1)[0] == g.proto.base_coin.lower(),(
+ "not a string beginning with the prefix '{}:'".format(g.proto.base_coin.lower()))
assert set(s[4:]) <= set(ascii_letters+digits),'contains non-ascii characters'
assert len(s) > 4,'not more that four characters long'
ret,sort_key,idtype = str(s),'z_'+s,'non-mmgen'
@@ -514,7 +518,7 @@ class MMGenTxID(HexStr,Hilite,InitErrors):
m = "{}\n{!r}: value cannot be converted to {}"
return cls.init_fail(m.format(e[0],s,cls.__name__),on_fail)
-class BitcoinTxID(MMGenTxID):
+class CoinTxID(MMGenTxID):
color = 'purple'
width = 64
hexcase = 'lower'
@@ -667,32 +671,30 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
'comp':False,
'gen':'p2pkh',
'fmt':'p2pkh',
- 'desc':'Legacy uncompressed Bitcoin address'},
+ 'desc':'Legacy uncompressed address'},
'S': { 'name':'segwit',
'comp':True,
'gen':'segwit',
'fmt':'p2sh',
- 'desc':'Bitcoin Segwit P2SH-P2WPK address' },
+ 'desc':'Segwit P2SH-P2WPKH address' },
'C': { 'name':'compressed',
'comp':True,
'gen':'p2pkh',
'fmt':'p2pkh',
- 'desc':'Compressed Bitcoin P2PKH address'}
-# 'l': 'litecoin',
-# 'e': 'ethereum',
-# 'E': 'ethereum_classic',
-# 'm': 'monero',
-# 'z': 'zcash',
+ 'desc':'Compressed P2PKH address'}
}
dfl_mmtype = 'L'
def __new__(cls,s,on_fail='die',errmsg=None):
if type(s) == cls: return s
cls.arg_chk(cls,on_fail)
+ from mmgen.globalvars import g
try:
for k,v in cls.mmtypes.items():
if s in (k,v['name']):
if s == v['name']: s = k
me = str.__new__(cls,s)
+ assert me in g.proto.mmtypes + ('P',), (
+ "'{}': invalid address type for {}".format(me,g.proto.__name__))
me.name = v['name']
me.compressed = v['comp']
me.gen_method = v['gen']
@@ -701,9 +703,14 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
return me
raise ValueError,'not found'
except Exception as e:
- m = errmsg or '{!r}: invalid value for {} ({})'.format(s,cls.__name__,e[0])
+ m = '{}{!r}: invalid value for {} ({})'.format(
+ ('{!r}\n'.format(errmsg) if errmsg else ''),s,cls.__name__,e[0])
return cls.init_fail(m,on_fail)
+ @classmethod
+ def get_names(cls):
+ return [v['name'] for v in cls.mmtypes.values()]
+
class MMGenPasswordType(MMGenAddrType):
mmtypes = {
'P': { 'name':'password',
diff --git a/mmgen/opts.py b/mmgen/opts.py
index e0a5bb4e..dbf33837 100755
--- a/mmgen/opts.py
+++ b/mmgen/opts.py
@@ -45,30 +45,6 @@ def _show_hash_presets():
msg(fs.format("'%s'" % i, *g.hash_presets[i]))
msg('N = memory usage (power of two), p = iterations (rounds)')
-# most, but not all, of these set the corresponding global var
-common_opts_data = """
---, --coin=c Choose coin unit. Default: {cu_dfl}. Options: {cu_all}
---, --color=0|1 Disable or enable color output
---, --force-256-color Force 256-color output when color is enabled
---, --bitcoin-data-dir=d Specify Bitcoin data directory location 'd'
---, --data-dir=d Specify {pnm} data directory location 'd'
---, --no-license Suppress the GPL license prompt
---, --rpc-host=h Communicate with bitcoind running on host 'h'
---, --rpc-port=p Communicate with bitcoind listening on port 'p'
---, --rpc-user=user Override 'rpcuser' in bitcoin.conf
---, --rpc-password=pass Override 'rpcpassword' in bitcoin.conf
---, --regtest=0|1 Disable or enable regtest mode
---, --testnet=0|1 Disable or enable testnet
---, --skip-cfg-file Skip reading the configuration file
---, --version Print version information and exit
---, --bob Switch to user "Bob" in MMGen regtest setup
---, --alice Switch to user "Alice" in MMGen regtest setup
-""".format(
- pnm=g.proj_name,
- cu_dfl=g.coin,
- cu_all=' '.join(g.coins),
- )
-
def opt_preproc_debug(short_opts,long_opts,skipped_opts,uopts,args):
d = (
('Cmdline', ' '.join(sys.argv)),
@@ -118,36 +94,55 @@ def set_data_dir_root():
# mainnet and testnet share cfg file, as with Core
g.cfg_file = os.path.join(g.data_dir_root,'{}.cfg'.format(g.proj_name.lower()))
-def get_data_from_config_file():
- from mmgen.util import msg,die,check_or_create_dir
- check_or_create_dir(g.data_dir_root) # dies on error
-
+def get_cfg_template_data():
# https://wiki.debian.org/Python:
# Debian (Ubuntu) sys.prefix is '/usr' rather than '/usr/local, so add 'local'
# TODO - test for Windows
# This must match the configuration in setup.py
- data = u''
+ cfg_template = os.path.join(*([sys.prefix]
+ + (['share'],['local','share'])[g.platform=='linux']
+ + [g.proj_name.lower(),os.path.basename(g.cfg_file)]))
try:
- with open(g.cfg_file,'rb') as f: data = f.read().decode('utf8')
+ with open(cfg_template,'rb') as f:
+ return f.read()
except:
- cfg_template = os.path.join(*([sys.prefix]
- + (['share'],['local','share'])[g.platform=='linux']
- + [g.proj_name.lower(),os.path.basename(g.cfg_file)]))
+ msg("WARNING: configuration template not found at '{}'".format(cfg_template))
+ return u''
+
+def get_data_from_cfg_file():
+ from mmgen.util import msg,die,check_or_create_dir
+ check_or_create_dir(g.data_dir_root) # dies on error
+ template_data = get_cfg_template_data()
+ data = {}
+
+ def copy_template_data(fn):
try:
- with open(cfg_template,'rb') as f: template_data = f.read()
+ with open(fn,'wb') as f: f.write(template_data)
+ os.chmod(fn,0600)
except:
- msg("WARNING: configuration template not found at '{}'".format(cfg_template))
- else:
- try:
- with open(g.cfg_file,'wb') as f: f.write(template_data)
- os.chmod(g.cfg_file,0600)
- except:
- die(2,"ERROR: unable to write to datadir '{}'".format(g.data_dir))
- return data
+ die(2,"ERROR: unable to write to datadir '{}'".format(g.data_dir))
+
+ for k,suf in (('cfg',''),('sample','.sample')):
+ try:
+ with open(g.cfg_file+suf,'rb') as f:
+ data[k] = f.read().decode('utf8')
+ except:
+ if template_data:
+ copy_template_data(g.cfg_file+suf)
+ data[k] = template_data
+ else:
+ data[k] = u''
+
+ if template_data and data['sample'] != template_data:
+ g.cfg_options_changed = True
+ copy_template_data(g.cfg_file+'.sample')
+
+ return data['cfg']
def override_from_cfg_file(cfg_data):
from mmgen.util import die,strip_comments,set_for_type
import re
+ from mmgen.protocol import CoinProtocol
for n,l in enumerate(cfg_data.splitlines(),1): # DOS-safe
l = strip_comments(l)
if l == '': continue
@@ -155,9 +150,16 @@ def override_from_cfg_file(cfg_data):
if not m: die(2,"Parse error in file '{}', line {}".format(g.cfg_file,n))
name,val = m.groups()
if name in g.cfg_file_opts:
- setattr(g,name,set_for_type(val,getattr(g,name),name,src=g.cfg_file))
+ pfx,cfg_var = name.split('_',1)
+ if pfx in CoinProtocol.coins:
+ cls,attr = CoinProtocol(pfx,False),cfg_var
+ else:
+ cls,attr = g,name
+ setattr(cls,attr,set_for_type(val,getattr(cls,attr),attr,src=g.cfg_file))
+ # pmsg(cls,attr,getattr(cls,attr))
else:
die(2,"'{}': unrecognized option in '{}'".format(name,g.cfg_file))
+# pdie('xxx')
def override_from_env():
from mmgen.util import set_for_type
@@ -170,12 +172,37 @@ def override_from_env():
def init(opts_f,add_opts=[],opt_filter=None):
+ from mmgen.protocol import CoinProtocol,BitcoinProtocol
+ g.proto = BitcoinProtocol # this must be initialized to something before opts_f is called
+
+ # most, but not all, of these set the corresponding global var
+ common_opts_data = """
+--, --coin=c Choose coin unit. Default: {cu_dfl}. Options: {cu_all}
+--, --color=0|1 Disable or enable color output
+--, --force-256-color Force 256-color output when color is enabled
+--, --daemon-data-dir=d Specify coin daemon data directory location 'd'
+--, --data-dir=d Specify {pnm} data directory location 'd'
+--, --no-license Suppress the GPL license prompt
+--, --rpc-host=h Communicate with {dn} running on host 'h'
+--, --rpc-port=p Communicate with {dn} listening on port 'p'
+--, --rpc-user=user Override 'rpcuser' in {pn}.conf
+--, --rpc-password=pass Override 'rpcpassword' in {pn}.conf
+--, --regtest=0|1 Disable or enable regtest mode
+--, --testnet=0|1 Disable or enable testnet
+--, --skip-cfg-file Skip reading the configuration file
+--, --version Print version information and exit
+--, --bob Switch to user "Bob" in MMGen regtest setup
+--, --alice Switch to user "Alice" in MMGen regtest setup
+ """.format( pnm=g.proj_name,pn=g.proto.name,dn=g.proto.daemon_name,
+ cu_dfl=g.coin,
+ cu_all=' '.join(CoinProtocol.coins))
+
opts_data = opts_f()
opts_data['long_options'] = common_opts_data
version_info = """
{pgnm_uc} version {g.version}
- Part of the {pnm} suite, a Bitcoin cold-storage solution for the command line.
+ Part of the {pnm} suite, an online/offline cryptocoin wallet for the command line.
Copyright (C) {g.Cdates} {g.author} {g.email}
""".format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip()
@@ -199,11 +226,10 @@ def init(opts_f,add_opts=[],opt_filter=None):
# NB: user opt --data-dir is actually g.data_dir_root
# cfg file is in g.data_dir_root, wallet and other data are in g.data_dir
- # Must set g.data_dir_root and g.cfg_file from cmdline before processing cfg file
+ # We must set g.data_dir_root and g.cfg_file from cmdline before processing cfg file
set_data_dir_root()
if not opt.skip_cfg_file:
- cfg_data = get_data_from_config_file()
- override_from_cfg_file(cfg_data)
+ override_from_cfg_file(get_data_from_cfg_file())
override_from_env()
# User opt sets global var - do these here, before opt is set from g.global_sets_opt
@@ -214,10 +240,10 @@ def init(opts_f,add_opts=[],opt_filter=None):
if g.regtest: g.testnet = True # These are equivalent for now
# g.testnet is set, so we can set g.proto
- from mmgen.protocol import get_coin_protocol
- g.proto = get_coin_protocol(g.coin,g.testnet)
+ g.proto = CoinProtocol(g.coin,g.testnet)
- if not g.daemon_data_dir: g.daemon_data_dir = g.proto.daemon_data_dir
+ # global sets proto
+ if g.daemon_data_dir: g.proto.daemon_data_dir = g.daemon_data_dir
# g.proto is set, so we can set g.data_dir
g.data_dir = os.path.normpath(os.path.join(g.data_dir_root,g.proto.data_subdir))
@@ -250,7 +276,7 @@ def init(opts_f,add_opts=[],opt_filter=None):
if g.bob or g.alice:
g.testnet = True
- g.proto = get_coin_protocol(g.coin,g.testnet)
+ g.proto = CoinProtocol(g.coin,g.testnet)
g.data_dir = os.path.join(g.data_dir_root,'regtest',('alice','bob')[g.bob])
check_or_create_dir(g.data_dir)
import regtest as rt
@@ -263,6 +289,10 @@ def init(opts_f,add_opts=[],opt_filter=None):
if not check_opts(uopts):
sys.exit(1)
+ if hasattr(g,'cfg_options_changed'):
+ ymsg("Warning: config file options have changed! See '{}' for details".format(g.cfg_file+'.sample'))
+ my_raw_input('Hit ENTER to continue: ')
+
if g.debug: opt_postproc_debug()
# We don't need this data anymore
@@ -308,8 +338,9 @@ def check_opts(usr_opts): # Returns false if any check fails
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.max_tx_fee:
- msg("'{}': invalid {} (> max_tx_fee ({} {}))".format(val,desc,g.max_tx_fee,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
@@ -420,7 +451,11 @@ def check_opts(usr_opts): # Returns false if any check fails
if not opt_compares(val,'<=',len(g.key_generators),desc): return False
if not opt_compares(val,'>',0,desc): return False
elif key == 'coin':
- if not opt_is_in_list(val.upper(),g.coins,'coin'): return False
+ from mmgen.protocol import CoinProtocol
+ if not opt_is_in_list(val.lower(),CoinProtocol.coins.keys(),'coin'): return False
+ elif key == 'rbf':
+ if not g.proto.cap('rbf'):
+ die(1,'--rbf requested, but {} does not support replace-by-fee transactions'.format(g.coin))
elif key in ('bob','alice'):
from mmgen.regtest import daemon_dir
m = "Regtest (Bob and Alice) mode not set up yet. Run '{}-regtest setup' to initialize."
diff --git a/mmgen/protocol.py b/mmgen/protocol.py
index 358fd633..6e2145e6 100755
--- a/mmgen/protocol.py
+++ b/mmgen/protocol.py
@@ -23,6 +23,8 @@ 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.globalvars import g
def hash160(hexnum): # take hex, return hex - OP_HASH160
return hashlib.new('ripemd160',hashlib.sha256(unhexlify(hexnum)).digest()).hexdigest()
@@ -51,43 +53,38 @@ def _b58tonum(b58num):
if not i in _b58a: return False
return sum(_b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1])))
-def get_coin_protocol(coin,testnet):
- coin = coin.lower()
- coins = {
- 'btc': (BitcoinProtocol,BitcoinTestnetProtocol),
- 'bch': (BitcoinCashProtocol,BitcoinCashTestnetProtocol),
- 'ltc': (LitecoinProtocol,LitecoinTestnetProtocol),
- 'eth': (EthereumProtocol,EthereumTestnetProtocol),
- }
- assert type(testnet) == bool
- assert coin in coins
- return coins[coin][testnet]
-
-from mmgen.obj import MMGenObject
-from mmgen.globalvars import g
-
class BitcoinProtocol(MMGenObject):
- # devdoc/ref_transactions.md:
- addr_ver_num = { 'p2pkh': ('00','1'), 'p2sh': ('05','3') }
- addr_pfxs = '13'
- uncompressed_wif_pfx = '5'
- privkey_pfx = '80'
- mmtypes = ('L','C','S')
- data_subdir = ''
- rpc_port = 8332
+ name = 'bitcoin'
+ daemon_name = 'bitcoind'
+ addr_ver_num = { 'p2pkh': ('00','1'), 'p2sh': ('05','3') } # chainparams.cpp
+ privkey_pfx = '80'
+ mmtypes = ('L','C','S')
+ data_subdir = ''
+ rpc_port = 8332
+ secs_per_block = 600
+ coin_amt = BTCAmt
+ max_tx_fee = BTCAmt('0.01')
daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Bitcoin') if g.platform == 'win' \
else os.path.join(g.home_dir,'.bitcoin')
+ daemon_data_subdir = ''
sighash_type = 'ALL'
block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
forks = [
(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','bch')
]
+ caps = ('rbf','segwit')
+ base_coin = 'BTC'
+
+ @staticmethod
+ def get_protocol_by_chain(chain):
+ return CoinProtocol(g.coin,{'mainnet':False,'testnet':True,'regtest':True}[chain])
+
+ @staticmethod
+ def get_rpc_coin_amt_type():
+ return (float,str)[g.daemon_version>=120000]
@classmethod
- def get_chain_protocol(cls,chain):
- chain_protos = { 'mainnet':'', 'testnet':'Testnet', 'regtest':'Testnet' }
- assert chain in chain_protos
- return globals()['Bitcoin{}Protocol'.format(chain_protos[chain])]
+ def cap(cls,s): return s in cls.caps
@classmethod
def hex2wif(cls,hexpriv,compressed=False):
@@ -99,9 +96,10 @@ class BitcoinProtocol(MMGenObject):
num = _b58tonum(wif)
if num == False: return False
key = '{:x}'.format(num)
- compressed = wif[0] != cls.uncompressed_wif_pfx
- klen = (66,68)[bool(compressed)]
+ if len(key) not in (74,76): return False
+ compressed = len(key) == 76
if compressed and key[66:68] != '01': return False
+ klen = (66,68)[compressed]
if (key[:2] == cls.privkey_pfx and key[klen:] == hash256(key[:klen])[:8]):
return { 'hex':key[2:66], 'compressed':compressed }
else:
@@ -109,7 +107,7 @@ class BitcoinProtocol(MMGenObject):
@classmethod
def verify_addr(cls,addr,verbose=False,return_dict=False):
- for addr_fmt in ('p2pkh','p2sh'):
+ for addr_fmt in cls.addr_ver_num:
ver_num,ldigit = cls.addr_ver_num[addr_fmt]
if addr[0] not in ldigit: continue
num = _b58tonum(addr)
@@ -117,7 +115,10 @@ class BitcoinProtocol(MMGenObject):
addr_hex = '{:050x}'.format(num)
if addr_hex[:2] != ver_num: continue
if hash256(addr_hex[:42])[:8] == addr_hex[42:]:
- return { 'hex':addr_hex[2:42], 'format':addr_fmt } if return_dict else True
+ return {
+ 'hex': addr_hex[2:42],
+ 'format': {'p2pkh':'p2pkh','p2sh':'p2sh','p2sh2':'p2sh'}[addr_fmt],
+ } if return_dict else True
else:
if verbose: Msg("Invalid checksum in address '{}'".format(addr))
break
@@ -125,9 +126,9 @@ class BitcoinProtocol(MMGenObject):
return False
@classmethod
- def hexaddr2addr(cls,hexaddr,p2sh=False):
+ def hexaddr2addr(cls,hexaddr,p2sh):
s = cls.addr_ver_num[('p2pkh','p2sh')[p2sh]][0] + hexaddr
- lzeroes = (len(s) - len(s.lstrip('0'))) / 2
+ lzeroes = (len(s) - len(s.lstrip('0'))) / 2 # non-zero only for ver num '00' (BTC p2pkh)
return ('1' * lzeroes) + _numtob58(int(s+hash256(s)[:8],16))
# Segwit:
@@ -144,38 +145,84 @@ class BitcoinProtocol(MMGenObject):
class BitcoinTestnetProtocol(BitcoinProtocol):
addr_ver_num = { 'p2pkh': ('6f','mn'), 'p2sh': ('c4','2') }
- addr_pfxs = 'mn2'
- uncompressed_wif_pfx = '9'
privkey_pfx = 'ef'
- data_subdir = 'testnet3'
+ data_subdir = 'testnet'
+ daemon_data_subdir = 'testnet3'
rpc_port = 18332
class BitcoinCashProtocol(BitcoinProtocol):
# TODO: assumes MSWin user installs in custom dir 'Bitcoin_ABC'
+ daemon_name = 'bitcoind-abc'
daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Bitcoin_ABC') if g.platform == 'win' \
else os.path.join(g.home_dir,'.bitcoin-abc')
- rpc_port = 8442
- mmtypes = ('L','C')
- sighash_type = 'ALL|FORKID'
- block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
+ rpc_port = 8442
+ mmtypes = ('L','C')
+ sighash_type = 'ALL|FORKID'
forks = [
(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','btc')
]
+ caps = ()
+ coin_amt = BCHAmt
+ max_tx_fee = BCHAmt('0.1')
@classmethod
def pubhex2redeem_script(cls,pubhex): raise NotImplementedError
@classmethod
def pubhex2segwitaddr(cls,pubhex): raise NotImplementedError
-class BitcoinCashTestnetProtocol(BitcoinTestnetProtocol):
- rpc_port = 18442
- @classmethod
- def pubhex2redeem_script(cls,pubhex): raise NotImplementedError
- @classmethod
- def pubhex2segwitaddr(cls,pubhex): raise NotImplementedError
+class BitcoinCashTestnetProtocol(BitcoinCashProtocol):
+ rpc_port = 18442
+ addr_ver_num = { 'p2pkh': ('6f','mn'), 'p2sh': ('c4','2') }
+ privkey_pfx = 'ef'
+ data_subdir = 'testnet'
+ daemon_data_subdir = 'testnet3'
-class LitecoinProtocol(BitcoinProtocol): pass
-class LitecoinTestnetProtocol(LitecoinProtocol): pass
+class LitecoinProtocol(BitcoinProtocol):
+ block0 = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2'
+ name = 'litecoin'
+ daemon_name = 'litecoind'
+ daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Litecoin') if g.platform == 'win' \
+ else os.path.join(g.home_dir,'.litecoin')
+ addr_ver_num = { 'p2pkh': ('30','L'), 'p2sh': ('32','M'), 'p2sh2': ('05','3') } # 'p2sh' is new fmt
+ privkey_pfx = 'b0'
+ secs_per_block = 150
+ rpc_port = 9332
+ coin_amt = LTCAmt
+ max_tx_fee = LTCAmt('0.3')
+ base_coin = 'LTC'
+ forks = []
+
+class LitecoinTestnetProtocol(LitecoinProtocol):
+ # addr ver nums same as Bitcoin testnet, except for 'p2sh'
+ addr_ver_num = { 'p2pkh': ('6f','mn'), 'p2sh': ('3a','Q'), 'p2sh2': ('c4','2') }
+ privkey_pfx = 'ef' # same as Bitcoin testnet
+ data_subdir = 'testnet'
+ daemon_data_subdir = 'testnet4'
+ rpc_port = 19332
+
+class EthereumProtocol(MMGenObject):
+ base_coin = 'ETH'
-class EthereumProtocol(MMGenObject): pass
class EthereumTestnetProtocol(EthereumProtocol): pass
+
+class CoinProtocol(MMGenObject):
+ coins = {
+ 'btc': (BitcoinProtocol,BitcoinTestnetProtocol),
+ 'bch': (BitcoinCashProtocol,BitcoinCashTestnetProtocol),
+ 'ltc': (LitecoinProtocol,LitecoinTestnetProtocol),
+# 'eth': (EthereumProtocol,EthereumTestnetProtocol),
+ }
+ def __new__(cls,coin,testnet):
+ coin = coin.lower()
+ assert type(testnet) == bool
+ if coin not in cls.coins:
+ from mmgen.util import die
+ die(1,"'{}': not a valid coin. Valid choices are '{}'".format(coin,"','".join(cls.coins)))
+ return cls.coins[coin][testnet]
+
+ @classmethod
+ def get_base_coin_from_name(cls,name):
+ for proto,foo in cls.coins.values():
+ if name == proto.__name__[:-8].lower():
+ return proto.base_coin
+ return False
diff --git a/mmgen/regtest.py b/mmgen/regtest.py
index 63fbcdaa..e51c8253 100755
--- a/mmgen/regtest.py
+++ b/mmgen/regtest.py
@@ -17,7 +17,7 @@
# along with this program. If not, see .
"""
-regtest: Bitcoind regression test mode setup and operations for the MMGen suite
+regtest: Coin daemon regression test mode setup and operations for the MMGen suite
"""
import os,subprocess,time,shutil
@@ -42,7 +42,7 @@ common_args = (
def start_daemon(user,quiet=False,daemon=True):
cmd = (
- 'bitcoind',
+ g.proto.daemon_name,
'-keypool=1',
'-wallet={}'.format(os.path.basename(tr_wallet(user)))
) + common_args
@@ -51,7 +51,7 @@ def start_daemon(user,quiet=False,daemon=True):
p = subprocess.Popen(cmd,stdout=PIPE,stderr=PIPE)
err = process_output(p,silent=False)[1]
if err:
- rdie(1,'Error starting the Bitcoin daemon:\n{}'.format(err))
+ rdie(1,'Error starting the {} daemon:\n{}'.format(g.proto.name.capitalize(),err))
def start_daemon_mswin(user,quiet=False):
import threading
@@ -63,7 +63,7 @@ def start_daemon_mswin(user,quiet=False):
def start_cmd(*args,**kwargs):
cmd = args
if args[0] == 'cli':
- cmd = ('bitcoin-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']]
@@ -93,19 +93,22 @@ def wait_for_daemon(state,silent=False,nonl=False):
def get_balances():
user1 = get_current_user(quiet=True)
if user1 == None:
- die(1,'Regtest daemon not running')
+ user('bob')
+ user1 = get_current_user(quiet=True)
+# die(1,'Regtest daemon not running')
user2 = ('bob','alice')[user1=='bob']
tbal = 0
- from mmgen.obj import BTCAmt
+ # don't need to save and restore these, as we exit immediately
+ g.rpc_host = 'localhost'
+ g.rpc_port = rpc_port
+ g.rpc_user = rpc_user
+ g.rpc_password = rpc_password
+ g.testnet = True
+ rpc_init()
for u in (user1,user2):
- p = start_cmd('python','mmgen-tool',
- '--{}'.format(u),'--data-dir='+g.data_dir,
- 'getbalance','quiet=1')
- bal = p.stdout.read().replace(' \b','') # hack
+ bal = g.proto.coin_amt(g.rpch.getbalance('*',0,True))
if u == user1: user(user2)
- bal = BTCAmt(bal)
- ustr = "{}'s balance:".format(u.capitalize())
- msg('{:<16} {:12}'.format(ustr,bal))
+ msg('{:<16} {:12}'.format(u.capitalize()+"'s balance:",bal))
tbal += bal
msg('{:<16} {:12}'.format('Total balance:',tbal))
@@ -131,7 +134,7 @@ def process_output(p,silent=False):
return out,err
def start_and_wait(user,silent=False,nonl=False):
- vmsg('Starting bitcoin regtest daemon')
+ vmsg('Starting {} regtest daemon'.format(g.proto.name))
(start_daemon_mswin,start_daemon)[g.platform=='linux'](user)
wait_for_daemon('ready',silent=silent,nonl=nonl)
@@ -141,7 +144,7 @@ def stop_and_wait(silent=False,nonl=False,stop_silent=False,ignore_noconnect_err
def send(addr,amt):
user('miner')
- gmsg('Sending {} BTC to address {}'.format(amt,addr))
+ gmsg('Sending {} {} to address {}'.format(amt,g.coin,addr))
p = start_cmd('cli','sendtoaddress',addr,str(amt))
process_output(p)
p.wait()
@@ -189,7 +192,7 @@ def get_current_user_win(quiet=False):
return None
def get_current_user_unix(quiet=False):
- p = start_cmd('pgrep','-af', 'bitcoind.*-rpcuser={}.*'.format(rpc_user))
+ p = start_cmd('pgrep','-af','{}.*-rpcuser={}.*'.format(g.proto.daemon_name,rpc_user))
cmdline = p.stdout.read()
if not cmdline: return None
for k in ('miner','bob','alice'):
@@ -215,6 +218,7 @@ def user(user=None,quiet=False):
return True
gmsg_r('Switching to user {}'.format(user.capitalize()))
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()))
@@ -223,12 +227,12 @@ def user(user=None,quiet=False):
def stop(silent=False,ignore_noconnect_error=True):
if test_daemon() != 'stopped' and not silent:
- gmsg('Stopping bitcoin regtest daemon')
+ gmsg('Stopping {} regtest daemon'.format(g.proto.name))
p = start_cmd('cli','stop')
err = process_output(p)[1]
if err:
if "couldn't connect to server" in err and not ignore_noconnect_error:
- rdie(1,'Error stopping the Bitcoin daemon:\n{}'.format(err))
+ rdie(1,'Error stopping the {} daemon:\n{}'.format(g.proto.name.capitalize(),err))
msg(err)
return p.wait()
diff --git a/mmgen/rpc.py b/mmgen/rpc.py
index 83d091d1..c9772162 100755
--- a/mmgen/rpc.py
+++ b/mmgen/rpc.py
@@ -17,20 +17,19 @@
# along with this program. If not, see .
"""
-rpc.py: Bitcoin RPC library for the MMGen suite
+rpc.py: Cryptocoin RPC library for the MMGen suite
"""
import httplib,base64,json
from mmgen.common import *
from decimal import Decimal
-from mmgen.obj import BTCAmt
-class BitcoinRPCConnection(object):
+class CoinDaemonRPCConnection(object):
def __init__(self,host=None,port=None,user=None,passwd=None,auth_cookie=None):
- dmsg('=== BitcoinRPCConnection.__init__() debug ===')
+ dmsg('=== CoinDaemonRPCConnection.__init__() debug ===')
dmsg(' host [{}] port [{}] user [{}] passwd [{}] auth_cookie [{}]\n'.format(
host,port,user,passwd,auth_cookie))
@@ -39,19 +38,23 @@ class BitcoinRPCConnection(object):
elif auth_cookie:
self.auth_str = auth_cookie
else:
- msg('Error: no Bitcoin RPC authentication method found')
- if passwd: die(1,"'rpcuser' entry not found in bitcoin.conf or mmgen.cfg")
- elif user: die(1,"'rpcpassword' entry not found in bitcoin.conf or mmgen.cfg")
+ msg('Error: no {} RPC authentication method found'.format(g.proto.name.capitalize()))
+ if passwd: die(1,"'rpcuser' entry not found in {}.conf or mmgen.cfg".format(g.proto.name))
+ elif user: die(1,"'rpcpassword' entry not found in {}.conf or mmgen.cfg".format(g.proto.name))
else:
- m1 = 'Either provide rpcuser/rpcpassword in bitcoin.conf or mmgen.cfg'
- m2 = '(or, alternatively, copy the authentication cookie to Bitcoin data dir'
- m3 = 'if {} and Bitcoin are running as different users)'.format(g.proj_name)
- die(1,'\n'.join((m1,m2,m3)))
+ m1 = 'Either provide rpcuser/rpcpassword in {pn}.conf or mmgen.cfg\n'
+ m2 = '(or, alternatively, copy the authentication cookie to the {pnu}\n'
+ m3 = 'data dir if {pnm} and {dn} are running as different users)'
+ die(1,(m1+m2+m3).format(
+ pn=g.proto.name,
+ pnu=g.proto.name.capitalize(),
+ dn=g.proto.daemon_name,
+ pnm=g.proj_name))
self.host = host
self.port = port
- # Normal mode: call with arg list unrolled, exactly as with 'bitcoin-cli'
+ # Normal mode: call with arg list unrolled, exactly as with cli
# Batch mode: call with list of arg lists as first argument
# kwargs are for local use and are not passed to server
@@ -84,8 +87,8 @@ class BitcoinRPCConnection(object):
caller = self
class MyJSONEncoder(json.JSONEncoder):
def default(self, obj):
- if isinstance(obj, BTCAmt):
- return (float,str)[g.bitcoind_version>=120000](obj)
+ if isinstance(obj,g.proto.coin_amt):
+ return g.proto.get_rpc_coin_amt_type()(obj)
return json.JSONEncoder.default(self, obj)
# TODO: UTF-8 labels
@@ -101,20 +104,20 @@ class BitcoinRPCConnection(object):
'Authorization': 'Basic {}'.format(base64.b64encode(self.auth_str))
})
except Exception as e:
- m = '{}\nUnable to connect to bitcoind at {}:{}'
- return die_maybe(None,2,m.format(e,self.host,self.port))
+ m = '{}\nUnable to connect to {} at {}:{}'
+ return die_maybe(None,2,m.format(e,g.proto.daemon_name,self.host,self.port))
try:
r = hc.getresponse() # returns HTTPResponse instance
except Exception:
- m = 'Unable to connect to bitcoind at {}:{} (but port is bound?)'
- return die_maybe(None,2,m.format(self.host,self.port))
+ m = 'Unable to connect to {} at {}:{} (but port is bound?)'
+ return die_maybe(None,2,m.format(g.proto.daemon_name,self.host,self.port))
dmsg(' RPC GETRESPONSE data ==> %s\n' % r.__dict__)
if r.status != 200:
if cf['on_fail'] != 'silent':
- msg_r(yellow('Bitcoind RPC Error: '))
+ msg_r(yellow('{} RPC Error: '.format(g.proto.daemon_name.capitalize())))
msg(red('{} {}'.format(r.status,r.reason)))
e1 = r.read()
try:
@@ -137,7 +140,8 @@ class BitcoinRPCConnection(object):
for resp in r3 if cf['batch'] else [r3]:
if 'error' in resp and resp['error'] != None:
- return die_maybe(r,1,'Bitcoind returned an error: %s' % resp['error'])
+ return die_maybe(r,1,'{} returned an error: {}'.format(
+ g.proto.daemon_name.capitalize(),resp['error']))
elif 'result' not in resp:
return die_maybe(r,1, 'Missing JSON-RPC result\n' + repr(resps))
else:
diff --git a/mmgen/tool.py b/mmgen/tool.py
index a4db4b31..3702c63e 100755
--- a/mmgen/tool.py
+++ b/mmgen/tool.py
@@ -61,7 +61,7 @@ cmd_data = OrderedDict([
('Wif2hex', [' [str-]']),
('Wif2addr', [' [str-]','segwit [bool=False]']),
('Wif2segwit_pair',[' [str-]']),
- ('Hexaddr2addr', [' [str-]']),
+ ('Hexaddr2addr', [' [str-]','p2sh [bool=False]']),
('Addr2hexaddr', [' [str-]']),
('Privhex2addr', [' [str-]','compressed [bool=False]','segwit [bool=False]']),
('Privhex2pubhex',[' [str-]','compressed [bool=False]']),
@@ -78,9 +78,9 @@ cmd_data = OrderedDict([
('Mn_printlist', ["wordlist [str='electrum']"]),
('Listaddress',['<{} address> [str]'.format(pnm),'minconf [int=1]','pager [bool=False]','showempty [bool=True]''showbtcaddr [bool=True]']),
- ('Listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=True]','all_labels [bool=False]']),
+ ('Listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=True]','all_labels [bool=False]',"sort [str=''] (options: reverse, age)"]),
('Getbalance', ['minconf [int=1]','quiet [bool=False]']),
- ('Txview', ['<{} TX file(s)> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]',"sort [str='mtime'] (options: 'ctime','atime')",'MARGS']),
+ ('Txview', ['<{} TX file(s)> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]',"sort [str='mtime'] (options: ctime, atime)",'MARGS']),
('Twview', ["sort [str='age']",'reverse [bool=False]','show_days [bool=True]','show_mmid [bool=True]','minconf [int=1]','wide [bool=False]','pager [bool=False]']),
('Add_label', ['<{} address> [str]'.format(pnm),'