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),'