From c3f185e8b08be5678f57536702fc34f7131b0b45 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 28 May 2020 09:53:34 +0000 Subject: [PATCH] eliminate use of global vars g.proto, g.coin, g.rpc and others This patch eliminates nearly all the global variables that changed during the execution of scripts. With a few minor exceptions, global vars are now used only during initialization or reserved for cfg file / cmdline options and other unchanging values. The result is a code base that's much more maintainable and extensible and less error-prone. The autosigning code, which supports signing of transactions for multiple protocols and networks, has been greatly simplified. Doing away with globals required many changes throughout the code base, and other related (and not so related) changes and cleanups were made along the way, resulting in an enormous patch. Additional code changes include: - tx.py: complete reorganization of TX classes and use of nesting - protocol.py: separation of Regtest and Testnet into distinct subclasses with separate address and transaction files and file extensions - new module help.py for the help notes, loaded on demand - addr.py: rewrite of the address file label parsing code - tx.py,tw.py: use of generators to create formatted text User-visible changes include: - importing of addresses for tokens not yet in the user's tracking wallet is now performed with the `--token-addr` option instead of `--token` Testing: Testing this patch requires a full run of the test suite as described on the Test-Suite wiki page. --- mmgen/addr.py | 336 +-- mmgen/altcoins/eth/contract.py | 39 +- mmgen/altcoins/eth/tw.py | 114 +- mmgen/altcoins/eth/tx.py | 919 ++++---- mmgen/common.py | 214 -- mmgen/daemon.py | 50 +- mmgen/exception.py | 2 + mmgen/globalvars.py | 13 +- mmgen/help.py | 256 +++ mmgen/main_addrgen.py | 30 +- mmgen/main_addrimport.py | 50 +- mmgen/main_autosign.py | 67 +- mmgen/main_passgen.py | 7 +- mmgen/main_seedjoin.py | 2 +- mmgen/main_split.py | 27 +- mmgen/main_txbump.py | 55 +- mmgen/main_txcreate.py | 29 +- mmgen/main_txdo.py | 44 +- mmgen/main_txsend.py | 25 +- mmgen/main_txsign.py | 51 +- mmgen/main_wallet.py | 2 +- mmgen/obj.py | 151 +- mmgen/opts.py | 122 +- mmgen/protocol.py | 131 +- mmgen/regtest.py | 17 +- mmgen/rpc.py | 151 +- mmgen/share/Opts.py | 47 +- mmgen/tool.py | 121 +- mmgen/tw.py | 589 ++--- mmgen/tx.py | 2601 +++++++++++----------- mmgen/txfile.py | 74 +- mmgen/txsign.py | 79 +- mmgen/util.py | 77 +- scripts/create-token.py | 8 +- scripts/tx-btc2bch.py | 60 - setup.py | 1 + test/gentest.py | 72 +- test/include/common.py | 3 + test/objattrtest.py | 10 +- test/objattrtest_py_d/oat_btc_mainnet.py | 30 +- test/objtest.py | 21 +- test/objtest_py_d/ot_btc_mainnet.py | 166 +- test/objtest_py_d/ot_btc_testnet.py | 62 +- test/objtest_py_d/ot_common.py | 1 - test/objtest_py_d/ot_ltc_mainnet.py | 62 +- test/objtest_py_d/ot_ltc_testnet.py | 62 +- test/test.py | 42 +- test/test_py_d/ts_autosign.py | 1 - test/test_py_d/ts_base.py | 8 +- test/test_py_d/ts_chainsplit.py | 17 +- test/test_py_d/ts_ethdev.py | 181 +- test/test_py_d/ts_main.py | 87 +- test/test_py_d/ts_misc.py | 6 +- test/test_py_d/ts_ref.py | 27 +- test/test_py_d/ts_ref_altcoin.py | 68 +- test/test_py_d/ts_regtest.py | 146 +- test/test_py_d/ts_shared.py | 20 +- test/test_py_d/ts_wallet.py | 2 +- test/tooltest.py | 33 +- test/tooltest2.py | 46 +- test/unit_tests_d/ut_rpc.py | 115 +- test/unit_tests_d/ut_tx.py | 36 +- test/unit_tests_d/ut_tx_deserialize.py | 30 +- 63 files changed, 4297 insertions(+), 3618 deletions(-) create mode 100755 mmgen/help.py delete mode 100755 scripts/tx-btc2bch.py diff --git a/mmgen/addr.py b/mmgen/addr.py index f89d10cc..ae76d698 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -24,6 +24,7 @@ from hashlib import sha256,sha512 from .common import * from .obj import * from .baseconv import * +from .protocol import init_proto pnm = g.proj_name @@ -32,11 +33,11 @@ def dmsg_sc(desc,data): Msg(f'sc_debug_{desc}: {data}') class AddrGenerator(MMGenObject): - def __new__(cls,addr_type): + def __new__(cls,proto,addr_type): if type(addr_type) == str: # allow override w/o check gen_method = addr_type elif type(addr_type) == MMGenAddrType: - assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {g.coin}' + assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {proto.coin}' gen_method = addr_type.gen_method else: raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()') @@ -50,13 +51,14 @@ class AddrGenerator(MMGenObject): assert gen_method in gen_methods me = super(cls,cls).__new__(gen_methods[gen_method]) me.desc = gen_methods + me.proto = proto return me class AddrGeneratorP2PKH(AddrGenerator): def to_addr(self,pubhex): from .protocol import hash160 assert type(pubhex) == PubKey - return CoinAddr(g.proto.pubhash2addr(hash160(pubhex),p2sh=False)) + return CoinAddr(self.proto,self.proto.pubhash2addr(hash160(pubhex),p2sh=False)) def to_segwit_redeem_script(self,pubhex): raise NotImplementedError('Segwit redeem script not supported by this address type') @@ -64,24 +66,24 @@ class AddrGeneratorP2PKH(AddrGenerator): class AddrGeneratorSegwit(AddrGenerator): def to_addr(self,pubhex): assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit' - return CoinAddr(g.proto.pubhex2segwitaddr(pubhex)) + return CoinAddr(self.proto,self.proto.pubhex2segwitaddr(pubhex)) def to_segwit_redeem_script(self,pubhex): assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit' - return HexStr(g.proto.pubhex2redeem_script(pubhex)) + return HexStr(self.proto.pubhex2redeem_script(pubhex)) class AddrGeneratorBech32(AddrGenerator): def to_addr(self,pubhex): assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit' from .protocol import hash160 - return CoinAddr(g.proto.pubhash2bech32addr(hash160(pubhex))) + return CoinAddr(self.proto,self.proto.pubhash2bech32addr(hash160(pubhex))) def to_segwit_redeem_script(self,pubhex): raise NotImplementedError('Segwit redeem script not supported by this address type') class AddrGeneratorEthereum(AddrGenerator): - def __init__(self,addr_type): + def __init__(self,proto,addr_type): try: assert not g.use_internal_keccak_module @@ -95,7 +97,7 @@ class AddrGeneratorEthereum(AddrGenerator): def to_addr(self,pubhex): assert type(pubhex) == PubKey - return CoinAddr(self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:]) + return CoinAddr(self.proto,self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:]) def to_wallet_passwd(self,sk_hex): return WalletPassword(self.hash256(sk_hex)[:32]) @@ -119,9 +121,9 @@ class AddrGeneratorZcashZ(AddrGenerator): from nacl.bindings import crypto_scalarmult_base p2 = crypto_scalarmult_base(self.zhash256(key,1)) from .protocol import _b58chk_encode - ver_bytes = g.proto.addr_fmt_to_ver_bytes('zcash_z') + ver_bytes = self.proto.addr_fmt_to_ver_bytes('zcash_z') ret = _b58chk_encode(ver_bytes + self.zhash256(key,0) + p2) - return CoinAddr(ret) + return CoinAddr(self.proto,ret) def to_viewkey(self,pubhex): # pubhex is really privhex key = bytes.fromhex(pubhex) @@ -131,16 +133,16 @@ class AddrGeneratorZcashZ(AddrGenerator): vk[63] &= 0x7f vk[63] |= 0x40 from .protocol import _b58chk_encode - ver_bytes = g.proto.addr_fmt_to_ver_bytes('viewkey') + ver_bytes = self.proto.addr_fmt_to_ver_bytes('viewkey') ret = _b58chk_encode(ver_bytes + vk) - return ZcashViewKey(ret) + return ZcashViewKey(self.proto,ret) def to_segwit_redeem_script(self,pubhex): raise NotImplementedError('Zcash z-addresses incompatible with Segwit') class AddrGeneratorMonero(AddrGenerator): - def __init__(self,addr_type): + def __init__(self,proto,addr_type): try: assert not g.use_internal_keccak_module @@ -189,9 +191,10 @@ class AddrGeneratorMonero(AddrGenerator): vk_hex = self.to_viewkey(sk_hex) pk_str = self.encodepoint(scalarmultbase(hex2int_le(sk_hex))) pvk_str = self.encodepoint(scalarmultbase(hex2int_le(vk_hex))) - addr_p1 = g.proto.addr_fmt_to_ver_bytes('monero') + pk_str + pvk_str + addr_p1 = self.proto.addr_fmt_to_ver_bytes('monero') + pk_str + pvk_str return CoinAddr( + proto = self.proto, addr = self.b58enc(addr_p1 + self.keccak_256(addr_p1).digest()[:4]) ) def to_wallet_passwd(self,sk_hex): @@ -200,34 +203,36 @@ class AddrGeneratorMonero(AddrGenerator): def to_viewkey(self,sk_hex): assert len(sk_hex) == 64, f'{len(sk_hex)}: incorrect privkey length' return MoneroViewKey( - g.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).digest(),None).hex() ) + self.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).digest(),None).hex() ) def to_segwit_redeem_script(self,sk_hex): raise NotImplementedError('Monero addresses incompatible with Segwit') class KeyGenerator(MMGenObject): - def __new__(cls,addr_type,generator=None,silent=False): + def __new__(cls,proto,addr_type,generator=None,silent=False): if type(addr_type) == str: # allow override w/o check pubkey_type = addr_type elif type(addr_type) == MMGenAddrType: - assert addr_type in g.proto.mmtypes,'{}: invalid address type for coin {}'.format(addr_type,g.coin) + assert addr_type in proto.mmtypes, f'{address}: invalid address type for coin {proto.coin}' pubkey_type = addr_type.pubkey_type else: - raise TypeError('{}: incorrect argument type for {}()'.format(type(addr_type),cls.__name__)) + raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()') if pubkey_type == 'std': if cls.test_for_secp256k1(silent=silent) and generator != 1: if not opt.key_generator or opt.key_generator == 2 or generator == 2: - return super(cls,cls).__new__(KeyGeneratorSecp256k1) + me = super(cls,cls).__new__(KeyGeneratorSecp256k1) else: qmsg('Using (slow) native Python ECDSA library for address generation') - return super(cls,cls).__new__(KeyGeneratorPython) + me = super(cls,cls).__new__(KeyGeneratorPython) elif pubkey_type in ('zcash_z','monero'): me = super(cls,cls).__new__(KeyGeneratorDummy) me.desc = 'mmgen-'+pubkey_type - return me else: - raise ValueError('{}: invalid pubkey_type argument'.format(pubkey_type)) + raise ValueError(f'{pubkey_type}: invalid pubkey_type argument') + + me.proto = proto + return me @classmethod def test_for_secp256k1(self,silent=False): @@ -288,19 +293,25 @@ class KeyGeneratorDummy(KeyGenerator): assert type(privhex) == PrivKey return PubKey(privhex,compressed=privhex.compressed) -class AddrListEntry(MMGenListItem): - addr = ListItemAttr('CoinAddr') +class AddrListEntryBase(MMGenListItem): + invalid_attrs = {'proto'} + def __init__(self,proto,**kwargs): + self.__dict__['proto'] = proto + MMGenListItem.__init__(self,**kwargs) + +class AddrListEntry(AddrListEntryBase): + addr = ListItemAttr('CoinAddr',include_proto=True) idx = ListItemAttr('AddrIdx') # not present in flat addrlists label = ListItemAttr('TwComment',reassign_ok=True) - sec = ListItemAttr(PrivKey,typeconv=False) - viewkey = ListItemAttr('ViewKey') + sec = ListItemAttr('PrivKey',include_proto=True) + viewkey = ListItemAttr('ViewKey',include_proto=True) wallet_passwd = ListItemAttr('WalletPassword') -class PasswordListEntry(MMGenListItem): +class PasswordListEntry(AddrListEntryBase): passwd = ListItemAttr(str,typeconv=False) # TODO: create Password type idx = ImmutableAttr('AddrIdx') label = ListItemAttr('TwComment',reassign_ok=True) - sec = ListItemAttr(PrivKey,typeconv=False) + sec = ListItemAttr('PrivKey',include_proto=True) class AddrListChksum(str,Hilite): color = 'pink' @@ -335,7 +346,7 @@ class AddrListIDStr(str,Hilite): if fmt_str: ret = fmt_str.format(s) else: - bc = (g.proto.base_coin,g.coin)[g.proto.base_coin=='ETH'] + bc = (addrlist.proto.base_coin,addrlist.proto.coin)[addrlist.proto.base_coin=='ETH'] mt = addrlist.al_id.mmtype ret = '{}{}{}[{}]'.format(addrlist.al_id.sid,('-'+bc,'')[bc=='BTC'],('-'+mt,'')[mt in ('L','E')],s) @@ -376,7 +387,7 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file chksum_rec_f = lambda foo,e: (str(e.idx), e.addr) line_ctr = 0 - def __init__(self, + def __init__(self,proto, addrfile = '', al_id = '', adata = [], @@ -389,8 +400,14 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file do_chksum = True self.update_msgs() - mmtype = mmtype or g.proto.dfl_mmtype - assert mmtype in MMGenAddrType.mmtypes,'{}: mmtype not in {}'.format(mmtype,repr(MMGenAddrType.mmtypes)) + mmtype = mmtype or proto.dfl_mmtype + assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}' + + from .protocol import CoinProtocol + self.bitcoin_addrtypes = tuple( + MMGenAddrType(CoinProtocol.Bitcoin,key).name for key in CoinProtocol.Bitcoin.mmtypes) + + self.proto = proto if seed and addr_idxs: # data from seed + idxs self.al_id,src = AddrListID(seed.sid,mmtype),'gen' @@ -403,10 +420,10 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file do_chksum = False elif addrlist: # data from flat address list self.al_id = None - adata = AddrListList([AddrListEntry(addr=a) for a in set(addrlist)]) + adata = AddrListData([AddrListEntry(proto=proto,addr=a) for a in set(addrlist)]) elif keylist: # data from flat key list self.al_id = None - adata = AddrListList([AddrListEntry(sec=PrivKey(wif=k)) for k in set(keylist)]) + adata = AddrListData([AddrListEntry(proto=proto,sec=PrivKey(proto=proto,wif=k)) for k in set(keylist)]) elif seed or addr_idxs: die(3,'Must specify both seed and addr indexes') elif al_id or adata: @@ -448,10 +465,10 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file gen_viewkey = type(self) == KeyAddrList and 'viewkey' in self.al_id.mmtype.extra_attrs if self.gen_addrs: - kg = KeyGenerator(self.al_id.mmtype) - ag = AddrGenerator(self.al_id.mmtype) + kg = KeyGenerator(self.proto,self.al_id.mmtype) + ag = AddrGenerator(self.proto,self.al_id.mmtype) - t_addrs,num,pos,out = len(addrnums),0,0,AddrListList() + t_addrs,num,pos,out = len(addrnums),0,0,AddrListData() le = self.entry_type while pos != t_addrs: @@ -465,10 +482,11 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file if not g.debug: qmsg_r('\rGenerating {} #{} ({} of {})'.format(self.gen_desc,num,pos,t_addrs)) - e = le(idx=num) + e = le(proto=self.proto,idx=num) # Secret key is double sha256 of seed hash round /num/ e.sec = PrivKey( + self.proto, sha256(sha256(seed).digest()).digest(), compressed = compressed, pubkey_type = pubkey_type ) @@ -497,17 +515,17 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file return True # format is checked when added to list entry object def scramble_seed(self,seed): - is_btcfork = g.proto.base_coin == 'BTC' - if is_btcfork and self.al_id.mmtype == 'L' and not g.proto.testnet: + is_btcfork = self.proto.base_coin == 'BTC' + if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet: dmsg_sc('str','(none)') return seed - if g.proto.base_coin == 'ETH': - scramble_key = g.coin.lower() + if self.proto.base_coin == 'ETH': + scramble_key = self.proto.coin.lower() else: - scramble_key = (g.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name + scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name from .crypto import scramble_seed - if g.proto.testnet: - scramble_key += ':testnet' + if self.proto.testnet: + scramble_key += ':' + self.proto.network dmsg_sc('str',scramble_key) return scramble_seed(seed,scramble_key.encode()) @@ -517,7 +535,7 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file self.ext += '.'+g.mmenc_ext def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None): - tn = ('','.testnet')[g.proto.testnet] + tn = ('.' + self.proto.network) if self.proto.testnet else '' fn = '{}{x}{}.{}'.format(self.id_str,tn,self.ext,x='-α' if g.debug_utf8 else '') ask_tty = self.has_keys and not opt.quiet write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary) @@ -554,12 +572,14 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file if idx == e.idx: e.label = comment - def make_reverse_dict(self,coinaddrs): - d,b = MMGenDict(),coinaddrs + def make_reverse_dict_addrlist(self,coinaddrs): + d = MMGenDict() + b = coinaddrs for e in self.data: try: - d[b[b.index(e.addr)]] = MMGenID('{}:{}'.format(self.al_id,e.idx)),e.label - except: pass + d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.label ) + except ValueError: + pass return d def remove_dup_keys(self,cmplist): @@ -585,9 +605,9 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file def generate_addrs_from_keys(self): # assume that the first listed mmtype is valid for flat key list - t = MMGenAddrType(g.proto.mmtypes[0]) - kg = KeyGenerator(t.pubkey_type) - ag = AddrGenerator(t.gen_method) + t = self.proto.addr_type(self.proto.mmtypes[0]) + kg = KeyGenerator(self.proto,t.pubkey_type) + ag = AddrGenerator(self.proto,t.gen_method) d = self.data for n,e in enumerate(d,1): qmsg_r('\rGenerating addresses from keylist: {}/{}'.format(n,len(d))) @@ -597,10 +617,10 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file qmsg('\rGenerated addresses from keylist: {}/{} '.format(n,len(d))) def make_label(self): - bc,mt = g.proto.base_coin,self.al_id.mmtype - l_coin = [] if bc == 'BTC' else [g.coin] if bc == 'ETH' else [bc] - l_type = [] if mt == 'E' or (mt == 'L' and not g.proto.testnet) else [mt.name.upper()] - l_tn = [] if not g.proto.testnet else ['TESTNET'] + bc,mt = self.proto.base_coin,self.al_id.mmtype + l_coin = [] if bc == 'BTC' else [self.proto.coin] if bc == 'ETH' else [bc] + l_type = [] if mt == 'E' or (mt == 'L' and not self.proto.testnet) else [mt.name.upper()] + l_tn = [] if not self.proto.testnet else [self.proto.network.upper()] lbl_p2 = ':'.join(l_coin+l_type+l_tn) return self.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2 @@ -646,34 +666,36 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file def parse_file_body(self,lines): - ret = AddrListList() + ret = AddrListData() le = self.entry_type iifs = "{!r}: invalid identifier [expected '{}:']" while lines: idx,addr,lbl = self.get_line(lines) - assert is_mmgen_idx(idx),'invalid address index {!r}'.format(idx) + assert is_mmgen_idx(idx), f'invalid address index {idx!r}' self.check_format(addr) - a = le(**{ 'idx':int(idx), self.main_attr:addr, 'label':lbl }) + a = le(**{ 'proto': self.proto, 'idx':int(idx), self.main_attr:addr, 'label':lbl }) if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd d = self.get_line(lines) assert d[0] == self.al_id.mmtype.wif_label+':',iifs.format(d[0],self.al_id.mmtype.wif_label) - a.sec = PrivKey(wif=d[1]) - for k,dtype in (('viewkey',ViewKey),('wallet_passwd',WalletPassword)): + a.sec = PrivKey(proto=self.proto,wif=d[1]) + for k,dtype,add_proto in ( + ('viewkey',ViewKey,True), + ('wallet_passwd',WalletPassword,False) ): if k in self.al_id.mmtype.extra_attrs: d = self.get_line(lines) assert d[0] == k+':',iifs.format(d[0],k) - setattr(a,k,dtype(d[1])) + setattr(a,k,dtype( *((self.proto,d[1]) if add_proto else (d[1],)) ) ) ret.append(a) if self.has_keys: if (hasattr(opt,'yes') and opt.yes) or keypress_confirm('Check key-to-address validity?'): - kg = KeyGenerator(self.al_id.mmtype) - ag = AddrGenerator(self.al_id.mmtype) + kg = KeyGenerator(self.proto,self.al_id.mmtype) + ag = AddrGenerator(self.proto,self.al_id.mmtype) llen = len(ret) for n,e in enumerate(ret): qmsg_r('\rVerifying keys {}/{}'.format(n+1,llen)) @@ -685,35 +707,46 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file def parse_file(self,fn,buf=[],exit_on_error=True): - def parse_addrfile_label(lbl): # we must maintain backwards compat, so parse is tricky - al_coin,al_mmtype = None,None - tn = lbl[-8:] == ':TESTNET' - if tn: - assert g.proto.testnet, f'{self.data_desc} file is testnet but protocol is mainnet!' + def parse_addrfile_label(lbl): + """ + label examples: + - Bitcoin legacy mainnet: no label + - Bitcoin legacy testnet: 'LEGACY:TESTNET' + - Bitcoin Segwit: 'SEGWIT' + - Bitcoin Segwit testnet: 'SEGWIT:TESTNET' + - Bitcoin Bech32 regtest: 'BECH32:REGTEST' + - Litecoin legacy mainnet: 'LTC' + - Litecoin Bech32 mainnet: 'LTC:BECH32' + - Litecoin legacy testnet: 'LTC:LEGACY:TESTNET' + - Ethereum mainnet: 'ETH' + - Ethereum Classic mainnet: 'ETC' + - Ethereum regtest: 'ETH:REGTEST' + """ + lbl = lbl.lower() + + # remove the network component: + if lbl.endswith(':testnet'): + network = 'testnet' + lbl = lbl[:-8] + elif lbl.endswith(':regtest'): + network = 'regtest' lbl = lbl[:-8] else: - assert not g.proto.testnet, f'{self.data_desc} file is mainnet but protocol is testnet!' - lbl = lbl.split(':',1) - if len(lbl) == 2: - al_coin,al_mmtype = lbl[0],lbl[1].lower() - else: - if lbl[0].lower() in MMGenAddrType.get_names(): - al_mmtype = lbl[0].lower() - else: - al_coin = lbl[0] + network = 'mainnet' - # this block fails if al_mmtype is invalid for g.coin - if not al_mmtype: - mmtype = MMGenAddrType('E' if al_coin in ('ETH','ETC') else 'L',on_fail='raise') - else: - mmtype = MMGenAddrType(al_mmtype,on_fail='raise') + if lbl in self.bitcoin_addrtypes: + coin,mmtype_key = ( 'BTC', lbl ) + elif ':' in lbl: # first component is coin, second is mmtype_key + coin,mmtype_key = lbl.split(':') + else: # only component is coin + coin,mmtype_key = ( lbl, None ) - from .protocol import init_proto - return (init_proto(al_coin or 'BTC').base_coin, mmtype, tn) + proto = init_proto(coin=coin,network=network) - def check_coin_mismatch(base_coin): # die if addrfile coin doesn't match g.coin - m = '{} address file format, but base coin is {}!' - assert base_coin == g.proto.base_coin, m.format(base_coin,g.proto.base_coin) + if mmtype_key == None: + mmtype_key = proto.mmtypes[0] + + return ( proto, proto.addr_type(mmtype_key) ) lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True) @@ -732,20 +765,31 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file self.set_pw_fmt(ss[0]) self.set_pw_len(ss[1]) self.pw_id_str = MMGenPWIDString(ls.pop()) - base_coin,mmtype = None,MMGenPasswordType('P') - testnet = False + proto = init_proto('btc')# FIXME: dummy protocol + mmtype = MMGenPasswordType(proto,'P') elif len(ls) == 1: - base_coin,mmtype,testnet = parse_addrfile_label(ls[0]) - check_coin_mismatch(base_coin) + proto,mmtype = parse_addrfile_label(ls[0]) elif len(ls) == 0: - base_coin,mmtype = 'BTC',MMGenAddrType('L') - testnet = False - check_coin_mismatch(base_coin) + proto = init_proto('btc') + mmtype = proto.addr_type('L') else: - raise ValueError("'{}': Invalid first line for {} file '{}'".format(lines[0],self.gen_desc,fn)) + raise ValueError(f'{lines[0]}: Invalid first line for {self.gen_desc} file {fn!r}') - self.base_coin = base_coin - self.is_testnet = testnet + if type(self) != PasswordList: + if proto.base_coin != self.proto.base_coin or proto.network != self.proto.network: + """ + Having caller supply protocol and checking address file protocol against it here + allows us to catch all mismatches in one place. This behavior differs from that of + transaction files, which determine the protocol independently, requiring the caller + to check for protocol mismatches (e.g. MMGenTX.check_correct_chain()) + """ + raise ValueError( + f'{self.data_desc} file is ' + + f'{proto.base_coin} {proto.network} but protocol is ' + + f'{self.proto.base_coin} {self.proto.network}' ) + + self.base_coin = proto.base_coin + self.network = proto.network self.al_id = AddrListID(SeedID(sid=sid),mmtype) data = self.parse_file_body(lines[1:-1]) @@ -850,7 +894,7 @@ Record this checksum: it will be used to verify the password file in the future feature_warn_fs = 'WARNING: {!r} is a potentially dangerous feature. Use at your own risk!' hex2bip39 = False - def __init__(self, + def __init__(self,proto, infile = None, seed = None, pw_idxs = None, @@ -860,6 +904,7 @@ Record this checksum: it will be used to verify the password file in the future chk_params_only = False ): + self.proto = proto # proto is ignored self.update_msgs() if infile: @@ -877,7 +922,7 @@ Record this checksum: it will be used to verify the password file in the future if self.hex2bip39: ymsg(self.feature_warn_fs.format(pw_fmt)) self.set_pw_len_vs_seed_len(pw_len,seed) - self.al_id = AddrListID(seed.sid,MMGenPasswordType('P')) + self.al_id = AddrListID(seed.sid,MMGenPasswordType(self.proto,'P')) self.data = self.generate(seed,pw_idxs) if self.pw_fmt in ('bip39','xmrseed'): @@ -983,7 +1028,6 @@ Record this checksum: it will be used to verify the password file in the future pw_len_hex = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] * 2 # take most significant part bytes_trunc = bytes.fromhex(hex_sec[:pw_len_hex]) - from .protocol import init_proto bytes_preproc = init_proto('xmr').preprocess_key(bytes_trunc,None) return ' '.join(baseconv.frombytes(bytes_preproc,wl_id='xmrseed')) else: @@ -1043,11 +1087,13 @@ re-import your addresses. """.strip().format(pnm=pnm) } - def __new__(cls,*args,**kwargs): - return MMGenObject.__new__(altcoin_subclass(cls,'tw','AddrData')) + def __new__(cls,proto,*args,**kwargs): + return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw')) - def __init__(self,*args,**kwargs): + def __init__(self,proto,*args,**kwargs): self.al_ids = {} + self.proto = proto + self.rpc = None def seed_ids(self): return list(self.al_ids.keys()) @@ -1058,7 +1104,7 @@ re-import your addresses. return self.al_ids[al_id] def mmaddr2coinaddr(self,mmaddr): - al_id,idx = MMGenID(mmaddr).rsplit(':',1) + al_id,idx = MMGenID(self.proto,mmaddr).rsplit(':',1) coinaddr = '' if al_id in self.al_ids: coinaddr = self.addrlist(al_id).coinaddr(int(idx)) @@ -1068,38 +1114,6 @@ re-import your addresses. d = self.make_reverse_dict([coinaddr]) return (list(d.values())[0][0]) if d else None - @classmethod - async def get_tw_data(cls,wallet=None): - vmsg('Getting address data from tracking wallet') - if 'label_api' in g.rpc.caps: - accts = await g.rpc.call('listlabels') - ll = await g.rpc.batch_call('getaddressesbylabel',[(k,) for k in accts]) - alists = [list(a.keys()) for a in ll] - else: - accts = await g.rpc.call('listaccounts',0,True) - alists = await g.rpc.batch_call('getaddressesbyaccount',[(k,) for k in accts]) - return list(zip(accts,alists)) - - async def add_tw_data(self,wallet): - - twd = await type(self).get_tw_data(wallet) - out,i = {},0 - for acct,addr_array in twd: - l = TwLabel(acct,on_fail='silent') - if l and l.mmid.type == 'mmgen': - obj = l.mmid.obj - if len(addr_array) != 1: - die(2,self.msgs['too_many_acct_addresses'].format(acct)) - al_id = AddrListID(SeedID(sid=obj.sid),MMGenAddrType(obj.mmtype)) - if al_id not in out: - out[al_id] = [] - out[al_id].append(AddrListEntry(idx=obj.idx,addr=addr_array[0],label=l.comment)) - i += 1 - - vmsg('{n} {pnm} addresses found, {m} accounts total'.format(n=i,pnm=pnm,m=len(twd))) - for al_id in out: - self.add(AddrList(al_id=al_id,adata=AddrListList(sorted(out[al_id],key=lambda a: a.idx)))) - def add(self,addrlist): if type(addrlist) == AddrList: self.al_ids[addrlist.al_id] = addrlist @@ -1110,17 +1124,53 @@ re-import your addresses. def make_reverse_dict(self,coinaddrs): d = MMGenDict() for al_id in self.al_ids: - d.update(self.al_ids[al_id].make_reverse_dict(coinaddrs)) + d.update(self.al_ids[al_id].make_reverse_dict_addrlist(coinaddrs)) return d class TwAddrData(AddrData,metaclass=aInitMeta): - def __new__(cls,*args,**kwargs): - return MMGenObject.__new__(altcoin_subclass(cls,'tw','TwAddrData')) + def __new__(cls,proto,*args,**kwargs): + return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw')) - def __init__(self,*args,**kwargs): + def __init__(self,proto,*args,**kwargs): pass - async def __ainit__(self,wallet=None): + async def __ainit__(self,proto,wallet=None): + self.proto = proto + from .rpc import rpc_init + self.rpc = await rpc_init(proto) self.al_ids = {} await self.add_tw_data(wallet) + + async def get_tw_data(self,wallet=None): + vmsg('Getting address data from tracking wallet') + c = self.rpc + if 'label_api' in c.caps: + accts = await c.call('listlabels') + ll = await c.batch_call('getaddressesbylabel',[(k,) for k in accts]) + alists = [list(a.keys()) for a in ll] + else: + accts = await c.call('listaccounts',0,True) + alists = await c.batch_call('getaddressesbyaccount',[(k,) for k in accts]) + return list(zip(accts,alists)) + + async def add_tw_data(self,wallet): + + twd = await self.get_tw_data(wallet) + out,i = {},0 + for acct,addr_array in twd: + l = TwLabel(self.proto,acct,on_fail='silent') + if l and l.mmid.type == 'mmgen': + obj = l.mmid.obj + if len(addr_array) != 1: + die(2,self.msgs['too_many_acct_addresses'].format(acct)) + al_id = AddrListID(SeedID(sid=obj.sid),self.proto.addr_type(obj.mmtype)) + if al_id not in out: + out[al_id] = [] + out[al_id].append(AddrListEntry(self.proto,idx=obj.idx,addr=addr_array[0],label=l.comment)) + i += 1 + + vmsg('{n} {pnm} addresses found, {m} accounts total'.format(n=i,pnm=pnm,m=len(twd))) + for al_id in out: + self.add(AddrList(self.proto,al_id=al_id,adata=AddrListData(sorted(out[al_id],key=lambda a: a.idx)))) + diff --git a/mmgen/altcoins/eth/contract.py b/mmgen/altcoins/eth/contract.py index ae7f3f03..e0286bb0 100755 --- a/mmgen/altcoins/eth/contract.py +++ b/mmgen/altcoins/eth/contract.py @@ -37,13 +37,13 @@ except: def parse_abi(s): return [s[:8]] + [s[8+x*64:8+(x+1)*64] for x in range(len(s[8:])//64)] -def create_method_id(sig): return keccak_256(sig.encode()).hexdigest()[:8] +def create_method_id(sig): + return keccak_256(sig.encode()).hexdigest()[:8] class TokenBase(MMGenObject): # ERC20 - @staticmethod - def transferdata2sendaddr(data): # online - return CoinAddr(parse_abi(data)[1][-40:]) + def transferdata2sendaddr(self,data): # online + return CoinAddr(self.proto,parse_abi(data)[1][-40:]) def transferdata2amt(self,data): # online return ETHAmt(int(parse_abi(data)[-1],16) * self.base_unit) @@ -52,7 +52,7 @@ class TokenBase(MMGenObject): # ERC20 data = create_method_id(method_sig) + method_args if g.debug: msg('ETH_CALL {}: {}'.format(method_sig,'\n '.join(parse_abi(data)))) - ret = await g.rpc.call('eth_call',{ 'to': '0x'+self.addr, 'data': '0x'+data }) + ret = await self.rpc.call('eth_call',{ 'to': '0x'+self.addr, 'data': '0x'+data }) if toUnit: return int(ret,16) * self.base_unit else: @@ -91,7 +91,7 @@ class TokenBase(MMGenObject): # ERC20 'total supply:', await self.get_total_supply()) async def code(self): - return (await g.rpc.call('eth_getCode','0x'+self.addr))[2:] + return (await self.rpc.call('eth_getCode','0x'+self.addr))[2:] def create_data(self,to_addr,amt,method_sig='transfer(address,uint256)',from_addr=None): from_arg = from_addr.rjust(64,'0') if from_addr else '' @@ -114,14 +114,13 @@ class TokenBase(MMGenObject): # ERC20 from .pyethereum.transactions import Transaction if chain_id is None: - chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in g.rpc.caps] - chain_id = int(await g.rpc.call(chain_id_method),16) + chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in self.rpc.caps] + chain_id = int(await self.rpc.call(chain_id_method),16) tx = Transaction(**tx_in).sign(key,chain_id) hex_tx = rlp.encode(tx).hex() coin_txid = CoinTxID(tx.hash.hex()) if tx.sender.hex() != from_addr: - m = "Sender address '{}' does not match address of key '{}'!" - die(3,m.format(from_addr,tx.sender.hex())) + die(3,f'Sender address {from_addr!r} does not match address of key {tx.sender.hex()!r}!') if g.debug: msg('TOKEN DATA:') pp_msg(tx.to_dict()) @@ -131,7 +130,7 @@ class TokenBase(MMGenObject): # ERC20 # The following are used for token deployment only: async def txsend(self,hex_tx): - return (await g.rpc.call('eth_sendRawTransaction','0x'+hex_tx)).replace('0x','',1) + return (await self.rpc.call('eth_sendRawTransaction','0x'+hex_tx)).replace('0x','',1) async def transfer( self,from_addr,to_addr,amt,key,start_gas,gasPrice, method_sig='transfer(address,uint256)', @@ -140,7 +139,7 @@ class TokenBase(MMGenObject): # ERC20 tx_in = self.make_tx_in( from_addr,to_addr,amt, start_gas,gasPrice, - nonce = int(await g.rpc.call('parity_nextNonce','0x'+from_addr),16), + nonce = int(await self.rpc.call('parity_nextNonce','0x'+from_addr),16), method_sig = method_sig, from_addr2 = from_addr2 ) (hex_tx,coin_txid) = await self.txsign(tx_in,key,from_addr) @@ -148,20 +147,24 @@ class TokenBase(MMGenObject): # ERC20 class Token(TokenBase): - def __init__(self,addr,decimals): - self.addr = TokenAddr(addr) + def __init__(self,proto,addr,decimals,rpc=None): + self.proto = proto + self.addr = TokenAddr(proto,addr) assert isinstance(decimals,int),f'decimals param must be int instance, not {type(decimals)}' self.decimals = decimals self.base_unit = Decimal('10') ** -self.decimals + self.rpc = rpc class TokenResolve(TokenBase,metaclass=aInitMeta): - def __init__(self,addr): + def __init__(self,*args,**kwargs): return super().__init__() - async def __ainit__(self,addr): - self.addr = TokenAddr(addr) + async def __ainit__(self,proto,rpc,addr): + self.proto = proto + self.rpc = rpc + self.addr = TokenAddr(proto,addr) decimals = await self.get_decimals() # requires self.addr! if not decimals: raise TokenNotInBlockchain(f'Token {addr!r} not in blockchain') - Token.__init__(self,addr,decimals) + Token.__init__(self,proto,addr,decimals,rpc) diff --git a/mmgen/altcoins/eth/tw.py b/mmgen/altcoins/eth/tw.py index ab5ab0fd..9c791080 100755 --- a/mmgen/altcoins/eth/tw.py +++ b/mmgen/altcoins/eth/tw.py @@ -21,7 +21,7 @@ altcoins.eth.tw: Ethereum tracking wallet and related classes for the MMGen suit """ from mmgen.common import * -from mmgen.obj import ETHAmt,TwLabel,is_coin_addr,is_mmgen_id,MMGenListItem,ListItemAttr,ImmutableAttr +from mmgen.obj import ETHAmt,TwLabel,is_coin_addr,is_mmgen_id,ListItemAttr,ImmutableAttr from mmgen.tw import TrackingWallet,TwAddrList,TwUnspentOutputs,TwGetBalance from mmgen.addr import AddrData,TwAddrData from .contract import Token,TokenResolve @@ -36,7 +36,7 @@ class EthereumTrackingWallet(TrackingWallet): return addr in self.data_root def init_empty(self): - self.data = { 'coin': g.coin, 'accounts': {}, 'tokens': {} } + self.data = { 'coin': self.proto.coin, 'accounts': {}, 'tokens': {} } def upgrade_wallet_maybe(self): @@ -49,7 +49,7 @@ class EthereumTrackingWallet(TrackingWallet): import json self.data['accounts'] = json.loads(self.orig_data) if not 'coin' in self.data: - self.data['coin'] = g.coin + self.data['coin'] = self.proto.coin upgraded = True def have_token_params_fields(): @@ -75,7 +75,7 @@ class EthereumTrackingWallet(TrackingWallet): msg('{} upgraded successfully!'.format(self.desc)) async def rpc_get_balance(self,addr): - return ETHAmt(int(await g.rpc.call('eth_getBalance','0x'+addr),16),'wei') + return ETHAmt(int(await self.rpc.call('eth_getBalance','0x'+addr),16),'wei') @write_mode async def batch_import_address(self,args_list): @@ -97,9 +97,9 @@ class EthereumTrackingWallet(TrackingWallet): async def remove_address(self,addr): r = self.data_root - if is_coin_addr(addr): + if is_coin_addr(self.proto,addr): have_match = lambda k: k == addr - elif is_mmgen_id(addr): + elif is_mmgen_id(self.proto,addr): have_match = lambda k: r[k]['mmid'] == addr else: die(1,f'{addr!r} is not an Ethereum address or MMGen ID') @@ -107,7 +107,7 @@ class EthereumTrackingWallet(TrackingWallet): for k in r: if have_match(k): # return the addr resolved to mmid if possible - ret = r[k]['mmid'] if is_mmgen_id(r[k]['mmid']) else addr + ret = r[k]['mmid'] if is_mmgen_id(self.proto,r[k]['mmid']) else addr del r[k] self.write() return ret @@ -152,30 +152,33 @@ class EthereumTokenTrackingWallet(EthereumTrackingWallet): symbol = None cur_eth_balances = {} - async def __ainit__(self,mode='r'): - await super().__ainit__(mode=mode) + async def __ainit__(self,proto,mode='r',token_addr=None): + await super().__ainit__(proto,mode=mode) for v in self.data['tokens'].values(): self.conv_types(v) - if not is_coin_addr(g.token): - g.token = await self.sym2addr(g.token) # returns None on failure + if self.importing and token_addr: + if not is_coin_addr(proto,token_addr): + raise InvalidTokenAddress(f'{token_addr!r}: invalid token address') + else: + assert token_addr == None,'EthereumTokenTrackingWallet_chk1' + token_addr = await self.sym2addr(proto.tokensym) # returns None on failure + if not is_coin_addr(proto,token_addr): + raise UnrecognizedTokenSymbol(f'Specified token {proto.tokensym!r} could not be resolved!') - if not is_coin_addr(g.token): - if self.importing: - m = 'When importing addresses for a new token, the token must be specified by address, not symbol.' - raise InvalidTokenAddress(f'{g.token!r}: invalid token address\n{m}') - else: - raise UnrecognizedTokenSymbol(f'Specified token {g.token!r} could not be resolved!') + from mmgen.obj import TokenAddr + self.token = TokenAddr(proto,token_addr) - if g.token in self.data['tokens']: - self.decimals = self.data['tokens'][g.token]['params']['decimals'] - self.symbol = self.data['tokens'][g.token]['params']['symbol'] - elif not self.importing: - raise TokenNotInWallet('Specified token {!r} not in wallet!'.format(g.token)) + if self.token in self.data['tokens']: + self.decimals = self.get_param('decimals') + self.symbol = self.get_param('symbol') + elif self.importing: + await self.import_token(self.token) # sets self.decimals, self.symbol + else: + raise TokenNotInWallet(f'Specified token {self.token!r} not in wallet!') - self.token = g.token - g.proto.dcoin = self.symbol + proto.tokensym = self.symbol async def is_in_wallet(self,addr): return addr in self.data['tokens'][self.token] @@ -189,7 +192,7 @@ class EthereumTokenTrackingWallet(EthereumTrackingWallet): return 'token ' + self.get_param('symbol') async def rpc_get_balance(self,addr): - return await Token(self.token,self.decimals).get_balance(addr) + return await Token(self.proto,self.token,self.decimals,self.rpc).get_balance(addr) async def get_eth_balance(self,addr,force_rpc=False): cache = self.cur_eth_balances @@ -204,21 +207,19 @@ class EthereumTokenTrackingWallet(EthereumTrackingWallet): return self.data['tokens'][self.token]['params'][param] @write_mode - async def import_token(tw): + async def import_token(self,tokenaddr): """ Token 'symbol' and 'decimals' values are resolved from the network by the system just once, upon token import. Thereafter, token address, symbol and decimals are resolved either from the tracking wallet (online operations) or transaction file (when signing). """ - if not g.token in tw.data['tokens']: - t = await TokenResolve(g.token) - tw.token = g.token - tw.data['tokens'][tw.token] = { - 'params': { - 'symbol': await t.get_symbol(), - 'decimals': t.decimals - } + t = await TokenResolve(self.proto,self.rpc,tokenaddr) + self.data['tokens'][tokenaddr] = { + 'params': { + 'symbol': await t.get_symbol(), + 'decimals': t.decimals } + } # No unspent outputs with Ethereum, but naming must be consistent class EthereumTwUnspentOutputs(TwUnspentOutputs): @@ -242,10 +243,10 @@ Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view, 'q':'a_quit','p':'a_print','v':'a_view','w':'a_view_wide', 'l':'a_lbl_add','D':'a_addr_delete','R':'a_balance_refresh' } - async def __ainit__(self,*args,**kwargs): + async def __ainit__(self,proto,*args,**kwargs): if g.use_cached_balances: self.hdr_fmt += '\n' + yellow('WARNING: Using cached balances. These may be out of date!') - await TwUnspentOutputs.__ainit__(self,*args,**kwargs) + await TwUnspentOutputs.__ainit__(self,proto,*args,**kwargs) def do_sort(self,key=None,reverse=False): if key == 'txid': return @@ -256,22 +257,15 @@ Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view, if self.addrs: wl = [d for d in wl if d['addr'] in self.addrs] return [{ - 'account': TwLabel(d['mmid']+' '+d['comment'],on_fail='raise'), + 'account': TwLabel(self.proto,d['mmid']+' '+d['comment'],on_fail='raise'), 'address': d['addr'], 'amount': await self.wallet.get_balance(d['addr']), 'confirmations': 0, # TODO } for d in wl] - class MMGenTwUnspentOutput(MMGenListItem): - txid = ListItemAttr('CoinTxID') - vout = ListItemAttr(int,typeconv=False) - amt = ImmutableAttr(lambda val:g.proto.coin_amt(val),typeconv=False) - amt2 = ListItemAttr(lambda val:g.proto.coin_amt(val),typeconv=False) - label = ListItemAttr('TwComment',reassign_ok=True) - twmmid = ImmutableAttr('TwMMGenID') - addr = ImmutableAttr('CoinAddr') - confs = ImmutableAttr(int,typeconv=False) - skip = ListItemAttr(str,typeconv=False,reassign_ok=True) + class MMGenTwUnspentOutput(TwUnspentOutputs.MMGenTwUnspentOutput): + valid_attrs = {'txid','vout','amt','amt2','label','twmmid','addr','confs','skip'} + invalid_attrs = {'proto'} def age_disp(self,o,age_fmt): # TODO return None @@ -294,25 +288,26 @@ class EthereumTwAddrList(TwAddrList): has_age = False - async def __ainit__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None): + async def __ainit__(self,proto,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None): - self.wallet = wallet or await TrackingWallet(mode='w') + self.proto = proto + self.wallet = wallet or await TrackingWallet(self.proto,mode='w') tw_dict = self.wallet.mmid_ordered_dict - self.total = g.proto.coin_amt('0') + self.total = self.proto.coin_amt('0') from mmgen.obj import CoinAddr for mmid,d in list(tw_dict.items()): # if d['confirmations'] < minconf: continue # cannot get confirmations for eth account - label = TwLabel(mmid+' '+d['comment'],on_fail='raise') + label = TwLabel(self.proto,mmid+' '+d['comment'],on_fail='raise') if usr_addr_list and (label.mmid not in usr_addr_list): continue bal = await self.wallet.get_balance(d['addr']) if bal == 0 and not showempty: if not label.comment or not all_labels: continue - self[label.mmid] = {'amt': g.proto.coin_amt('0'), 'lbl': label } + self[label.mmid] = {'amt': self.proto.coin_amt('0'), 'lbl': label } if showbtcaddrs: - self[label.mmid]['addr'] = CoinAddr(d['addr']) + self[label.mmid]['addr'] = CoinAddr(self.proto,d['addr']) self[label.mmid]['lbl'].mmid.confs = None self[label.mmid]['amt'] += bal self.total += bal @@ -326,9 +321,9 @@ class EthereumTwGetBalance(TwGetBalance): fs = '{w:13} {c}\n' # TODO - for now, just suppress display of meaningless data - async def __ainit__(self,*args,**kwargs): - self.wallet = await TrackingWallet(mode='w') - await TwGetBalance.__ainit__(self,*args,**kwargs) + async def __ainit__(self,proto,*args,**kwargs): + self.wallet = await TrackingWallet(proto,mode='w') + await TwGetBalance.__ainit__(self,proto,*args,**kwargs) async def create_data(self): data = self.wallet.mmid_ordered_dict @@ -336,7 +331,7 @@ class EthereumTwGetBalance(TwGetBalance): if d.type == 'mmgen': key = d.obj.sid if key not in self.data: - self.data[key] = [g.proto.coin_amt('0')] * 4 + self.data[key] = [self.proto.coin_amt('0')] * 4 else: key = 'Non-MMGen' @@ -350,10 +345,9 @@ class EthereumTwGetBalance(TwGetBalance): class EthereumTwAddrData(TwAddrData): - @classmethod - async def get_tw_data(cls,wallet=None): + async def get_tw_data(self,wallet=None): vmsg('Getting address data from tracking wallet') - tw = (wallet or await TrackingWallet()).mmid_ordered_dict + tw = (wallet or await TrackingWallet(self.proto)).mmid_ordered_dict # emulate the output of RPC 'listaccounts' and 'getaddressesbyaccount' return [(mmid+' '+d['comment'],[d['addr']]) for mmid,d in list(tw.items())] diff --git a/mmgen/altcoins/eth/tx.py b/mmgen/altcoins/eth/tx.py index 1398e68a..f9e17d7a 100755 --- a/mmgen/altcoins/eth/tx.py +++ b/mmgen/altcoins/eth/tx.py @@ -25,523 +25,534 @@ from mmgen.common import * from mmgen.exception import TransactionChainMismatch from mmgen.obj import * -from mmgen.tx import MMGenTX,MMGenBumpTX,MMGenSplitTX,MMGenTxForSigning +from mmgen.tx import MMGenTX from mmgen.tw import TrackingWallet from .contract import Token -class EthereumMMGenTX(MMGenTX): - desc = 'Ethereum transaction' - contract_desc = 'contract' - tx_gas = ETHAmt(21000,'wei') # an approximate number, used for fee estimation purposes - start_gas = ETHAmt(21000,'wei') # the actual startgas amt used in the transaction - # for simple sends with no data, tx_gas = start_gas = 21000 - fee_fail_fs = 'Network fee estimation failed' - no_chg_msg = 'Warning: Transaction leaves account with zero balance' - rel_fee_desc = 'gas price' - rel_fee_disp = 'gas price in Gwei' - txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} Sig={s} Locktime={l}\n' - txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} Sig={s} Locktime={l}\n' - txview_ftr_fs = 'Total in account: {i} {d}\nTotal to spend: {o} {d}\nTX fee: {a} {c}{r}\n' - txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n' - fmt_keys = ('from','to','amt','nonce') - usr_fee_prompt = 'Enter transaction fee or gas price: ' - fn_fee_unit = 'Mwei' - usr_rel_fee = None # not in MMGenTX - disable_fee_check = False - txobj = None # "" - usr_contract_data = HexStr('') +class EthereumMMGenTX: - def __init__(self,*args,**kwargs): - MMGenTX.__init__(self,*args,**kwargs) - if hasattr(opt,'tx_gas') and opt.tx_gas: - self.tx_gas = self.start_gas = ETHAmt(int(opt.tx_gas),'wei') - if hasattr(opt,'contract_data') and opt.contract_data: - m = "'--contract-data' option may not be used with token transaction" - assert not 'Token' in type(self).__name__, m - self.usr_contract_data = HexStr(open(opt.contract_data).read().strip()) - self.disable_fee_check = True + class Base(MMGenTX.Base): - def check_txfile_hex_data(self): - pass + rel_fee_desc = 'gas price' + rel_fee_disp = 'gas price in Gwei' + txobj = None # "" + tx_gas = ETHAmt(21000,'wei') # an approximate number, used for fee estimation purposes + start_gas = ETHAmt(21000,'wei') # the actual startgas amt used in the transaction + # for simple sends with no data, tx_gas = start_gas = 21000 + contract_desc = 'contract' + usr_contract_data = HexStr('') + disable_fee_check = False - @classmethod - async def get_exec_status(cls,txid,silent=False): - d = await g.rpc.call('eth_getTransactionReceipt','0x'+txid) - if not silent: - if 'contractAddress' in d and d['contractAddress']: - msg('Contract address: {}'.format(d['contractAddress'].replace('0x',''))) - return int(d['status'],16) + # given absolute fee in ETH, return gas price in Gwei using tx_gas + def fee_abs2rel(self,abs_fee,to_unit='Gwei'): + ret = ETHAmt(int(abs_fee.toWei() // self.tx_gas.toWei()),'wei') + dmsg('fee_abs2rel() ==> {} ETH'.format(ret)) + return ret if to_unit == 'eth' else ret.to_unit(to_unit,show_decimal=True) - def is_replaceable(self): return True + def get_fee(self): + return self.fee - def get_fee(self): - return self.fee + def get_hex_locktime(self): + return None # TODO - def check_fee(self): - assert self.disable_fee_check or (self.fee <= g.proto.max_tx_fee) + # given rel fee in wei, return absolute fee using tx_gas (not in MMGenTX) + def fee_rel2abs(self,rel_fee): + assert isinstance(rel_fee,int),"'{}': incorrect type for fee estimate (not an integer)".format(rel_fee) + return ETHAmt(rel_fee * self.tx_gas.toWei(),'wei') - def get_hex_locktime(self): return None # TODO - - def check_pubkey_scripts(self): pass - - def check_sigs(self,deserial_tx=None): - if is_hex_str(self.hex): - self.mark_signed() + def is_replaceable(self): return True - return False - def parse_txfile_hex_data(self): - from .pyethereum.transactions import Transaction - from . import rlp - etx = rlp.decode(bytes.fromhex(self.hex),Transaction) - d = etx.to_dict() # ==> hex values have '0x' prefix, 0 is '0x' - for k in ('sender','to','data'): - if k in d: d[k] = d[k].replace('0x','',1) - o = { - 'from': CoinAddr(d['sender']), - 'to': CoinAddr(d['to']) if d['to'] else Str(''), # NB: for token, 'to' is token address - 'amt': ETHAmt(d['value'],'wei'), - 'gasPrice': ETHAmt(d['gasprice'],'wei'), - 'startGas': ETHAmt(d['startgas'],'wei'), - 'nonce': ETHNonce(d['nonce']), - 'data': HexStr(d['data']) } - if o['data'] and not o['to']: # token- or contract-creating transaction - o['token_addr'] = TokenAddr(etx.creates.hex()) # NB: could be a non-token contract address - self.disable_fee_check = True - txid = CoinTxID(etx.hash.hex()) - assert txid == self.coin_txid,"txid in tx.hex doesn't match value in MMGen transaction file" - self.tx_gas = o['startGas'] # approximate, but better than nothing - self.fee = self.fee_rel2abs(o['gasPrice'].toWei()) - self.txobj = o - return d # 'token_addr','decimals' required by Token subclass + async def get_exec_status(self,txid,silent=False): + d = await self.rpc.call('eth_getTransactionReceipt','0x'+txid) + if not silent: + if 'contractAddress' in d and d['contractAddress']: + msg('Contract address: {}'.format(d['contractAddress'].replace('0x',''))) + return int(d['status'],16) - async def get_nonce(self): - return ETHNonce(int(await g.rpc.call('parity_nextNonce','0x'+self.inputs[0].addr),16)) + class New(Base,MMGenTX.New): + hexdata_type = 'hex' + desc = 'transaction' + fee_fail_fs = 'Network fee estimation failed' + no_chg_msg = 'Warning: Transaction leaves account with zero balance' + usr_fee_prompt = 'Enter transaction fee or gas price: ' + usr_rel_fee = None # not in MMGenTX - async def make_txobj(self): # called by create_raw() - chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in g.rpc.caps] - self.txobj = { - 'from': self.inputs[0].addr, - 'to': self.outputs[0].addr if self.outputs else Str(''), - 'amt': self.outputs[0].amt if self.outputs else ETHAmt('0'), - 'gasPrice': self.usr_rel_fee or self.fee_abs2rel(self.fee,to_unit='eth'), - 'startGas': self.start_gas, - 'nonce': await self.get_nonce(), - 'chainId': Int(await g.rpc.call(chain_id_method),16), - 'data': self.usr_contract_data, - } + def __init__(self,*args,**kwargs): + MMGenTX.New.__init__(self,*args,**kwargs) + if getattr(opt,'tx_gas',None): + self.tx_gas = self.start_gas = ETHAmt(int(opt.tx_gas),'wei') + if getattr(opt,'contract_data',None): + m = "'--contract-data' option may not be used with token transaction" + assert not 'Token' in type(self).__name__, m + self.usr_contract_data = HexStr(open(opt.contract_data).read().strip()) + self.disable_fee_check = True - # Instead of serializing tx data as with BTC, just create a JSON dump. - # This complicates things but means we avoid using the rlp library to deserialize the data, - # thus removing an attack vector - async def create_raw(self): - assert len(self.inputs) == 1,'Transaction has more than one input!' - o_num = len(self.outputs) - o_ok = 0 if self.usr_contract_data else 1 - assert o_num == o_ok,'Transaction has {} output{} (should have {})'.format(o_num,suf(o_num),o_ok) - await self.make_txobj() - odict = { k: str(v) for k,v in self.txobj.items() if k != 'token_to' } - self.hex = json.dumps(odict) - self.update_txid() + async def get_nonce(self): + return ETHNonce(int(await self.rpc.call('parity_nextNonce','0x'+self.inputs[0].addr),16)) - def del_output(self,idx): - pass + async def make_txobj(self): # called by create_raw() + chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in self.rpc.caps] + self.txobj = { + 'from': self.inputs[0].addr, + 'to': self.outputs[0].addr if self.outputs else Str(''), + 'amt': self.outputs[0].amt if self.outputs else ETHAmt('0'), + 'gasPrice': self.usr_rel_fee or self.fee_abs2rel(self.fee,to_unit='eth'), + 'startGas': self.start_gas, + 'nonce': await self.get_nonce(), + 'chainId': Int(await self.rpc.call(chain_id_method),16), + 'data': self.usr_contract_data, + } - def update_txid(self): - assert not is_hex_str(self.hex),'update_txid() must be called only when self.hex is not hex data' - self.txid = MMGenTxID(make_chksum_6(self.hex).upper()) + # Instead of serializing tx data as with BTC, just create a JSON dump. + # This complicates things but means we avoid using the rlp library to deserialize the data, + # thus removing an attack vector + async def create_raw(self): + assert len(self.inputs) == 1,'Transaction has more than one input!' + o_num = len(self.outputs) + o_ok = 0 if self.usr_contract_data else 1 + assert o_num == o_ok,'Transaction has {} output{} (should have {})'.format(o_num,suf(o_num),o_ok) + await self.make_txobj() + odict = { k: str(v) for k,v in self.txobj.items() if k != 'token_to' } + self.hex = json.dumps(odict) + self.update_txid() - def process_cmd_args(self,cmd_args,ad_f,ad_w): - lc = len(cmd_args) - if lc == 0 and self.usr_contract_data and not 'Token' in type(self).__name__: - return - if lc != 1: - fs = '{} output{} specified, but Ethereum transactions must have exactly one' - die(1,fs.format(lc,suf(lc))) + def update_txid(self): + assert not is_hex_str(self.hex),'update_txid() must be called only when self.hex is not hex data' + self.txid = MMGenTxID(make_chksum_6(self.hex).upper()) - for a in cmd_args: - self.process_cmd_arg(a,ad_f,ad_w) + def del_output(self,idx): + pass - def select_unspent(self,unspent): - prompt = 'Enter an account to spend from: ' - while True: - reply = my_raw_input(prompt).strip() - if reply: - if not is_int(reply): - msg('Account number must be an integer') - elif int(reply) < 1: - msg('Account number must be >= 1') - elif int(reply) > len(unspent): - msg('Account number must be <= {}'.format(len(unspent))) - else: - return [int(reply)] + def process_cmd_args(self,cmd_args,ad_f,ad_w): + lc = len(cmd_args) + if lc == 0 and self.usr_contract_data and not 'Token' in type(self).__name__: + return + if lc != 1: + die(1,f'{lc} output{suf(lc)} specified, but Ethereum transactions must have exactly one') - # coin-specific fee routines: - @property - def relay_fee(self): - return ETHAmt('0') # TODO + for a in cmd_args: + self.process_cmd_arg(a,ad_f,ad_w) - # given absolute fee in ETH, return gas price in Gwei using tx_gas - def fee_abs2rel(self,abs_fee,to_unit='Gwei'): - ret = ETHAmt(int(abs_fee.toWei() // self.tx_gas.toWei()),'wei') - dmsg('fee_abs2rel() ==> {} ETH'.format(ret)) - return ret if to_unit == 'eth' else ret.to_unit(to_unit,show_decimal=True) + def select_unspent(self,unspent): + prompt = 'Enter an account to spend from: ' + while True: + reply = my_raw_input(prompt).strip() + if reply: + if not is_int(reply): + msg('Account number must be an integer') + elif int(reply) < 1: + msg('Account number must be >= 1') + elif int(reply) > len(unspent): + msg('Account number must be <= {}'.format(len(unspent))) + else: + return [int(reply)] - # get rel_fee (gas price) from network, return in native wei - async def get_rel_fee_from_network(self): - return Int(await g.rpc.call('eth_gasPrice'),16),'eth_gasPrice' # ==> rel_fee,fe_type + # coin-specific fee routines: + @property + def relay_fee(self): + return ETHAmt('0') # TODO - # given rel fee and units, return absolute fee using tx_gas - def convert_fee_spec(self,foo,units,amt,unit): - self.usr_rel_fee = ETHAmt(int(amt),units[unit]) - return ETHAmt(self.usr_rel_fee.toWei() * self.tx_gas.toWei(),'wei') + # get rel_fee (gas price) from network, return in native wei + async def get_rel_fee_from_network(self): + return Int(await self.rpc.call('eth_gasPrice'),16),'eth_gasPrice' # ==> rel_fee,fe_type - # given rel fee in wei, return absolute fee using tx_gas (not in MMGenTX) - def fee_rel2abs(self,rel_fee): - assert isinstance(rel_fee,int),"'{}': incorrect type for fee estimate (not an integer)".format(rel_fee) - return ETHAmt(rel_fee * self.tx_gas.toWei(),'wei') + def check_fee(self): + assert self.disable_fee_check or (self.fee <= self.proto.max_tx_fee) - # given fee estimate (gas price) in wei, return absolute fee, adjusting by opt.tx_fee_adj - def fee_est2abs(self,rel_fee,fe_type=None): - ret = self.fee_rel2abs(rel_fee) * opt.tx_fee_adj - if opt.verbose: - msg('Estimated fee: {} ETH'.format(ret)) - return ret + # given rel fee and units, return absolute fee using tx_gas + def convert_fee_spec(self,foo,units,amt,unit): + self.usr_rel_fee = ETHAmt(int(amt),units[unit]) + return ETHAmt(self.usr_rel_fee.toWei() * self.tx_gas.toWei(),'wei') - def convert_and_check_fee(self,tx_fee,desc='Missing description'): - abs_fee = self.process_fee_spec(tx_fee,None) - if abs_fee == False: - return False - elif not self.disable_fee_check and (abs_fee > g.proto.max_tx_fee): - m = '{} {c}: {} fee too large (maximum fee: {} {c})' - msg(m.format(abs_fee.hl(),desc,g.proto.max_tx_fee.hl(),c=g.coin)) - return False - else: - return abs_fee + # given fee estimate (gas price) in wei, return absolute fee, adjusting by opt.tx_fee_adj + def fee_est2abs(self,rel_fee,fe_type=None): + ret = self.fee_rel2abs(rel_fee) * opt.tx_fee_adj + if opt.verbose: + msg('Estimated fee: {} ETH'.format(ret)) + return ret - def update_change_output(self,change_amt): - if self.outputs and self.outputs[0].is_chg: - self.update_output_amt(0,ETHAmt(change_amt)) - - def update_send_amt(self,foo): - if self.outputs: - self.send_amt = self.outputs[0].amt - - def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse,sort): - m = {} - for k in ('inputs','outputs'): - if len(getattr(self,k)): - m[k] = getattr(self,k)[0].mmid if len(getattr(self,k)) else '' - m[k] = ' ' + m[k].hl() if m[k] else ' ' + MMGenID.hlc(nonmm_str) - fs = """From: {}{f_mmid} - To: {}{t_mmid} - Amount: {} {c} - Gas price: {g} Gwei - Start gas: {G} Kwei - Nonce: {} - Data: {d} - \n""".replace('\t','') - ld = len(self.txobj['data']) - return fs.format( *((self.txobj[k] if self.txobj[k] != '' else Str('None')).hl() for k in self.fmt_keys), - d='{}... ({} bytes)'.format(self.txobj['data'][:40],ld//2) if ld else Str('None'), - c=g.dcoin if len(self.outputs) else '', - g=yellow(str(self.txobj['gasPrice'].to_unit('Gwei',show_decimal=True))), - G=yellow(str(self.txobj['startGas'].toKwei())), - t_mmid=m['outputs'] if len(self.outputs) else '', - f_mmid=m['inputs']) - - def format_view_abs_fee(self): - fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei()) - note = ' (max)' if self.txobj['data'] else '' - return fee.hl() + note - - def format_view_rel_fee(self,terse): return '' - def format_view_verbose_footer(self): return '' # TODO - - def resolve_g_token_from_txfile(self): - die(2,"The '--token' option must be specified for token transaction files") - - def final_inputs_ok_msg(self,change_amt): - m = "Transaction leaves {} {} in the sender's account" - chg = '0' if (self.outputs and self.outputs[0].is_chg) else change_amt - return m.format(ETHAmt(chg).hl(),g.coin) - - async def get_status(self,status=False): - - class r(object): pass - - async def is_in_mempool(): - if not 'full_node' in g.rpc.caps: + def convert_and_check_fee(self,tx_fee,desc='Missing description'): + abs_fee = self.process_fee_spec(tx_fee,None) + if abs_fee == False: return False - return '0x'+self.coin_txid in [x['hash'] for x in await g.rpc.call('parity_pendingTransactions')] + elif not self.disable_fee_check and (abs_fee > self.proto.max_tx_fee): + m = '{} {c}: {} fee too large (maximum fee: {} {c})' + msg(m.format(abs_fee.hl(),desc,self.proto.max_tx_fee.hl(),c=self.proto.coin)) + return False + else: + return abs_fee - async def is_in_wallet(): - d = await g.rpc.call('eth_getTransactionReceipt','0x'+self.coin_txid) - if d and 'blockNumber' in d and d['blockNumber'] is not None: - r.confs = 1 + int(await g.rpc.call('eth_blockNumber'),16) - int(d['blockNumber'],16) - r.exec_status = int(d['status'],16) + def update_change_output(self,change_amt): + if self.outputs and self.outputs[0].is_chg: + self.update_output_amt(0,ETHAmt(change_amt)) + + def update_send_amt(self,foo): + if self.outputs: + self.send_amt = self.outputs[0].amt + + async def get_cmdline_input_addrs(self): + ret = [] + if opt.inputs: + r = (await TrackingWallet(self.proto)).data_root # must create new instance here + m = 'Address {!r} not in tracking wallet' + for i in opt.inputs.split(','): + if is_mmgen_id(self.proto,i): + for addr in r: + if r[addr]['mmid'] == i: + ret.append(addr) + break + else: + raise UserAddressNotInWallet(m.format(i)) + elif is_coin_addr(self.proto,i): + if not i in r: + raise UserAddressNotInWallet(m.format(i)) + ret.append(i) + else: + die(1,"'{}': not an MMGen ID or coin address".format(i)) + return ret + + def final_inputs_ok_msg(self,change_amt): + m = "Transaction leaves {} {} in the sender's account" + chg = '0' if (self.outputs and self.outputs[0].is_chg) else change_amt + return m.format(ETHAmt(chg).hl(),self.proto.coin) + + class Completed(Base,MMGenTX.Completed): + fn_fee_unit = 'Mwei' + txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} Sig={s} Locktime={l}\n' + txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} Sig={s} Locktime={l}\n' + txview_ftr_fs = 'Total in account: {i} {d}\nTotal to spend: {o} {d}\nTX fee: {a} {c}{r}\n' + txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n' + fmt_keys = ('from','to','amt','nonce') + + def check_txfile_hex_data(self): + pass + + def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse,sort): + m = {} + for k in ('inputs','outputs'): + if len(getattr(self,k)): + m[k] = getattr(self,k)[0].mmid if len(getattr(self,k)) else '' + m[k] = ' ' + m[k].hl() if m[k] else ' ' + MMGenID.hlc(nonmm_str) + fs = """From: {}{f_mmid} + To: {}{t_mmid} + Amount: {} {c} + Gas price: {g} Gwei + Start gas: {G} Kwei + Nonce: {} + Data: {d} + \n""".replace('\t','') + t = self.txobj + td = t['data'] + return fs.format( + *((t[k] if t[k] != '' else Str('None')).hl() for k in self.fmt_keys), + d = '{}... ({} bytes)'.format(td[:40],len(td)//2) if len(td) else Str('None'), + c = self.proto.dcoin if len(self.outputs) else '', + g = yellow(str(t['gasPrice'].to_unit('Gwei',show_decimal=True))), + G = yellow(str(t['startGas'].toKwei())), + t_mmid = m['outputs'] if len(self.outputs) else '', + f_mmid = m['inputs'] ) + + def format_view_abs_fee(self): + fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei()) + note = ' (max)' if self.txobj['data'] else '' + return fee.hl() + note + + def format_view_rel_fee(self,terse): + return '' + + def format_view_verbose_footer(self): + if self.txobj['data']: + from .contract import parse_abi + return '\nParsed contract data: ' + pp_fmt(parse_abi(self.txobj['data'])) + else: + return '' + + def check_sigs(self,deserial_tx=None): # TODO + if is_hex_str(self.hex): return True return False - if await is_in_mempool(): - msg('Transaction is in mempool' if status else 'Warning: transaction is in mempool!') - return + def check_pubkey_scripts(self): + pass - if status: - if await is_in_wallet(): - if self.txobj['data']: - cd = capfirst(self.contract_desc) - if r.exec_status == 0: - msg('{} failed to execute!'.format(cd)) - else: - msg('{} successfully executed with status {}'.format(cd,r.exec_status)) - die(0,'Transaction has {} confirmation{}'.format(r.confs,suf(r.confs))) - die(1,'Transaction is neither in mempool nor blockchain!') + class Unsigned(Completed,MMGenTX.Unsigned): + hexdata_type = 'json' + desc = 'unsigned transaction' - async def send(self,prompt_user=True,exit_on_fail=False): + def parse_txfile_hex_data(self): + d = json.loads(self.hex) + o = { + 'from': CoinAddr(self.proto,d['from']), + # NB: for token, 'to' is sendto address + 'to': CoinAddr(self.proto,d['to']) if d['to'] else Str(''), + 'amt': ETHAmt(d['amt']), + 'gasPrice': ETHAmt(d['gasPrice']), + 'startGas': ETHAmt(d['startGas']), + 'nonce': ETHNonce(d['nonce']), + 'chainId': Int(d['chainId']), + 'data': HexStr(d['data']) } + self.tx_gas = o['startGas'] # approximate, but better than nothing + self.fee = self.fee_rel2abs(o['gasPrice'].toWei()) + self.txobj = o + return d # 'token_addr','decimals' required by Token subclass - if not self.marked_signed(): - die(1,'Transaction is not signed!') + async def do_sign(self,wif,tx_num_str): + o = self.txobj + o_conv = { + 'to': bytes.fromhex(o['to']), + 'startgas': o['startGas'].toWei(), + 'gasprice': o['gasPrice'].toWei(), + 'value': o['amt'].toWei() if o['amt'] else 0, + 'nonce': o['nonce'], + 'data': bytes.fromhex(o['data']) } - self.check_correct_chain() + from .pyethereum.transactions import Transaction + etx = Transaction(**o_conv).sign(wif,o['chainId']) + assert etx.sender.hex() == o['from'],( + 'Sender address recovered from signature does not match true sender') - fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei()) + from . import rlp + self.hex = rlp.encode(etx).hex() + self.coin_txid = CoinTxID(etx.hash.hex()) - if not self.disable_fee_check and (fee > g.proto.max_tx_fee): - die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format( - fee, - g.proto.name, - g.proto.max_tx_fee, - g.proto.coin )) + if o['data']: + if o['to']: + assert self.txobj['token_addr'] == TokenAddr(etx.creates.hex()),'Token address mismatch' + else: # token- or contract-creating transaction + self.txobj['token_addr'] = TokenAddr(self.proto,etx.creates.hex()) - await self.get_status() + assert self.check_sigs(),'Signature check failed' - if prompt_user: - self.confirm_send() + async def sign(self,tx_num_str,keys): # return TX object or False; don't exit or raise exception - if g.bogus_send: - ret = None - else: try: - ret = await g.rpc.call('eth_sendRawTransaction','0x'+self.hex) - except: - raise - ret = False + self.check_correct_chain() + except TransactionChainMismatch: + return False - if ret == False: - msg(red('Send of MMGen transaction {} failed'.format(self.txid))) - if exit_on_fail: - sys.exit(1) - return False - else: - if g.bogus_send: - m = 'BOGUS transaction NOT sent: {}' - else: - m = 'Transaction sent: {}' - assert ret == '0x'+self.coin_txid,'txid mismatch (after sending)' - self.desc = 'sent transaction' - msg(m.format(self.coin_txid.hl())) - self.add_timestamp() - self.add_blockcount() - return True + msg_r(f'Signing transaction{tx_num_str}...') - async def get_cmdline_input_addrs(self): - ret = [] - if opt.inputs: - r = (await TrackingWallet()).data_root # must create new instance here - m = 'Address {!r} not in tracking wallet' - for i in opt.inputs.split(','): - if is_mmgen_id(i): - for addr in r: - if r[addr]['mmid'] == i: - ret.append(addr) - break - else: - raise UserAddressNotInWallet(m.format(i)) - elif is_coin_addr(i): - if not i in r: - raise UserAddressNotInWallet(m.format(i)) - ret.append(i) - else: - die(1,"'{}': not an MMGen ID or coin address".format(i)) - return ret + try: + await self.do_sign(keys[0].sec.wif,tx_num_str) + msg('OK') + return MMGenTX.Signed(data=self.__dict__) + except Exception as e: + msg("{e!s}: transaction signing failed!") + if g.traceback: + import traceback + ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info()))) + return False - def print_contract_addr(self): - if 'token_addr' in self.txobj: - msg('Contract address: {}'.format(self.txobj['token_addr'].hl())) + class Signed(Completed,MMGenTX.Signed): -class EthereumMMGenTxForSigning(EthereumMMGenTX,MMGenTxForSigning): + desc = 'signed transaction' - def parse_txfile_hex_data(self): - d = json.loads(self.hex) - o = { - 'from': CoinAddr(d['from']), - 'to': CoinAddr(d['to']) if d['to'] else Str(''), # NB: for token, 'to' is sendto address - 'amt': ETHAmt(d['amt']), - 'gasPrice': ETHAmt(d['gasPrice']), - 'startGas': ETHAmt(d['startGas']), - 'nonce': ETHNonce(d['nonce']), - 'chainId': Int(d['chainId']), - 'data': HexStr(d['data']) } - self.tx_gas = o['startGas'] # approximate, but better than nothing - self.fee = self.fee_rel2abs(o['gasPrice'].toWei()) - self.txobj = o - return d # 'token_addr','decimals' required by Token subclass + def parse_txfile_hex_data(self): + from .pyethereum.transactions import Transaction + from . import rlp + etx = rlp.decode(bytes.fromhex(self.hex),Transaction) + d = etx.to_dict() # ==> hex values have '0x' prefix, 0 is '0x' + for k in ('sender','to','data'): + if k in d: + d[k] = d[k].replace('0x','',1) + o = { + 'from': CoinAddr(self.proto,d['sender']), + # NB: for token, 'to' is token address + 'to': CoinAddr(self.proto,d['to']) if d['to'] else Str(''), + 'amt': ETHAmt(d['value'],'wei'), + 'gasPrice': ETHAmt(d['gasprice'],'wei'), + 'startGas': ETHAmt(d['startgas'],'wei'), + 'nonce': ETHNonce(d['nonce']), + 'data': HexStr(d['data']) } + if o['data'] and not o['to']: # token- or contract-creating transaction + # NB: could be a non-token contract address: + o['token_addr'] = TokenAddr(self.proto,etx.creates.hex()) + self.disable_fee_check = True + txid = CoinTxID(etx.hash.hex()) + assert txid == self.coin_txid,"txid in tx.hex doesn't match value in MMGen transaction file" + self.tx_gas = o['startGas'] # approximate, but better than nothing + self.fee = self.fee_rel2abs(o['gasPrice'].toWei()) + self.txobj = o + return d # 'token_addr','decimals' required by Token subclass - async def do_sign(self,wif,tx_num_str): - o = self.txobj - o_conv = { - 'to': bytes.fromhex(o['to']), - 'startgas': o['startGas'].toWei(), - 'gasprice': o['gasPrice'].toWei(), - 'value': o['amt'].toWei() if o['amt'] else 0, - 'nonce': o['nonce'], - 'data': bytes.fromhex(o['data']) } + async def get_status(self,status=False): - from .pyethereum.transactions import Transaction - etx = Transaction(**o_conv).sign(wif,o['chainId']) - assert etx.sender.hex() == o['from'],( - 'Sender address recovered from signature does not match true sender') + class r(object): + pass - from . import rlp - self.hex = rlp.encode(etx).hex() - self.coin_txid = CoinTxID(etx.hash.hex()) + async def is_in_mempool(): + if not 'full_node' in self.rpc.caps: + return False + return '0x'+self.coin_txid in [ + x['hash'] for x in await self.rpc.call('parity_pendingTransactions') ] - if o['data']: - if o['to']: - assert self.txobj['token_addr'] == TokenAddr(etx.creates.hex()),'Token address mismatch' - else: # token- or contract-creating transaction - self.txobj['token_addr'] = TokenAddr(etx.creates.hex()) + async def is_in_wallet(): + d = await self.rpc.call('eth_getTransactionReceipt','0x'+self.coin_txid) + if d and 'blockNumber' in d and d['blockNumber'] is not None: + r.confs = 1 + int(await self.rpc.call('eth_blockNumber'),16) - int(d['blockNumber'],16) + r.exec_status = int(d['status'],16) + return True + return False - assert self.check_sigs(),'Signature check failed' + if await is_in_mempool(): + msg('Transaction is in mempool' if status else 'Warning: transaction is in mempool!') + return - async def sign(self,tx_num_str,keys): # return True or False; don't exit or raise exception + if status: + if await is_in_wallet(): + if self.txobj['data']: + cd = capfirst(self.contract_desc) + if r.exec_status == 0: + msg('{} failed to execute!'.format(cd)) + else: + msg('{} successfully executed with status {}'.format(cd,r.exec_status)) + die(0,'Transaction has {} confirmation{}'.format(r.confs,suf(r.confs))) + die(1,'Transaction is neither in mempool nor blockchain!') - if self.marked_signed(): - msg('Transaction is already signed!') - return False + async def send(self,prompt_user=True,exit_on_fail=False): - try: self.check_correct_chain() - except TransactionChainMismatch: - return False - msg_r('Signing transaction{}...'.format(tx_num_str)) + fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei()) - try: - await self.do_sign(keys[0].sec.wif,tx_num_str) - msg('OK') - return True - except Exception as e: - m = "{!r}: transaction signing failed!" - msg(m.format(e.args[0])) - if g.traceback: - import traceback - ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info()))) - return False + if not self.disable_fee_check and (fee > self.proto.max_tx_fee): + die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format( + fee, + self.proto.name, + self.proto.max_tx_fee, + self.proto.coin )) -class EthereumTokenMMGenTX(EthereumMMGenTX): - desc = 'Ethereum token transaction' - contract_desc = 'token contract' - tx_gas = ETHAmt(52000,'wei') - start_gas = ETHAmt(60000,'wei') - fmt_keys = ('from','token_to','amt','nonce') - fee_is_approximate = True + await self.get_status() - def update_change_output(self,change_amt): - if self.outputs[0].is_chg: - self.update_output_amt(0,self.inputs[0].amt) + if prompt_user: + self.confirm_send() - # token transaction, so check both eth and token balances - # TODO: add test with insufficient funds - async def precheck_sufficient_funds(self,inputs_sum,sel_unspent): - eth_bal = await self.tw.get_eth_balance(sel_unspent[0].addr) - if eth_bal == 0: # we don't know the fee yet - msg('This account has no ether to pay for the transaction fee!') - return False - return await super().precheck_sufficient_funds(inputs_sum,sel_unspent) + if g.bogus_send: + ret = None + else: + try: + ret = await self.rpc.call('eth_sendRawTransaction','0x'+self.hex) + except: + raise + ret = False - def final_inputs_ok_msg(self,change_amt): - token_bal = ( ETHAmt('0') if self.outputs[0].is_chg else - self.inputs[0].amt - self.outputs[0].amt ) - m = "Transaction leaves ≈{} {} and {} {} in the sender's account" - return m.format( change_amt.hl(), g.coin, token_bal.hl(), g.dcoin ) + if ret == False: + msg(red('Send of MMGen transaction {} failed'.format(self.txid))) + if exit_on_fail: + sys.exit(1) + return False + else: + if g.bogus_send: + m = 'BOGUS transaction NOT sent: {}' + else: + m = 'Transaction sent: {}' + assert ret == '0x'+self.coin_txid,'txid mismatch (after sending)' + self.desc = 'sent transaction' + msg(m.format(self.coin_txid.hl())) + self.add_timestamp() + self.add_blockcount() + return True - async def get_change_amt(self): # here we know the fee - eth_bal = await self.tw.get_eth_balance(self.inputs[0].addr) - return eth_bal - self.fee + def print_contract_addr(self): + if 'token_addr' in self.txobj: + msg('Contract address: {}'.format(self.txobj['token_addr'].hl())) - def resolve_g_token_from_txfile(self): + class Bump(MMGenTX.Bump,Completed,New): + + @property + def min_fee(self): + return ETHAmt(self.fee * Decimal('1.101')) + + def update_fee(self,foo,fee): + self.fee = fee + + async def get_nonce(self): + return self.txobj['nonce'] + +class EthereumTokenMMGenTX: + + class Base(EthereumMMGenTX.Base): + tx_gas = ETHAmt(52000,'wei') + start_gas = ETHAmt(60000,'wei') + contract_desc = 'token contract' + + class New(Base,EthereumMMGenTX.New): + desc = 'transaction' + fee_is_approximate = True + + async def make_txobj(self): # called by create_raw() + await super().make_txobj() + t = Token(self.proto,self.tw.token,self.tw.decimals) + o = self.txobj + o['token_addr'] = t.addr + o['decimals'] = t.decimals + o['token_to'] = o['to'] + o['data'] = t.create_data(o['token_to'],o['amt']) + + def update_change_output(self,change_amt): + if self.outputs[0].is_chg: + self.update_output_amt(0,self.inputs[0].amt) + + # token transaction, so check both eth and token balances + # TODO: add test with insufficient funds + async def precheck_sufficient_funds(self,inputs_sum,sel_unspent): + eth_bal = await self.tw.get_eth_balance(sel_unspent[0].addr) + if eth_bal == 0: # we don't know the fee yet + msg('This account has no ether to pay for the transaction fee!') + return False + return await super().precheck_sufficient_funds(inputs_sum,sel_unspent) + + async def get_change_amt(self): # here we know the fee + eth_bal = await self.tw.get_eth_balance(self.inputs[0].addr) + return eth_bal - self.fee + + def final_inputs_ok_msg(self,change_amt): + token_bal = ( ETHAmt('0') if self.outputs[0].is_chg else + self.inputs[0].amt - self.outputs[0].amt ) + m = "Transaction leaves ≈{} {} and {} {} in the sender's account" + return m.format( change_amt.hl(), self.proto.coin, token_bal.hl(), self.proto.dcoin ) + + class Completed(Base,EthereumMMGenTX.Completed): + fmt_keys = ('from','token_to','amt','nonce') + + def format_view_body(self,*args,**kwargs): + return 'Token: {d} {c}\n{r}'.format( + d=self.txobj['token_addr'].hl(), + c=blue('(' + self.proto.dcoin + ')'), + r=super().format_view_body(*args,**kwargs)) + + class Unsigned(Completed,EthereumMMGenTX.Unsigned): + desc = 'unsigned transaction' + + def parse_txfile_hex_data(self): + d = EthereumMMGenTX.Unsigned.parse_txfile_hex_data(self) + o = self.txobj + o['token_addr'] = TokenAddr(self.proto,d['token_addr']) + o['decimals'] = Int(d['decimals']) + t = Token(self.proto,o['token_addr'],o['decimals']) + o['data'] = t.create_data(o['to'],o['amt']) + o['token_to'] = t.transferdata2sendaddr(o['data']) + + async def do_sign(self,wif,tx_num_str): + o = self.txobj + t = Token(self.proto,o['token_addr'],o['decimals']) + tx_in = t.make_tx_in(o['from'],o['to'],o['amt'],self.start_gas,o['gasPrice'],nonce=o['nonce']) + (self.hex,self.coin_txid) = await t.txsign(tx_in,wif,o['from'],chain_id=o['chainId']) + assert self.check_sigs(),'Signature check failed' + + class Signed(Completed,EthereumMMGenTX.Signed): + desc = 'signed transaction' + + def parse_txfile_hex_data(self): + d = EthereumMMGenTX.Signed.parse_txfile_hex_data(self) + o = self.txobj + assert self.tw.token == o['to'] + o['token_addr'] = TokenAddr(self.proto,o['to']) + o['decimals'] = self.tw.decimals + t = Token(self.proto,o['token_addr'],o['decimals']) + o['amt'] = t.transferdata2amt(o['data']) + o['token_to'] = t.transferdata2sendaddr(o['data']) + + class Bump(EthereumMMGenTX.Bump,Completed,New): pass - - async def make_txobj(self): # called by create_raw() - await super().make_txobj() - t = Token(self.tw.token,self.tw.decimals) - o = self.txobj - o['token_addr'] = t.addr - o['decimals'] = t.decimals - o['token_to'] = o['to'] - o['data'] = t.create_data(o['token_to'],o['amt']) - - def parse_txfile_hex_data(self): - d = EthereumMMGenTX.parse_txfile_hex_data(self) - o = self.txobj - assert self.tw.token == o['to'] - o['token_addr'] = TokenAddr(o['to']) - o['decimals'] = self.tw.decimals - t = Token(o['token_addr'],o['decimals']) - o['amt'] = t.transferdata2amt(o['data']) - o['token_to'] = type(t).transferdata2sendaddr(o['data']) - - def format_view_body(self,*args,**kwargs): - return 'Token: {d} {c}\n{r}'.format( - d=self.txobj['token_addr'].hl(), - c=blue('(' + g.dcoin + ')'), - r=super().format_view_body(*args,**kwargs)) - -class EthereumTokenMMGenTxForSigning(EthereumTokenMMGenTX,EthereumMMGenTxForSigning): - - def resolve_g_token_from_txfile(self): - d = json.loads(self.hex) - if g.token.upper() == self.dcoin: - g.token = d['token_addr'] - elif g.token != d['token_addr']: - die(1, - "{!r}: invalid --token parameter for {t} {} token transaction file\nPlease use '--token={t}'".format( - g.token, - g.proto.name, - t = self.dcoin )) - - def parse_txfile_hex_data(self): - d = EthereumMMGenTxForSigning.parse_txfile_hex_data(self) - o = self.txobj - o['token_addr'] = TokenAddr(d['token_addr']) - o['decimals'] = Int(d['decimals']) - t = Token(o['token_addr'],o['decimals']) - o['data'] = t.create_data(o['to'],o['amt']) - o['token_to'] = type(t).transferdata2sendaddr(o['data']) - - async def do_sign(self,wif,tx_num_str): - o = self.txobj - t = Token(o['token_addr'],o['decimals']) - tx_in = t.make_tx_in(o['from'],o['to'],o['amt'],self.start_gas,o['gasPrice'],nonce=o['nonce']) - (self.hex,self.coin_txid) = await t.txsign(tx_in,wif,o['from'],chain_id=o['chainId']) - assert self.check_sigs(),'Signature check failed' - -class EthereumMMGenBumpTX(MMGenBumpTX,EthereumMMGenTxForSigning): - - @property - def min_fee(self): - return ETHAmt(self.fee * Decimal('1.101')) - - def update_fee(self,foo,fee): - self.fee = fee - - async def get_nonce(self): - return self.txobj['nonce'] - -class EthereumTokenMMGenBumpTX(EthereumMMGenBumpTX,EthereumTokenMMGenTxForSigning): - pass - -class EthereumMMGenSplitTX(MMGenSplitTX): - pass diff --git a/mmgen/common.py b/mmgen/common.py index cf0f7b72..6eed5947 100755 --- a/mmgen/common.py +++ b/mmgen/common.py @@ -26,217 +26,3 @@ from .globalvars import * import mmgen.opts as opts from .opts import opt from .util import * - -def help_notes(k): - from .obj import SubSeedIdxRange,SeedShareIdx,SeedShareCount,MasterShareIdx - from .wallet import Wallet - from .tx import MMGenTX - def fee_spec_letters(use_quotes=False): - cu = g.proto.coin_amt.units - sep,conj = ((',',' or '),("','","' or '"))[use_quotes] - return sep.join(u[0] for u in cu[:-1]) + ('',conj)[len(cu)>1] + cu[-1][0] - def fee_spec_names(): - cu = g.proto.coin_amt.units - return ', '.join(cu[:-1]) + ('',' and ')[len(cu)>1] + cu[-1] + ('',',\nrespectively')[len(cu)>1] - return { - 'rel_fee_desc': MMGenTX().rel_fee_desc, - 'fee_spec_letters': fee_spec_letters(), - 'seedsplit': """ -COMMAND NOTES: - -This command generates shares one at a time. Shares may be output to any -MMGen wallet format, with one limitation: only one share in a given split may -be in hidden incognito format, and it must be the master share in the case of -a master-share split. - -If the command's optional first argument is omitted, the default wallet is -used for the split. - -The last argument is a seed split specifier consisting of an optional split -ID, a share index, and a share count, all separated by colons. The split ID -must be a valid UTF-8 string. If omitted, the ID 'default' is used. The -share index (the index of the share being generated) must be in the range -{sia}-{sib} and the share count (the total number of shares in the split) -in the range {sca}-{scb}. - -Master Shares - -Each seed has a total of {msb} master shares, which can be used as the first -shares in multiple splits if desired. To generate a master share, use the ---master-share (-M) option with an index in the range {msa}-{msb} and omit -the last argument. - -When creating and joining a split using a master share, ensure that the same -master share index is used in all split and join commands. - -EXAMPLES: - - Split a BIP39 seed phrase into two BIP39 shares. Rejoin the split: - - $ echo 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong' > sample.bip39 - - $ mmgen-seedsplit -o bip39 sample.bip39 1:2 - BIP39 mnemonic data written to file '03BAE887-default-1of2[D51CB683][128].bip39' - - $ mmgen-seedsplit -o bip39 sample.bip39 2:2 - BIP39 mnemonic data written to file '03BAE887-default-2of2[67BFD36E][128].bip39' - - $ mmgen-seedjoin -o bip39 \\ - '03BAE887-default-2of2[67BFD36E][128].bip39' \\ - '03BAE887-default-1of2[D51CB683][128].bip39' - BIP39 mnemonic data written to file '03BAE887[128].bip39' - - $ cat '03BAE887[128].bip39' - zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong - - Create a 3-way default split of your default wallet, outputting all shares - to default wallet format. Rejoin the split: - - $ mmgen-seedsplit 1:3 # Step A - $ mmgen-seedsplit 2:3 # Step B - $ mmgen-seedsplit 3:3 # Step C - $ mmgen-seedjoin - - Create a 2-way split of your default wallet with ID string 'alice', - outputting shares to MMGen native mnemonic format. Rejoin the split: - - $ mmgen-seedsplit -o words alice:1:2 # Step D - $ mmgen-seedsplit -o words alice:2:2 # Step E - $ mmgen-seedjoin - - Create a 2-way split of your default wallet with ID string 'bob' using - master share #7, outputting share #1 (the master share) to default wallet - format and share #2 to BIP39 format. Rejoin the split: - - $ mmgen-seedsplit -M7 # Step X - $ mmgen-seedsplit -M7 -o bip39 bob:2:2 # Step Y - $ mmgen-seedjoin -M7 --id-str=bob - - Create a 2-way split of your default wallet with ID string 'alice' using - master share #7. Rejoin the split using master share #7 generated in the - previous example: - - $ mmgen-seedsplit -M7 -o bip39 alice:2:2 # Step Z - $ mmgen-seedjoin -M7 --id-str=alice - - Create a 2-way default split of your default wallet with an incognito-format - master share hidden in file 'my.hincog' at offset 1325. Rejoin the split: - - $ mmgen-seedsplit -M4 -o hincog -J my.hincog,1325 1:2 # Step M (share A) - $ mmgen-seedsplit -M4 -o bip39 2:2 # Step N (share B) - $ mmgen-seedjoin -M4 -H my.hincog,1325 - -""".strip().format( - sia=SeedShareIdx.min_val,sib=SeedShareIdx.max_val, - sca=SeedShareCount.min_val,scb=SeedShareCount.max_val, - msa=MasterShareIdx.min_val,msb=MasterShareIdx.max_val), - 'subwallet': """ -SUBWALLETS: - -Subwallets (subseeds) are specified by a "Subseed Index" consisting of: - - a) an integer in the range 1-{}, plus - b) an optional single letter, 'L' or 'S' - -The letter designates the length of the subseed. If omitted, 'L' is assumed. - -Long ('L') subseeds are the same length as their parent wallet's seed -(typically 256 bits), while short ('S') subseeds are always 128-bit. -The long and short subseeds for a given index are derived independently, -so both may be used. - -MMGen has no notion of "depth", and to an outside observer subwallets are -identical to ordinary wallets. This is a feature rather than a bug, as it -denies an attacker any way of knowing whether a given wallet has a parent. - -Since subwallets are just wallets, they may be used to generate other -subwallets, leading to hierarchies of arbitrary depth. However, this is -inadvisable in practice for two reasons: Firstly, it creates accounting -complexity, requiring the user to independently keep track of a derivation -tree. More importantly, however, it leads to the danger of Seed ID -collisions between subseeds at different levels of the hierarchy, as -MMGen checks and avoids ID collisions only among sibling subseeds. - -An exception to this caveat would be a multi-user setup where sibling -subwallets are distributed to different users as their default wallets. -Since the subseeds derived from these subwallets are private to each user, -Seed ID collisions among them doesn't present a problem. - -A safe rule of thumb, therefore, is for *each user* to derive all of his/her -subwallets from a single parent. This leaves each user with a total of two -million subwallets, which should be enough for most practical purposes. -""".strip().format(SubSeedIdxRange.max_idx), - 'passwd': """ -PASSPHRASE NOTE: - -For passphrases all combinations of whitespace are equal, and leading and -trailing space are ignored. This permits reading passphrase or brainwallet -data from a multi-line file with free spacing and indentation. -""".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(), - 'txcreate': """ -The transaction's outputs are specified on the command line, while its inputs -are chosen from a list of the user's unspent 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 network fee -estimation for the default (or user-specified) number of confirmations. -If network fee estimation fails, the user will be prompted for a fee. - -Network-estimated 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.avg_bdi} 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,pnu=g.proto.name), - 'fee': """ -FEE SPECIFICATION: Transaction fees, both on the command line and at the -interactive prompt, may be specified as either absolute {c} amounts, using -a plain decimal number, or as {r}, using an integer followed by -'{l}', for {u}. -""".format( c=g.coin, - r=MMGenTX().rel_fee_desc, - l=fee_spec_letters(use_quotes=True), - u=fee_spec_names() ), - '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. -""".format( dn=g.proto.daemon_name, - pnm=g.proj_name, - pnu=g.proto.name, - pnl=g.proj_name.lower()) - }[k] + ('-α' if g.debug_utf8 else '') - -def exit_if_mswin(feature): - if g.platform == 'win': - m = capfirst(feature) + ' not supported on the MSWin / MSYS2 platform' - ydie(1,m) diff --git a/mmgen/daemon.py b/mmgen/daemon.py index 8c96a0bb..6c71c6ea 100755 --- a/mmgen/daemon.py +++ b/mmgen/daemon.py @@ -306,20 +306,20 @@ class CoinDaemon(Daemon): 'etc': cd('Ethereum Classic','Ethereum','parity', 'parity', 'parity.conf', None, 8545, 8545,8545) } - def __new__(cls,network_id,test_suite=False,flags=None): + def __new__(cls,network_id=None,test_suite=False,flags=None,proto=None): - network_id = network_id.lower() - assert network_id in cls.network_ids, '{!r}: invalid network ID'.format(network_id) + assert network_id or proto, 'CoinDaemon_chk1' + assert not (network_id and proto), 'CoinDaemon_chk2' - if network_id.endswith('_rt'): - network = 'regtest' - daemon_id = network_id[:-3] - elif network_id.endswith('_tn'): - network = 'testnet' - daemon_id = network_id[:-3] + if proto: + network_id = proto.network_id + network = proto.network + daemon_id = proto.coin.lower() else: - network = 'mainnet' - daemon_id = network_id + network_id = network_id.lower() + assert network_id in cls.network_ids, '{!r}: invalid network ID'.format(network_id) + from mmgen.protocol import CoinProtocol + daemon_id,network = CoinProtocol.Base.parse_network_id(network_id) me = Daemon.__new__(globals()[cls.daemon_ids[daemon_id].cls_pfx+'Daemon']) me.network_id = network_id @@ -336,22 +336,30 @@ class CoinDaemon(Daemon): 'regtest', daemon_id ) else: - me.datadir = os.path.join(g.data_dir_root,'regtest',daemon_id) + datadir = os.path.join(g.data_dir_root,'regtest',daemon_id) elif test_suite: me.desc = 'test suite daemon' rel_datadir = os.path.join('test','daemons',daemon_id) else: - from .protocol import init_proto - me.datadir = init_proto(daemon_id,False).daemon_data_dir + if proto: + datadir = proto.daemon_data_dir + else: + from .protocol import init_proto + datadir = init_proto(coin=daemon_id,testnet=False).daemon_data_dir if test_suite: - me.datadir = os.path.abspath(os.path.join(os.getcwd(),rel_datadir)) + datadir = os.path.join(os.getcwd(),rel_datadir) + + if g.daemon_data_dir: # user-set value must override + datadir = g.daemon_data_dir + + me.datadir = os.path.abspath(datadir) me.port_shift = 1237 if test_suite else 0 me.platform = g.platform return me - def __init__(self,network_id,test_suite=False,flags=None): + def __init__(self,network_id=None,test_suite=False,flags=None,proto=None): super().__init__() self.testnet_arg = [] @@ -386,6 +394,9 @@ class CoinDaemon(Daemon): 'regtest': self.dfl_rpc_rt, }[self.network] + self.port_shift + if g.rpc_port: # user-set value must override + self.rpc_port = g.rpc_port + self.net_desc = '{} {}'.format(self.coin,self.network) self.subclass_init() @@ -547,14 +558,11 @@ class EthereumDaemon(CoinDaemon): # the following code does not work async def do(): - print(g.rpc) - ret = await g.rpc.call('eth_chainId') - print(ret) + ret = await self.rpc.call('eth_chainId') return ('stopped','ready')[ret == '0x11'] - from mmgen.protocol import init_proto try: - return run_session(do(),proto=init_proto('eth')) # socket exception is not propagated + return run_session(do()) # socket exception is not propagated except:# SocketError: return 'stopped' diff --git a/mmgen/exception.py b/mmgen/exception.py index a6777542..0d7d14cf 100755 --- a/mmgen/exception.py +++ b/mmgen/exception.py @@ -47,9 +47,11 @@ class TransactionChainMismatch(Exception):mmcode = 2 # 3: yellow hl, 'MMGen Error' + exception + message class RPCFailure(Exception): mmcode = 3 +class RPCChainMismatch(Exception): mmcode = 3 class BadTxSizeEstimate(Exception): mmcode = 3 class MaxInputSizeExceeded(Exception): mmcode = 3 class MaxFileSizeExceeded(Exception): mmcode = 3 +class MaxFeeExceeded(Exception): mmcode = 3 class WalletFileError(Exception): mmcode = 3 class HexadecimalStringError(Exception): mmcode = 3 class SeedLengthError(Exception): mmcode = 3 diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index 1f857663..acfdd4fb 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -72,6 +72,7 @@ class GlobalContext: # Constant vars - some of these might be overridden in opts.py, but they don't change thereafter + coin = '' token = '' debug = False debug_opts = False @@ -86,9 +87,6 @@ class GlobalContext: accept_defaults = False use_internal_keccak_module = False - chain = None - chains = ('mainnet','testnet','regtest') - # rpc: rpc_host = '' rpc_port = 0 @@ -98,7 +96,6 @@ class GlobalContext: monero_wallet_rpc_user = 'monero' monero_wallet_rpc_password = '' rpc_fail_on_command = '' - rpc = None # global RPC handle aiohttp_rpc_queue_len = 16 use_cached_balances = False @@ -280,12 +277,4 @@ class GlobalContext: if name[:11] == 'MMGEN_DEBUG': os.environ[name] = '1' - @property - def coin(self): - return self.proto.coin - - @property - def dcoin(self): - return self.proto.dcoin - g = GlobalContext() diff --git a/mmgen/help.py b/mmgen/help.py new file mode 100755 index 00000000..6317541a --- /dev/null +++ b/mmgen/help.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2020 The MMGen Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +help.py: help notes for MMGen suite commands +""" + +def help_notes_func(proto,k): + from .globalvars import g + + def fee_spec_letters(use_quotes=False): + cu = proto.coin_amt.units + sep,conj = ((',',' or '),("','","' or '"))[use_quotes] + return sep.join(u[0] for u in cu[:-1]) + ('',conj)[len(cu)>1] + cu[-1][0] + + def fee_spec_names(): + cu = proto.coin_amt.units + return ', '.join(cu[:-1]) + ('',' and ')[len(cu)>1] + cu[-1] + ('',',\nrespectively')[len(cu)>1] + + class help_notes: + + def rel_fee_desc(): + from .tx import MMGenTX + return MMGenTX.Base().rel_fee_desc + + def fee_spec_letters(): + return fee_spec_letters() + + def fee(): + from .tx import MMGenTX + return """ +FEE SPECIFICATION: Transaction fees, both on the command line and at the +interactive prompt, may be specified as either absolute {c} amounts, using +a plain decimal number, or as {r}, using an integer followed by +'{l}', for {u}. +""".format( + c = proto.coin, + r = MMGenTX.Base().rel_fee_desc, + l = fee_spec_letters(use_quotes=True), + u = fee_spec_names() ) + + def passwd(): + return """ +PASSPHRASE NOTE: + +For passphrases all combinations of whitespace are equal, and leading and +trailing space are ignored. This permits reading passphrase or brainwallet +data from a multi-line file with free spacing and indentation. +""".strip() + + def brainwallet(): + return """ +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() + + def txcreate(): + return f""" +The transaction's outputs are specified on the command line, while its inputs +are chosen from a list of the user's unspent 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 network fee +estimation for the default (or user-specified) number of confirmations. +If network fee estimation fails, the user will be prompted for a fee. + +Network-estimated 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 {proto.avg_bdi} seconds. + +All addresses on the command line can be either {proto.name} addresses or {g.proj_name} +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. +""" + + def txsign(): + return """ +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. +""".format( + dn = proto.daemon_name, + pnm = g.proj_name, + pnu = proto.name, + pnl = g.proj_name.lower() ) + + def seedsplit(): + from .obj import SeedShareIdx,SeedShareCount,MasterShareIdx + return """ +COMMAND NOTES: + +This command generates shares one at a time. Shares may be output to any +MMGen wallet format, with one limitation: only one share in a given split may +be in hidden incognito format, and it must be the master share in the case of +a master-share split. + +If the command's optional first argument is omitted, the default wallet is +used for the split. + +The last argument is a seed split specifier consisting of an optional split +ID, a share index, and a share count, all separated by colons. The split ID +must be a valid UTF-8 string. If omitted, the ID 'default' is used. The +share index (the index of the share being generated) must be in the range +{si.min_val}-{si.max_val} and the share count (the total number of shares in the split) +in the range {sc.min_val}-{sc.max_val}. + +Master Shares + +Each seed has a total of {mi.max_val} master shares, which can be used as the first +shares in multiple splits if desired. To generate a master share, use the +--master-share (-M) option with an index in the range {mi.min_val}-{mi.max_val} and omit +the last argument. + +When creating and joining a split using a master share, ensure that the same +master share index is used in all split and join commands. + +EXAMPLES: + + Split a BIP39 seed phrase into two BIP39 shares. Rejoin the split: + + $ echo 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong' > sample.bip39 + + $ mmgen-seedsplit -o bip39 sample.bip39 1:2 + BIP39 mnemonic data written to file '03BAE887-default-1of2[D51CB683][128].bip39' + + $ mmgen-seedsplit -o bip39 sample.bip39 2:2 + BIP39 mnemonic data written to file '03BAE887-default-2of2[67BFD36E][128].bip39' + + $ mmgen-seedjoin -o bip39 \\ + '03BAE887-default-2of2[67BFD36E][128].bip39' \\ + '03BAE887-default-1of2[D51CB683][128].bip39' + BIP39 mnemonic data written to file '03BAE887[128].bip39' + + $ cat '03BAE887[128].bip39' + zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong + + Create a 3-way default split of your default wallet, outputting all shares + to default wallet format. Rejoin the split: + + $ mmgen-seedsplit 1:3 # Step A + $ mmgen-seedsplit 2:3 # Step B + $ mmgen-seedsplit 3:3 # Step C + $ mmgen-seedjoin + + Create a 2-way split of your default wallet with ID string 'alice', + outputting shares to MMGen native mnemonic format. Rejoin the split: + + $ mmgen-seedsplit -o words alice:1:2 # Step D + $ mmgen-seedsplit -o words alice:2:2 # Step E + $ mmgen-seedjoin + + Create a 2-way split of your default wallet with ID string 'bob' using + master share #7, outputting share #1 (the master share) to default wallet + format and share #2 to BIP39 format. Rejoin the split: + + $ mmgen-seedsplit -M7 # Step X + $ mmgen-seedsplit -M7 -o bip39 bob:2:2 # Step Y + $ mmgen-seedjoin -M7 --id-str=bob + + Create a 2-way split of your default wallet with ID string 'alice' using + master share #7. Rejoin the split using master share #7 generated in the + previous example: + + $ mmgen-seedsplit -M7 -o bip39 alice:2:2 # Step Z + $ mmgen-seedjoin -M7 --id-str=alice + + Create a 2-way default split of your default wallet with an incognito-format + master share hidden in file 'my.hincog' at offset 1325. Rejoin the split: + + $ mmgen-seedsplit -M4 -o hincog -J my.hincog,1325 1:2 # Step M (share A) + $ mmgen-seedsplit -M4 -o bip39 2:2 # Step N (share B) + $ mmgen-seedjoin -M4 -H my.hincog,1325 + +""".strip().format( + si = SeedShareIdx, + sc = SeedShareCount, + mi = MasterShareIdx ) + + def subwallet(): + from .obj import SubSeedIdxRange + return f""" +SUBWALLETS: + +Subwallets (subseeds) are specified by a "Subseed Index" consisting of: + + a) an integer in the range 1-{SubSeedIdxRange.max_idx}, plus + b) an optional single letter, 'L' or 'S' + +The letter designates the length of the subseed. If omitted, 'L' is assumed. + +Long ('L') subseeds are the same length as their parent wallet's seed +(typically 256 bits), while short ('S') subseeds are always 128-bit. +The long and short subseeds for a given index are derived independently, +so both may be used. + +MMGen has no notion of "depth", and to an outside observer subwallets are +identical to ordinary wallets. This is a feature rather than a bug, as it +denies an attacker any way of knowing whether a given wallet has a parent. + +Since subwallets are just wallets, they may be used to generate other +subwallets, leading to hierarchies of arbitrary depth. However, this is +inadvisable in practice for two reasons: Firstly, it creates accounting +complexity, requiring the user to independently keep track of a derivation +tree. More importantly, however, it leads to the danger of Seed ID +collisions between subseeds at different levels of the hierarchy, as +MMGen checks and avoids ID collisions only among sibling subseeds. + +An exception to this caveat would be a multi-user setup where sibling +subwallets are distributed to different users as their default wallets. +Since the subseeds derived from these subwallets are private to each user, +Seed ID collisions among them doesn't present a problem. + +A safe rule of thumb, therefore, is for *each user* to derive all of his/her +subwallets from a single parent. This leaves each user with a total of two +million subwallets, which should be enough for most practical purposes. +""".strip() + + return getattr(help_notes,k)() + ('-α' if g.debug_utf8 else '') diff --git a/mmgen/main_addrgen.py b/mmgen/main_addrgen.py index bf683666..afa9120b 100755 --- a/mmgen/main_addrgen.py +++ b/mmgen/main_addrgen.py @@ -23,16 +23,18 @@ mmgen-addrgen: Generate a series or range of addresses from an MMGen from .common import * from .crypto import * -from .addr import * +from .addr import AddrList,KeyAddrList,KeyList,MMGenAddrType,AddrIdxList from .wallet import Wallet if g.prog_name == 'mmgen-keygen': gen_what = 'keys' + gen_clsname = 'KeyAddrList' gen_desc = 'secret keys' opt_filter = None note_addrkey = 'By default, both addresses and secret keys are generated.\n\n' else: gen_what = 'addresses' + gen_clsname = 'AddrList' gen_desc = 'addresses' opt_filter = 'hbcdeEiHOkKlpzPqrStUv-' note_addrkey = '' @@ -102,16 +104,16 @@ FMT CODES: """ }, 'code': { - 'options': lambda s: s.format( + 'options': lambda proto,s: s.format( seed_lens=', '.join(map(str,g.seed_lens)), - dmat="'{}' or '{}'".format(g.proto.dfl_mmtype,MMGenAddrType.mmtypes[g.proto.dfl_mmtype].name), + dmat="'{}' or '{}'".format(proto.dfl_mmtype,MMGenAddrType.mmtypes[proto.dfl_mmtype].name), kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]), kg=g.key_generator, pnm=g.proj_name, what=gen_what, g=g, ), - 'notes': lambda s: s.format( + 'notes': lambda help_notes,s: s.format( n_addrkey=note_addrkey, n_sw=help_notes('subwallet')+'\n\n', n_pw=help_notes('passwd')+'\n\n', @@ -126,7 +128,14 @@ FMT CODES: cmd_args = opts.init(opts_data,add_opts=['b16'],opt_filter=opt_filter) errmsg = "'{}': invalid parameter for --type option".format(opt.type) -addr_type = MMGenAddrType(opt.type or g.proto.dfl_mmtype,errmsg=errmsg) + +from .protocol import init_proto_from_opts +proto = init_proto_from_opts() + +addr_type = MMGenAddrType( + proto = proto, + id_str = opt.type or proto.dfl_mmtype, + errmsg = errmsg ) if len(cmd_args) < 1: opts.usage() @@ -143,8 +152,15 @@ ss = Wallet(sf) ss_seed = ss.seed if opt.subwallet is None else ss.seed.subseed(opt.subwallet,print_msg=True) -i = (gen_what=='addresses') or bool(opt.no_addresses)*2 -al = (KeyAddrList,AddrList,KeyList)[i](seed=ss_seed,addr_idxs=idxs,mmtype=addr_type) +if opt.no_addresses: + gen_clsname = 'KeyList' + +al = globals()[gen_clsname]( + proto = proto, + seed = ss_seed, + addr_idxs = idxs, + mmtype = addr_type ) + al.format() if al.gen_addrs and opt.print_checksum: diff --git a/mmgen/main_addrimport.py b/mmgen/main_addrimport.py index 0ebd4eed..808032a9 100755 --- a/mmgen/main_addrimport.py +++ b/mmgen/main_addrimport.py @@ -24,7 +24,7 @@ import time from .common import * from .addr import AddrList,KeyAddrList -from .obj import TwLabel,is_coin_addr +from .obj import TwLabel ai_msgs = lambda k: { 'rescan': """ @@ -61,6 +61,7 @@ opts_data = { -q, --quiet Suppress warnings -r, --rescan Rescan the blockchain. Required if address to import is in the blockchain and has a balance. Rescanning is slow. +-t, --token-addr=A Import addresses for ERC20 token with address 'A' """, 'notes': """\n This command can also be used to update the comment fields of addresses @@ -71,13 +72,12 @@ The --batch and --rescan options cannot be used together. } } -def parse_cmd_args(cmd_args): +def parse_cmd_args(rpc,cmd_args): def import_mmgen_list(infile): - al = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile) + al = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](proto,infile) if al.al_id.mmtype in ('S','B'): - from .tx import segwit_is_active - if not segwit_is_active(): + if not rpc.info('segwit_is_active'): rdie(2,'Segwit is not active on this chain. Cannot import Segwit addresses') return al @@ -85,14 +85,14 @@ def parse_cmd_args(cmd_args): infile = cmd_args[0] check_infile(infile) if opt.addrlist: - al = AddrList(addrlist=get_lines_from_file( - infile, - 'non-{pnm} addresses'.format(pnm=g.proj_name), - trim_comments=True)) + al = AddrList( + proto = proto, + 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]) + al = AddrList(proto=proto,addrlist=[opt.address]) infile = 'command line' else: die(1,ai_msgs('bad_args')) @@ -145,17 +145,28 @@ def make_args_list(tw,al,batch,rescan): label = '{}:{}'.format(al.al_id,e.idx) + (' ' + e.label if e.label else '') add_msg = label else: - label = '{}:{}'.format(g.proto.base_coin.lower(),e.addr) + label = '{}:{}'.format(proto.base_coin.lower(),e.addr) add_msg = 'non-'+g.proj_name if batch: - yield (e.addr,TwLabel(label),False) + yield (e.addr,TwLabel(proto,label),False) else: msg_args = ( f'{num}/{al.num_addrs}:', e.addr, '('+add_msg+')' ) - yield (tw,e.addr,TwLabel(label),rescan,fs,msg_args) + yield (tw,e.addr,TwLabel(proto,label),rescan,fs,msg_args) async def main(): - al,infile = parse_cmd_args(cmd_args) + from .tw import TrackingWallet + if opt.token_addr: + proto.tokensym = 'foo' # hack to trigger 'Token' in altcoin_subclass() + tw = await TrackingWallet( + proto = proto, + token_addr = opt.token_addr, + mode = 'i' ) + + from .rpc import rpc_init + tw.rpc = await rpc_init(proto) + + al,infile = parse_cmd_args(tw.rpc,cmd_args) qmsg( f'OK. {al.num_addrs} addresses' @@ -165,17 +176,8 @@ async def main(): f'Importing {len(al.data)} address{suf(al.data,"es")} from {infile}' + (' (batch mode)' if opt.batch else '') ) - if not al.data[0].addr.is_for_chain(g.chain): - die(2,f'Address{(" list","")[bool(opt.address)]} incompatible with {g.chain} chain!') - - from .tw import TrackingWallet - tw = await TrackingWallet(mode='i') - batch,rescan = check_opts(tw) - if g.token: - await tw.import_token() - args_list = make_args_list(tw,al,batch,rescan) if batch: @@ -192,5 +194,7 @@ async def main(): del tw cmd_args = opts.init(opts_data) +from .protocol import init_proto_from_opts +proto = init_proto_from_opts() import asyncio run_session(main()) diff --git a/mmgen/main_autosign.py b/mmgen/main_autosign.py index 32681470..21c774f2 100755 --- a/mmgen/main_autosign.py +++ b/mmgen/main_autosign.py @@ -134,17 +134,13 @@ async def check_daemons_running(): coins = ['BTC'] for coin in coins: - g.proto = init_proto(coin,g.proto.testnet) - if g.proto.sign_mode == 'daemon': - if g.test_suite: - g.proto.daemon_data_dir = 'test/daemons/' + coin.lower() - g.rpc_port = CoinDaemon(get_network_id(coin,g.proto.testnet),test_suite=True).rpc_port + proto = init_proto(coin,testnet=g.testnet) + if proto.sign_mode == 'daemon': vmsg(f'Checking {coin} daemon') try: - await rpc_init() - except SystemExit as e: - if e.code != 0: - ydie(1,f'{coin} daemon not running or not listening on port {g.proto.rpc_port}') + await rpc_init(proto) + except SocketError as e: + ydie(1,f'{coin} daemon not running or not listening on port {proto.rpc_port}') def get_wallet_files(): try: @@ -175,45 +171,23 @@ def do_umount(): msg(f'Unmounting {mountpoint}') run(['umount',mountpoint],check=True) -async def sign_tx_file(txfile,signed_txs): +async def sign_tx_file(txfile): + from .tx import MMGenTX try: - g.proto = init_proto('BTC',testnet=False) - tmp_tx = mmgen.tx.MMGenTX(txfile,metadata_only=True) - g.proto = init_proto(tmp_tx.coin) - - if tmp_tx.chain != 'mainnet': - if tmp_tx.chain == 'testnet' or ( - hasattr(g.proto,'chain_name') and tmp_tx.chain != g.proto.chain_name): - g.proto = init_proto(tmp_tx.coin,testnet=True) - - if hasattr(g.proto,'chain_name'): - if tmp_tx.chain != g.proto.chain_name: - die(2, f'Chains do not match! tx file: {tmp_tx.chain}, proto: {g.proto.chain_name}') - - g.chain = tmp_tx.chain - g.token = tmp_tx.dcoin - g.proto.dcoin = tmp_tx.dcoin or g.proto.coin - - tx = mmgen.tx.MMGenTxForSigning(txfile) - - if g.proto.sign_mode == 'daemon': - if g.test_suite: - g.proto.daemon_data_dir = 'test/daemons/' + g.coin.lower() - g.rpc_port = CoinDaemon(get_network_id(g.coin,g.proto.testnet),test_suite=True).rpc_port - await rpc_init() - - if await txsign(tx,wfs,None,None): - tx.write_to_file(ask_write=False) - signed_txs.append(tx) - return True + tx1 = MMGenTX.Unsigned(filename=txfile) + if tx1.proto.sign_mode == 'daemon': + tx1.rpc = await rpc_init(tx1.proto) + tx2 = await txsign(tx1,wfs,None,None) + if tx2: + tx2.write_to_file(ask_write=False) + return tx2 else: return False except Exception as e: - msg(f'An error occurred: {e.args[0]}') - if g.debug or g.traceback: - print_stack_trace(f'AUTOSIGN {txfile}') + ymsg(f'An error occurred with transaction {txfile!r}:\n {e!s}') return False except: + ymsg(f'An error occurred with transaction {txfile!r}') return False async def sign(): @@ -224,8 +198,10 @@ async def sign(): if unsigned: signed_txs,fails = [],[] for txfile in unsigned: - ret = await sign_tx_file(txfile,signed_txs) - if not ret: + ret = await sign_tx_file(txfile) + if ret: + signed_txs.append(ret) + else: fails.append(txfile) qmsg('') time.sleep(0.3) @@ -265,7 +241,6 @@ def print_summary(signed_txs): bmsg('\nAutosign summary:\n') def gen(): for tx in signed_txs: - g.proto = init_proto(tx.coin,testnet=tx.chain=='testnet') yield tx.format_view(terse=True) msg_r(''.join(gen())) return @@ -444,4 +419,4 @@ async def main(): elif cmd_args[0] == 'wait': await do_loop() -run_session(main(),do_rpc_init=False) +run_session(main()) diff --git a/mmgen/main_passgen.py b/mmgen/main_passgen.py index 940df3be..0428b95e 100755 --- a/mmgen/main_passgen.py +++ b/mmgen/main_passgen.py @@ -120,7 +120,7 @@ FMT CODES: dpf=PasswordList.dfl_pw_fmt, kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]) ), - 'notes': lambda s: s.format( + 'notes': lambda help_notes,s: s.format( o=opts,g=g,i58=pwi['b58'],i32=pwi['b32'],i39=pwi['bip39'], ml=MMGenPWIDString.max_len, fs="', '".join(MMGenPWIDString.forbidden), @@ -147,7 +147,11 @@ sf = get_seed_file(cmd_args,1) pw_fmt = opt.passwd_fmt or PasswordList.dfl_pw_fmt pw_len = pwi[pw_fmt].dfl_len // 2 if opt.passwd_len in ('h','H') else opt.passwd_len +from .protocol import init_proto +proto = init_proto('btc') # TODO: get rid of dummy proto + PasswordList( + proto = proto, pw_id_str = pw_id_str, pw_len = pw_len, pw_fmt = pw_fmt, @@ -158,6 +162,7 @@ do_license_msg() ss = Wallet(sf) al = PasswordList( + proto = proto, seed = ss.seed, pw_idxs = pw_idxs, pw_id_str = pw_id_str, diff --git a/mmgen/main_seedjoin.py b/mmgen/main_seedjoin.py index 7a606ac6..1070093f 100755 --- a/mmgen/main_seedjoin.py +++ b/mmgen/main_seedjoin.py @@ -82,7 +82,7 @@ FMT CODES: ms_max=MasterShareIdx.max_val, g=g, ), - 'notes': lambda s: s.format( + 'notes': lambda help_notes,s: s.format( f='\n '.join(Wallet.format_fmt_codes().splitlines()), n_pw=help_notes('passwd'), ) diff --git a/mmgen/main_split.py b/mmgen/main_split.py index ec6259bc..a60ded14 100755 --- a/mmgen/main_split.py +++ b/mmgen/main_split.py @@ -80,19 +80,23 @@ transaction reconfirmed before the timelock expires. Use at your own risk. """.format(pnm=g.proj_name) }, 'code': { - 'options': lambda s: s.format( - oc=g.proto.forks[-1][2].upper(), + 'options': lambda proto,s: s.format( + oc=proto.forks[-1][2].upper(), bh='current block height'), } } cmd_args = opts.init(opts_data,add_opts=['tx_fee','tx_fee_adj','comment_file']) +from .protocol import init_proto_from_opts +proto = init_proto_from_opts() + die(1,'This command is disabled') -opt.other_coin = opt.other_coin.upper() if opt.other_coin else g.proto.forks[-1][2].upper() -if opt.other_coin.lower() not in [e[2] for e in g.proto.forks if e[3] == True]: - die(1,"'{}': not a replayable fork of {} chain".format(opt.other_coin,g.coin)) +# the following code is broken: +opt.other_coin = opt.other_coin.upper() if opt.other_coin else proto.forks[-1][2].upper() +if opt.other_coin.lower() not in [e[2] for e in proto.forks if e[3] == True]: + die(1,"'{}': not a replayable fork of {} chain".format(opt.other_coin,proto.coin)) if len(cmd_args) != 2: fs = 'This command requires exactly two {} addresses as arguments' @@ -111,8 +115,8 @@ from .tx import MMGenSplitTX from .protocol import init_proto if opt.tx_fees: - for idx,g_coin in ((1,opt.other_coin),(0,g.coin)): - g.proto = init_proto(g_coin) + for idx,g_coin in ((1,opt.other_coin),(0,proto.coin)): + proto = init_proto(g_coin) opt.tx_fee = opt.tx_fees.split(',')[idx] opts.opt_is_tx_fee('foo',opt.tx_fee,'transaction fee') # raises exception on error @@ -120,8 +124,11 @@ tx1 = MMGenSplitTX() opt.no_blank = True async def main(): - gmsg("Creating timelocked transaction for long chain ({})".format(g.coin)) - locktime = int(opt.locktime or 0) or await g.rpc.call('getblockcount') + gmsg("Creating timelocked transaction for long chain ({})".format(proto.coin)) + locktime = int(opt.locktime) + if not locktime: + rpc = rpc_init(proto) + locktime = rpc.call('getblockcount') tx1.create(mmids[0],locktime) tx1.format() @@ -129,7 +136,7 @@ async def main(): gmsg("\nCreating transaction for short chain ({})".format(opt.other_coin)) - g.proto = init_proto(opt.other_coin) + proto = init_proto(opt.other_coin) tx2 = MMGenSplitTX() tx2.inputs = tx1.inputs diff --git a/mmgen/main_txbump.py b/mmgen/main_txbump.py index 72bd0abf..fdc28b13 100755 --- a/mmgen/main_txbump.py +++ b/mmgen/main_txbump.py @@ -81,7 +81,7 @@ column below: """ }, 'code': { - 'options': lambda s: s.format( + 'options': lambda help_notes,proto,s: s.format( g=g, pnm=g.proj_name, pnl=g.proj_name.lower(), @@ -89,8 +89,8 @@ column below: fu=help_notes('rel_fee_desc'),fl=help_notes('fee_spec_letters'), kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]), kg=g.key_generator, - cu=g.coin), - 'notes': lambda s: s.format( + cu=proto.coin), + 'notes': lambda help_notes,s: s.format( help_notes('fee'), help_notes('txsign'), f='\n '.join(Wallet.format_fmt_codes().splitlines())) @@ -107,41 +107,56 @@ from .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) - -sign_and_send = bool(seed_files or kl or kal) - do_license_msg() silent = opt.yes and opt.tx_fee != None and opt.output_to_reduce != None +ext = get_extension(tx_file) +ext_data = { + MMGenTX.Unsigned.ext: 'Unsigned', + MMGenTX.Signed.ext: 'Signed', +} +if ext not in ext_data: + die(1,f'{ext!r}: unrecognized file extension') + async def main(): - from .tw import TrackingWallet - tx = MMGenBumpTX(filename=tx_file,send=sign_and_send,tw=await TrackingWallet() if g.token else None) + orig_tx = getattr(MMGenTX,ext_data[ext])(filename=tx_file) if not silent: msg(green('ORIGINAL TRANSACTION')) - msg(tx.format_view(terse=True)) + msg(orig_tx.format_view(terse=True)) + + kal = get_keyaddrlist(orig_tx.proto,opt) + kl = get_keylist(orig_tx.proto,opt) + sign_and_send = bool(seed_files or kl or kal) + + from .tw import TrackingWallet + tx = MMGenTX.Bump( + data = orig_tx.__dict__, + send = sign_and_send, + tw = await TrackingWallet(orig_tx.proto) if orig_tx.proto.tokensym else None ) + + from .rpc import rpc_init + tx.rpc = await rpc_init(tx.proto) tx.check_bumpable() # needs cached networkinfo['relayfee'] - msg('Creating new transaction') + msg('Creating replacement transaction') op_idx = tx.choose_output() if not silent: - msg('Minimum fee for new transaction: {} {}'.format(tx.min_fee.hl(),g.coin)) + msg('Minimum fee for new transaction: {} {}'.format(tx.min_fee.hl(),tx.proto.coin)) fee = tx.get_usr_fee_interactive(tx_fee=opt.tx_fee,desc='User-selected') tx.update_fee(op_idx,fee) d = tx.get_fee() - assert d == fee and d <= g.proto.max_tx_fee + assert d == fee and d <= tx.proto.max_tx_fee - if g.proto.base_proto == 'Bitcoin': + if tx.proto.base_proto == 'Bitcoin': tx.outputs.sort_bip69() # output amts have changed, so re-sort if not opt.yes: @@ -159,10 +174,12 @@ async def main(): msg_r(tx.format_view(terse=True)) if sign_and_send: - if await txsign(tx,seed_files,kl,kal): - tx.write_to_file(ask_write=False) - await tx.send(exit_on_fail=True) - tx.write_to_file(ask_write=False) + tx2 = MMGenTX.Unsigned(data=tx.__dict__) + tx3 = await txsign(tx2,seed_files,kl,kal) + if tx3: + tx3.write_to_file(ask_write=False) + await tx3.send(exit_on_fail=True) + tx3.write_to_file(ask_write=False) else: die(2,'Transaction could not be signed') else: diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index ee36c141..2addeb63 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -61,14 +61,14 @@ opts_data = { 'notes': '\n{}{}', }, 'code': { - 'options': lambda s: s.format( + 'options': lambda proto,help_notes,s: s.format( fu=help_notes('rel_fee_desc'), fl=help_notes('fee_spec_letters'), fe_all=fmt_list(g.autoset_opts['fee_estimate_mode'].choices,fmt='no_spc'), fe_dfl=g.autoset_opts['fee_estimate_mode'].choices[0], - cu=g.coin, + cu=proto.coin, g=g), - 'notes': lambda s: s.format( + 'notes': lambda help_notes,s: s.format( help_notes('txcreate'), help_notes('fee')) } @@ -79,10 +79,27 @@ cmd_args = opts.init(opts_data) g.use_cached_balances = opt.cached_balances async def main(): + + from .protocol import init_proto_from_opts + proto = init_proto_from_opts() + from .tx import MMGenTX from .tw import TrackingWallet - tx = MMGenTX(tw=await TrackingWallet() if g.token else None) - await tx.create(cmd_args,int(opt.locktime or 0),do_info=opt.info) - tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False) + tx1 = MMGenTX.New( + proto = proto, + tw = await TrackingWallet(proto) if proto.tokensym else None ) + + from .rpc import rpc_init + tx1.rpc = await rpc_init(proto) + + tx2 = await tx1.create( + cmd_args = cmd_args, + locktime = int(opt.locktime or 0), + do_info = opt.info ) + + tx2.write_to_file( + ask_write = not opt.yes, + ask_overwrite = not opt.yes, + ask_write_default_yes = False ) run_session(main()) diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index 3e166c69..e636b869 100755 --- a/mmgen/main_txdo.py +++ b/mmgen/main_txdo.py @@ -93,7 +93,7 @@ column below: """ }, 'code': { - 'options': lambda s: s.format( + 'options': lambda proto,help_notes,s: s.format( g=g,pnm=g.proj_name,pnl=g.proj_name.lower(), kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]), fu=help_notes('rel_fee_desc'), @@ -103,8 +103,8 @@ column below: fe_all=fmt_list(g.autoset_opts['fee_estimate_mode'].choices,fmt='no_spc'), fe_dfl=g.autoset_opts['fee_estimate_mode'].choices[0], kg=g.key_generator, - cu=g.coin), - 'notes': lambda s: s.format( + cu=proto.coin), + 'notes': lambda help_notes,s: s.format( help_notes('txcreate'), help_notes('fee'), help_notes('txsign'), @@ -121,24 +121,36 @@ from .txsign import * seed_files = get_seed_files(opt,cmd_args) -kal = get_keyaddrlist(opt) -kl = get_keylist(opt) -if kl and kal: - kl.remove_dup_keys(kal) - async def main(): from .tw import TrackingWallet - tx1 = MMGenTX(caller='txdo',tw=await TrackingWallet() if g.token else None) - await tx1.create(cmd_args,int(opt.locktime or 0)) + from .protocol import init_proto_from_opts + proto = init_proto_from_opts() - tx2 = MMGenTxForSigning(data=tx1.__dict__) + tx1 = MMGenTX.New( + proto = proto, + tw = await TrackingWallet(proto) if proto.tokensym else None ) - if await txsign(tx2,seed_files,kl,kal): - tx2.write_to_file(ask_write=False) - await tx2.send(exit_on_fail=True) - tx2.write_to_file(ask_overwrite=False,ask_write=False) - tx2.print_contract_addr() + from .rpc import rpc_init + tx1.rpc = await rpc_init(proto) + + tx2 = await tx1.create( + cmd_args = cmd_args, + locktime = int(opt.locktime or 0), + caller = 'txdo' ) + + kal = get_keyaddrlist(proto,opt) + kl = get_keylist(proto,opt) + if kl and kal: + kl.remove_dup_keys(kal) + + tx3 = await txsign(tx2,seed_files,kl,kal) + + if tx3: + tx3.write_to_file(ask_write=False) + await tx3.send(exit_on_fail=True) + tx3.write_to_file(ask_overwrite=False,ask_write=False) + tx3.print_contract_addr() else: die(2,'Transaction could not be signed') diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 7ecb7931..e510f009 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -41,37 +41,36 @@ opts_data = { cmd_args = opts.init(opts_data) if len(cmd_args) == 1: - infile = cmd_args[0]; check_infile(infile) + infile = cmd_args[0] + check_infile(infile) else: opts.usage() if not opt.status: do_license_msg() -from .tx import * - async def main(): - from .tw import TrackingWallet - tx = MMGenTX(infile,quiet_open=True,tw=await TrackingWallet() if g.token else None) + from .tx import MMGenTX - if g.token: - from .tw import TrackingWallet - tx.tw = await TrackingWallet() + tx = MMGenTX.Signed( + filename = infile, + quiet_open = True, + tw = await MMGenTX.Signed.get_tracking_wallet(infile) ) - vmsg("Signed transaction file '{}' is valid".format(infile)) + from .rpc import rpc_init + tx.rpc = await rpc_init(tx.proto) - if not tx.marked_signed(): - die(1,'Transaction is not signed!') + vmsg(f'Signed transaction file {infile!r} is valid') if opt.status: if tx.coin_txid: - qmsg('{} txid: {}'.format(g.coin,tx.coin_txid.hl())) + qmsg(f'{tx.proto.coin} txid: {tx.coin_txid.hl()}') await tx.get_status(status=True) sys.exit(0) if not opt.yes: - tx.view_with_prompt('View transaction data?') + tx.view_with_prompt('View transaction details?') if tx.add_comment(): # edits an existing comment, returns true if changed tx.write_to_file(ask_write_default_yes=True) diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index 98e2887d..f394646d 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -77,17 +77,17 @@ column below: """ }, 'code': { - 'options': lambda s: s.format( + 'options': lambda proto,s: s.format( g=g, pnm=g.proj_name, pnl=g.proj_name.lower(), - dn=g.proto.daemon_name, + dn=proto.daemon_name, kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]), kg=g.key_generator, ss=g.subseeds, ss_max=SubSeedIdxRange.max_idx, - cu=g.coin), - 'notes': lambda s: s.format( + cu=proto.coin), + 'notes': lambda help_notes,s: s.format( help_notes('txsign'), f='\n '.join(Wallet.format_fmt_codes().splitlines())) } @@ -108,43 +108,47 @@ from .txsign import * tx_files = get_tx_files(opt,infiles) seed_files = get_seed_files(opt,infiles) -kal = get_keyaddrlist(opt) -kl = get_keylist(opt) - -if kl and kal: - kl.remove_dup_keys(kal) async def main(): + bad_tx_count = 0 tx_num_disp = '' + for tx_num,tx_file in enumerate(tx_files,1): + if len(tx_files) > 1: - msg('\nTransaction #{} of {}:'.format(tx_num,len(tx_files))) tx_num_disp = f' #{tx_num}' + msg(f'\nTransaction{tx_num_disp} of {len(tx_files)}:') - tx = MMGenTxForSigning(tx_file) - - if tx.marked_signed(): - msg('Transaction is already signed!') - continue + tx1 = MMGenTX.Unsigned(filename=tx_file) vmsg(f'Successfully opened transaction file {tx_file!r}') + if tx1.proto.sign_mode == 'daemon': + from .rpc import rpc_init + tx1.rpc = await rpc_init(tx1.proto) + if opt.tx_id: - msg(tx.txid) + msg(tx1.txid) continue if opt.info or opt.terse_info: - tx.view(pause=False,terse=opt.terse_info) + tx1.view(pause=False,terse=opt.terse_info) continue if not opt.yes: - tx.view_with_prompt(f'View data for transaction{tx_num_disp}?') + tx1.view_with_prompt(f'View data for transaction{tx_num_disp}?') - if await txsign(tx,seed_files,kl,kal,tx_num_disp): + kal = get_keyaddrlist(tx1.proto,opt) + kl = get_keylist(tx1.proto,opt) + if kl and kal: + kl.remove_dup_keys(kal) + + tx2 = await txsign(tx1,seed_files,kl,kal,tx_num_disp) + if tx2: if not opt.yes: - tx.add_comment() # edits an existing comment - tx.write_to_file(ask_write=not opt.yes,ask_write_default_yes=True,add_desc=tx_num_disp) + tx2.add_comment() # edits an existing comment + tx2.write_to_file(ask_write=not opt.yes,ask_write_default_yes=True,add_desc=tx_num_disp) else: ymsg('Transaction could not be signed') bad_tx_count += 1 @@ -152,7 +156,4 @@ async def main(): if bad_tx_count: ydie(2,f'{bad_tx_count} transaction{suf(bad_tx_count)} could not be signed') -run_session( - main(), - do_rpc_init = g.proto.sign_mode == 'daemon' -) +run_session(main()) diff --git a/mmgen/main_wallet.py b/mmgen/main_wallet.py index cd56675d..a9488d4c 100755 --- a/mmgen/main_wallet.py +++ b/mmgen/main_wallet.py @@ -132,7 +132,7 @@ FMT CODES: ms_max=MasterShareIdx.max_val, g=g, ), - 'notes': lambda s: s.format( + 'notes': lambda help_notes,s: s.format( f='\n '.join(Wallet.format_fmt_codes().splitlines()), n_ss=('',help_notes('seedsplit')+'\n\n')[do_ss_note], n_sw=('',help_notes('subwallet')+'\n\n')[do_sw_note], diff --git a/mmgen/obj.py b/mmgen/obj.py index 8bcc95f3..5366a5d1 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -36,14 +36,13 @@ class aInitMeta(type): def is_mmgen_seed_id(s): return SeedID(sid=s,on_fail='silent') def is_mmgen_idx(s): return AddrIdx(s,on_fail='silent') -def is_mmgen_id(s): return MMGenID(s,on_fail='silent') -def is_coin_addr(s): return CoinAddr(s,on_fail='silent') def is_addrlist_id(s): return AddrListID(s,on_fail='silent') -def is_tw_label(s): return TwLabel(s,on_fail='silent') -def is_wif(s): return WifKey(s,on_fail='silent') -def is_viewkey(s): return ViewKey(s,on_fail='silent') def is_seed_split_specifier(s): return SeedSplitSpecifier(s,on_fail='silent') +def is_mmgen_id(proto,s): return MMGenID(proto,s,on_fail='silent') +def is_coin_addr(proto,s): return CoinAddr(proto,s,on_fail='silent') +def is_wif(proto,s): return WifKey(proto,s,on_fail='silent') + def truncate_str(s,width): # width = screen width wide_count = 0 for i in range(len(s)): @@ -87,7 +86,7 @@ class IndexedDict(dict): class MMGenList(list,MMGenObject): pass class MMGenDict(dict,MMGenObject): pass -class AddrListList(list,MMGenObject): pass +class AddrListData(list,MMGenObject): pass class InitErrors(object): @@ -230,16 +229,21 @@ class ImmutableAttr: # Descriptor """ ok_dtypes = (str,type,type(None),type(lambda:0)) - def __init__(self,dtype,typeconv=True,set_none_ok=False): + def __init__(self,dtype,typeconv=True,set_none_ok=False,include_proto=False): assert isinstance(dtype,self.ok_dtypes), 'ImmutableAttr_check1' + if include_proto: assert typeconv and type(dtype) == str, 'ImmutableAttr_check2' if set_none_ok: assert typeconv and type(dtype) != str, 'ImmutableAttr_check3' - if type(dtype).__name__ == 'function': - self.conv = lambda instance,value: dtype(value) + if dtype is None: + 'use instance-defined conversion function for this attribute' + self.conv = lambda instance,value: getattr(instance.conv_funcs,self.name)(instance,value) elif typeconv: "convert this attribute's type" if type(dtype) == str: - self.conv = lambda instance,value: globals()[dtype](value,on_fail='raise') + if include_proto: + self.conv = lambda instance,value: globals()[dtype](instance.proto,value,on_fail='raise') + else: + self.conv = lambda instance,value: globals()[dtype](value,on_fail='raise') else: if set_none_ok: self.conv = lambda instance,value: None if value is None else dtype(value) @@ -280,10 +284,10 @@ class ListItemAttr(ImmutableAttr): For attributes that might not be present in the data instance Reassignment or deletion allowed if specified """ - def __init__(self,dtype,typeconv=True,reassign_ok=False,delete_ok=False): + def __init__(self,dtype,typeconv=True,include_proto=False,reassign_ok=False,delete_ok=False): self.reassign_ok = reassign_ok self.delete_ok = delete_ok - ImmutableAttr.__init__(self,dtype,typeconv=typeconv) + ImmutableAttr.__init__(self,dtype,typeconv=typeconv,include_proto=include_proto) def __get__(self,instance,owner): "return None if attribute doesn't exist" @@ -301,8 +305,7 @@ class ListItemAttr(ImmutableAttr): ImmutableAttr.__delete__(self,instance) class MMGenListItem(MMGenObject): - - valid_attrs = None + valid_attrs = set() valid_attrs_extra = set() invalid_attrs = { 'pfmt', @@ -312,15 +315,20 @@ class MMGenListItem(MMGenObject): 'valid_attrs_extra', 'invalid_attrs', 'immutable_attr_init_check', + 'conv_funcs', + '_asdict', } def __init__(self,*args,**kwargs): - if self.valid_attrs == None: - type(self).valid_attrs = ( - ( {e for e in dir(self) if e[:2] != '__'} | self.valid_attrs_extra ) - self.invalid_attrs ) + # generate valid_attrs, or use the class valid_attrs if set + self.__dict__['valid_attrs'] = self.valid_attrs or ( + ( {e for e in dir(self) if e[:2] != '__'} | self.valid_attrs_extra ) + - MMGenListItem.invalid_attrs + - self.invalid_attrs + ) if args: - raise ValueError('Non-keyword args not allowed in {!r} constructor'.format(type(self).__name__)) + raise ValueError(f'Non-keyword args not allowed in {type(self).__name__!r} constructor') for k,v in kwargs.items(): if v != None: @@ -332,10 +340,12 @@ class MMGenListItem(MMGenObject): # allow only valid attributes to be set def __setattr__(self,name,value): if name not in self.valid_attrs: - m = "'{}': no such attribute in class {}" - raise AttributeError(m.format(name,type(self))) + raise AttributeError(f'{name!r}: no such attribute in class {type(self)}') return object.__setattr__(self,name,value) + def _asdict(self): + return dict((k,v) for k,v in self.__dict__.items() if k in self.valid_attrs) + class MMGenIdx(Int): min_val = 1 class SeedShareIdx(MMGenIdx): max_val = 1024 class SeedShareCount(SeedShareIdx): min_val = 2 @@ -526,49 +536,38 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject): hex_width = 40 width = 1 trunc_ok = False - def __new__(cls,s,on_fail='die'): - if type(s) == cls: return s + def __new__(cls,proto,addr,on_fail='die'): + if type(addr) == cls: + return addr cls.arg_chk(on_fail) try: - assert set(s) <= set(ascii_letters+digits),'contains non-alphanumeric characters' - me = str.__new__(cls,s) - ap = g.proto.parse_addr(s) - assert ap,'coin address {!r} could not be parsed'.format(s) + assert set(addr) <= set(ascii_letters+digits),'contains non-alphanumeric characters' + me = str.__new__(cls,addr) + ap = proto.parse_addr(addr) + assert ap, f'coin address {addr!r} could not be parsed' me.addr_fmt = ap.fmt me.hex = ap.bytes.hex() + me.proto = proto return me except Exception as e: - return cls.init_fail(e,s,objname=f'{g.proto.cls_name} address') + return cls.init_fail(e,addr,objname=f'{proto.cls_name} address') @classmethod - def fmtc(cls,s,**kwargs): + def fmtc(cls,addr,**kwargs): w = kwargs['width'] or cls.width - return super().fmtc(s[:w-2]+'..' if w < len(s) else s, **kwargs) - - def is_for_chain(self,chain): - - if g.proto.name.startswith('Ethereum'): - return True - - from mmgen.protocol import init_proto - proto = init_proto(g.coin,network=chain) - - if self.addr_fmt == 'bech32': - return self[:len(proto.bech32_hrp)] == proto.bech32_hrp - else: - return bool(proto.parse_addr(self)) + return super().fmtc(addr[:w-2]+'..' if w < len(addr) else addr, **kwargs) class TokenAddr(CoinAddr): color = 'blue' class ViewKey(object): - def __new__(cls,s,on_fail='die'): - if g.proto.name == 'Zcash': - return ZcashViewKey.__new__(ZcashViewKey,s,on_fail) - elif g.proto.name == 'Monero': - return MoneroViewKey.__new__(MoneroViewKey,s,on_fail) + def __new__(cls,proto,viewkey,on_fail='die'): + if proto.name == 'Zcash': + return ZcashViewKey.__new__(ZcashViewKey,proto,viewkey,on_fail) + elif proto.name == 'Monero': + return MoneroViewKey.__new__(MoneroViewKey,viewkey,on_fail) else: - raise ValueError(f'{g.proto.name}: protocol does not support view keys') + raise ValueError(f'{proto.name}: protocol does not support view keys') class ZcashViewKey(CoinAddr): hex_width = 128 @@ -620,39 +619,40 @@ class MMGenID(str,Hilite,InitErrors,MMGenObject): color = 'orange' width = 0 trunc_ok = False - def __new__(cls,s,on_fail='die'): + def __new__(cls,proto,id_str,on_fail='die'): cls.arg_chk(on_fail) try: - ss = str(s).split(':') + ss = str(id_str).split(':') assert len(ss) in (2,3),'not 2 or 3 colon-separated items' - t = MMGenAddrType((ss[1],g.proto.dfl_mmtype)[len(ss)==2],on_fail='raise') + t = proto.addr_type((ss[1],proto.dfl_mmtype)[len(ss)==2],on_fail='raise') me = str.__new__(cls,'{}:{}:{}'.format(ss[0],t,ss[-1])) me.sid = SeedID(sid=ss[0],on_fail='raise') me.idx = AddrIdx(ss[-1],on_fail='raise') me.mmtype = t - assert t in g.proto.mmtypes, f'{t}: invalid address type for {g.proto.cls_name}' + assert t in proto.mmtypes, f'{t}: invalid address type for {proto.cls_name}' me.al_id = str.__new__(AddrListID,me.sid+':'+me.mmtype) # checks already done me.sort_key = '{}:{}:{:0{w}}'.format(me.sid,me.mmtype,me.idx,w=me.idx.max_digits) + me.proto = proto return me except Exception as e: - return cls.init_fail(e,s) + return cls.init_fail(e,id_str) class TwMMGenID(str,Hilite,InitErrors,MMGenObject): color = 'orange' width = 0 trunc_ok = False - def __new__(cls,id_str,on_fail='die'): + def __new__(cls,proto,id_str,on_fail='die'): if type(id_str) == cls: return id_str cls.arg_chk(on_fail) ret = None try: - ret = MMGenID(id_str,on_fail='raise') + ret = MMGenID(proto,id_str,on_fail='raise') sort_key,idtype = ret.sort_key,'mmgen' except Exception as e: try: - assert id_str.split(':',1)[0] == g.proto.base_coin.lower(),( - "not a string beginning with the prefix '{}:'".format(g.proto.base_coin.lower())) + assert id_str.split(':',1)[0] == proto.base_coin.lower(),( + f'not a string beginning with the prefix {proto.base_coin.lower()!r}:' ) assert set(id_str[4:]) <= set(ascii_letters+digits),'contains non-alphanumeric characters' assert len(id_str) > 4,'not more that four characters long' ret,sort_key,idtype = str(id_str),'z_'+id_str,'non-mmgen' @@ -663,21 +663,23 @@ class TwMMGenID(str,Hilite,InitErrors,MMGenObject): me.obj = ret me.sort_key = sort_key me.type = idtype + me.proto = proto return me # non-displaying container for TwMMGenID,TwComment class TwLabel(str,InitErrors,MMGenObject): - def __new__(cls,text,on_fail='die'): + def __new__(cls,proto,text,on_fail='die'): if type(text) == cls: return text cls.arg_chk(on_fail) try: ts = text.split(None,1) - mmid = TwMMGenID(ts[0],on_fail='raise') + mmid = TwMMGenID(proto,ts[0],on_fail='raise') comment = TwComment(ts[1] if len(ts) == 2 else '',on_fail='raise') me = str.__new__(cls,'{}{}'.format(mmid,' {}'.format(comment) if comment else '')) me.mmid = mmid me.comment = comment + me.proto = proto return me except Exception as e: return cls.init_fail(e,text) @@ -704,7 +706,7 @@ class HexStr(str,Hilite,InitErrors): class CoinTxID(HexStr): color,width,hexcase = 'purple',64,'lower' class WalletPassword(HexStr): color,width,hexcase = 'blue',32,'lower' -class MoneroViewKey(HexStr): color,width,hexcase = 'cyan',64,'lower' +class MoneroViewKey(HexStr): color,width,hexcase = 'cyan',64,'lower' # FIXME - no checking performed class MMGenTxID(HexStr): color,width,hexcase = 'red',6,'upper' class WifKey(str,Hilite,InitErrors): @@ -714,13 +716,13 @@ class WifKey(str,Hilite,InitErrors): """ width = 53 color = 'blue' - def __new__(cls,wif,on_fail='die'): + def __new__(cls,proto,wif,on_fail='die'): if type(wif) == cls: return wif cls.arg_chk(on_fail) try: assert set(wif) <= set(ascii_letters+digits),'not an ascii alphanumeric string' - g.proto.parse_wif(wif) # raises exception on error + proto.parse_wif(wif) # raises exception on error return str.__new__(cls,wif) except Exception as e: return cls.init_fail(e,wif) @@ -751,7 +753,7 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject): wif = ImmutableAttr(WifKey,typeconv=False) # initialize with (priv_bin,compressed), WIF or self - def __new__(cls,s=None,compressed=None,wif=None,pubkey_type=None,on_fail='die'): + def __new__(cls,proto,s=None,compressed=None,wif=None,pubkey_type=None,on_fail='die'): if type(s) == cls: return s cls.arg_chk(on_fail) @@ -760,18 +762,19 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject): try: assert s == None,"'wif' and key hex args are mutually exclusive" assert set(wif) <= set(ascii_letters+digits),'not an ascii alphanumeric string' - k = g.proto.parse_wif(wif) # raises exception on error + k = proto.parse_wif(wif) # raises exception on error me = str.__new__(cls,k.sec.hex()) me.compressed = k.compressed me.pubkey_type = k.pubkey_type me.wif = str.__new__(WifKey,wif) # check has been done me.orig_hex = None - if k.sec != g.proto.preprocess_key(k.sec,k.pubkey_type): + if k.sec != proto.preprocess_key(k.sec,k.pubkey_type): raise PrivateKeyError( - f'{g.proto.cls_name} WIF key {me.wif!r} encodes private key with invalid value {me}') + f'{proto.cls_name} WIF key {me.wif!r} encodes private key with invalid value {me}') + me.proto = proto return me except Exception as e: - return cls.init_fail(e,s,objname='{} WIF key'.format(g.coin)) + return cls.init_fail(e,s,objname=f'{proto.coin} WIF key') else: try: assert s,'private key bytes data missing' @@ -782,11 +785,12 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject): else: assert compressed is not None, "'compressed' arg missing" assert type(compressed) == bool,"{!r}: 'compressed' not of type 'bool'".format(compressed) - me = str.__new__(cls,g.proto.preprocess_key(s,pubkey_type).hex()) - me.wif = WifKey(g.proto.hex2wif(me,pubkey_type,compressed),on_fail='raise') + me = str.__new__(cls,proto.preprocess_key(s,pubkey_type).hex()) + me.wif = WifKey(proto,proto.hex2wif(me,pubkey_type,compressed),on_fail='raise') me.compressed = compressed me.pubkey_type = pubkey_type me.orig_hex = s.hex() # save the non-preprocessed key + me.proto = proto return me except Exception as e: return cls.init_fail(e,s) @@ -915,20 +919,21 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject): 'Z': ati('zcash_z','zcash_z',False,'zcash_z', 'zcash_z', 'wif', ('viewkey',), 'Zcash z-address'), 'M': ati('monero', 'monero', False,'monero', 'monero', 'spendkey',('viewkey','wallet_passwd'),'Monero address'), } - def __new__(cls,id_str,on_fail='die',errmsg=None): + def __new__(cls,proto,id_str,on_fail='die',errmsg=None): if type(id_str) == cls: return id_str cls.arg_chk(on_fail) try: - for k,v in list(cls.mmtypes.items()): + for k,v in cls.mmtypes.items(): if id_str in (k,v.name): if id_str == v.name: id_str = k me = str.__new__(cls,id_str) for k in v._fields: setattr(me,k,getattr(v,k)) - if me not in g.proto.mmtypes + ('P',): - raise ValueError(f'{me.name!r}: invalid address type for {g.proto.cls_name}') + if me not in proto.mmtypes + ('P',): + raise ValueError(f'{me.name!r}: invalid address type for {proto.name} protocol') + me.proto = proto return me raise ValueError(f'{id_str}: unrecognized address type for protocol {proto.name}') except Exception as e: diff --git a/mmgen/opts.py b/mmgen/opts.py index f8c824a9..6516d2e6 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -44,18 +44,22 @@ def print_help(po,opts_data,opt_filter): if not 'code' in opts_data: opts_data['code'] = {} + from .protocol import init_proto_from_opts + proto = init_proto_from_opts() + if getattr(opt,'longhelp',None): opts_data['code']['long_options'] = common_opts_data['code'] def remove_unneeded_long_opts(): d = opts_data['text']['long_options'] if g.prog_name != 'mmgen-tool': d = '\n'.join(''+i for i in d.split('\n') if not '--monero-wallet' in i) - if g.proto.base_proto != 'Ethereum': + if proto.base_proto != 'Ethereum': d = '\n'.join(''+i for i in d.split('\n') if not '--token' in i) opts_data['text']['long_options'] = d remove_unneeded_long_opts() mmgen.share.Opts.print_help( # exits + proto, po, opts_data, opt_filter ) @@ -76,6 +80,7 @@ def _show_hash_presets(): for i in sorted(g.hash_presets.keys()): msg(fs.format(i,*g.hash_presets[i])) msg('N = memory usage (power of two), p = iterations (rounds)') + sys.exit(0) def opt_preproc_debug(po): d = ( @@ -205,10 +210,10 @@ common_opts_data = { --, --bob Switch to user "Bob" in MMGen regtest setup --, --alice Switch to user "Alice" in MMGen regtest setup """, - 'code': lambda s: s.format( + 'code': lambda proto,s: s.format( pnm = g.proj_name, - dn = g.proto.daemon_name, - cu_dfl = g.coin, + dn = proto.daemon_name, + cu_dfl = proto.coin, ) } @@ -256,14 +261,24 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False): version() # exits # === begin global var initialization === # - - # 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 - # We must set g.data_dir_root from --data-dir before processing cfg file - g.data_dir_root = ( - os.path.normpath(os.path.expanduser(opt.data_dir)) - if opt.data_dir else - os.path.join(g.home_dir,'.'+g.proj_name.lower()) ) + """ + NB: user opt --data-dir is actually data_dir_root + - data_dir is data_dir_root plus optionally 'regtest' or 'testnet', so for mainnet + data_dir == data_dir_root + - As with Bitcoin Core, cfg file is in data_dir_root, wallets and other data are + in data_dir + - Since cfg file is in data_dir_root, data_dir_root must be finalized before we + can process cfg file + - Since data_dir depends on the values of g.testnet and g.regtest, these must be + finalized before setting data_dir + """ + if opt.data_dir: + g.data_dir_root = os.path.normpath(os.path.expanduser(opt.data_dir)) + elif os.getenv('MMGEN_TEST_SUITE'): + from test.include.common import get_data_dir + g.data_dir_root = get_data_dir() + else: + g.data_dir_root = os.path.join(g.home_dir,'.'+g.proj_name.lower()) check_or_create_dir(g.data_dir_root) @@ -278,30 +293,18 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False): # Set globals from opts, setting type from original global value # Do here, before opts are set from globals below - # g.coin is finalized here for k in (g.common_opts + g.opt_sets_global): if hasattr(opt,k): val = getattr(opt,k) if val != None and hasattr(g,k): setattr(g,k,set_for_type(val,getattr(g,k),'--'+k)) - from .protocol import init_genonly_altcoins,init_proto - - altcoin_trust_level = init_genonly_altcoins( - opt.coin or 'btc', - testnet = g.testnet or g.regtest ) - - g.proto = init_proto( - opt.coin or 'btc', - testnet = g.testnet, - regtest = g.regtest ) - - # this could have been set from long opts - 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)) + """ + g.testnet and g.regtest are finalized, so we can set g.data_dir + """ + g.data_dir = os.path.normpath(os.path.join( + g.data_dir_root, + ('regtest' if g.regtest else 'testnet' if g.testnet else '') )) # Set user opts from globals: # - if opt is unset, set it to global value @@ -314,15 +317,14 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False): else: setattr(opt,k,getattr(g,k)) - if opt.show_hash_presets: + if opt.show_hash_presets: # exits _show_hash_presets() - sys.exit(0) - if opt.verbose: - opt.quiet = None + g.coin = g.coin.upper() or 'BTC' + g.token = g.token.upper() or None if g.bob or g.alice: - g.proto = init_proto(g.coin,regtest=True) + g.regtest = True g.rpc_host = 'localhost' g.data_dir = os.path.join(g.data_dir_root,'regtest',g.coin.lower(),('alice','bob')[g.bob]) from .regtest import MMGenRegtest @@ -330,14 +332,21 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False): g.rpc_password = MMGenRegtest.rpc_password g.rpc_port = MMGenRegtest(g.coin).d.rpc_port - # === end global var initialization === # + from .protocol import init_genonly_altcoins + altcoin_trust_level = init_genonly_altcoins( + g.coin, + testnet = g.testnet or g.regtest ) - die_on_incompatible_opts(g.incompatible_opts) + # === end global var initialization === # # print help screen only after global vars are initialized: if getattr(opt,'help',None) or getattr(opt,'longhelp',None): print_help(po,opts_data,opt_filter) # exits + warn_altcoins(g.coin,altcoin_trust_level) + + die_on_incompatible_opts(g.incompatible_opts) + check_or_create_dir(g.data_dir) # g.data_dir is finalized, so we can create it # Check user-set opts without modifying them @@ -346,14 +355,15 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False): # Check all opts against g.autoset_opts, setting if unset check_and_set_autoset_opts() + if opt.verbose: + opt.quiet = None + if g.debug and g.prog_name != 'test.py': opt.verbose,opt.quiet = (True,None) if g.debug_opts: opt_postproc_debug() - warn_altcoins(g.coin,altcoin_trust_level) - # We don't need this data anymore del mmgen.share.Opts for k in ('text','notes','code'): @@ -362,6 +372,7 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False): return po.cmd_args +# DISABLED def opt_is_tx_fee(key,val,desc): # 'key' must remain a placeholder # contract data or non-standard startgas: disable fee checking @@ -371,18 +382,19 @@ def opt_is_tx_fee(key,val,desc): # 'key' must remain a placeholder return from .tx import MMGenTX - tx = MMGenTX() + from .protocol import init_proto_from_opts + tx = MMGenTX.New(init_proto_from_opts()) # Size of 224 is just a ball-park figure to eliminate the most extreme cases at startup # This check will be performed again once we know the true size ret = tx.process_fee_spec(val,224) if ret == False: raise UserOptError('{!r}: invalid {}\n(not a {} amount or {} specification)'.format( - val,desc,g.coin.upper(),tx.rel_fee_desc)) + val,desc,tx.proto.coin.upper(),tx.rel_fee_desc)) - if ret > g.proto.max_tx_fee: + if ret > tx.proto.max_tx_fee: raise UserOptError('{!r}: invalid {}\n({} > max_tx_fee ({} {}))'.format( - val,desc,ret.fmt(fs='1.1'),g.proto.max_tx_fee,g.coin.upper())) + val,desc,ret.fmt(fs='1.1'),tx.proto.max_tx_fee,tx.proto.coin.upper())) def check_usr_opts(usr_opts): # Raises an exception if any check fails @@ -519,10 +531,11 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails from .protocol import CoinProtocol opt_is_in_list(val.lower(),CoinProtocol.coins,'coin') - def chk_rbf(key,val,desc): - if not g.proto.cap('rbf'): - m = '--rbf requested, but {} does not support replace-by-fee transactions' - raise UserOptError(m.format(g.coin)) +# TODO: move this check elsewhere +# def chk_rbf(key,val,desc): +# if not proto.cap('rbf'): +# m = '--rbf requested, but {} does not support replace-by-fee transactions' +# raise UserOptError(m.format(proto.coin)) def chk_bob(key,val,desc): m = "Regtest (Bob and Alice) mode not set up yet. Run '{}-regtest setup' to initialize." @@ -538,13 +551,14 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails opt_is_int(val,desc) opt_compares(int(val),'>',0,desc) - def chk_token(key,val,desc): - if not 'token' in g.proto.caps: - raise UserOptError('Coin {!r} does not support the --token option'.format(g.coin)) - if len(val) == 40 and is_hex_str(val): - return - if len(val) > 20 or not all(s.isalnum() for s in val): - raise UserOptError('{!r}: invalid parameter for --token option'.format(val)) +# TODO: move this check elsewhere +# def chk_token(key,val,desc): +# if not 'token' in proto.caps: +# raise UserOptError('Coin {!r} does not support the --token option'.format(tx.coin)) +# if len(val) == 40 and is_hex_str(val): +# return +# if len(val) > 20 or not all(s.isalnum() for s in val): +# raise UserOptError('{!r}: invalid parameter for --token option'.format(val)) cfuncs = { k:v for k,v in locals().items() if k.startswith('chk_') } diff --git a/mmgen/protocol.py b/mmgen/protocol.py index f76cc459..d9518355 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -25,7 +25,7 @@ from collections import namedtuple from .util import msg,ymsg,Msg,ydie from .devtools import * -from .obj import BTCAmt,LTCAmt,BCHAmt,B2XAmt,ETHAmt +from .obj import BTCAmt,LTCAmt,BCHAmt,B2XAmt,ETHAmt,CoinAddr,MMGenAddrType,PrivKey from .globalvars import g import mmgen.bech32 as bech32 @@ -87,14 +87,61 @@ class CoinProtocol(MMGenObject): is_fork_of = None networks = ('mainnet','testnet','regtest') - def __init__(self,coin,name,network): - self.coin = coin.upper() - self.dcoin = self.coin # display coin - for Ethereum, is set to ERC20 token name - self.name = name - self.cls_name = type(self).__name__ - self.network = network - self.testnet = network in ('testnet','regtest') - self.regtest = network == 'regtest' + def __init__(self,coin,name,network,tokensym=None): + self.coin = coin.upper() + self.name = name + self.network = network + self.tokensym = tokensym + self.cls_name = type(self).__name__ + self.testnet = network in ('testnet','regtest') + self.regtest = network == 'regtest' + self.network_id = coin.lower() + { + 'mainnet': '', + 'testnet': '_tn', + 'regtest': '_rt', + }[network] + + if not hasattr(self,'chain_name'): + self.chain_name = self.network + + if self.tokensym: + assert isinstance(self,CoinProtocol.Ethereum), 'CoinProtocol.Base_chk1' + + @property + def dcoin(self): + return self.coin + + @classmethod + def chain_name_to_network(cls,coin,chain_name): + """ + The generic networks 'mainnet', 'testnet' and 'regtest' are required for all coins + that support transaction operations. + + For protocols that have specific names for chains corresponding to these networks, + the attribute 'chain_name' is used, while 'network' retains the generic name. + For Bitcoin and Bitcoin forks, 'network' and 'chain_name' are equivalent. + """ + for network,suf in ( + ('mainnet',''), + ('testnet','Testnet'), + ('regtest','Regtest' ), + ): + name = CoinProtocol.coins[coin.lower()].name + suf + proto = getattr(CoinProtocol,name) + proto_chain_name = getattr(proto,'chain_name',None) or network + if chain_name == proto_chain_name: + return network + raise ValueError(f'{chain_name}: unrecognized chain name for coin {coin}') + + @staticmethod + def parse_network_id(network_id): + nid = namedtuple('parsed_network_id',['coin','network']) + if network_id.endswith('_tn'): + return nid(network_id[:-3],'testnet') + elif network_id.endswith('_rt'): + return nid(network_id[:-3],'regtest') + else: + return nid(network_id,'mainnet') def cap(self,s): return s in self.caps @@ -118,7 +165,19 @@ class CoinProtocol(MMGenObject): return False + def coin_addr(self,addr): + return CoinAddr(proto=self,addr=addr) + + def addr_type(self,id_str,on_fail='die'): + return MMGenAddrType(proto=self,id_str=id_str,on_fail=on_fail) + + def priv_key(self,s,on_fail='die'): + return PrivKey(proto=self,s=s,on_fail=on_fail) + class Secp256k1(Base): + """ + Bitcoin and Ethereum protocols inherit from this class + """ secp256k1_ge = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 privkey_len = 32 @@ -138,6 +197,9 @@ class CoinProtocol(MMGenObject): return (pk % self.secp256k1_ge).to_bytes(self.privkey_len,'big') class Bitcoin(Secp256k1): # chainparams.cpp + """ + All Bitcoin code and chain forks inherit from this class + """ mod_clsname = 'Bitcoin' daemon_name = 'bitcoind' daemon_family = 'bitcoind' @@ -146,7 +208,6 @@ class CoinProtocol(MMGenObject): wif_ver_num = { 'std': '80' } mmtypes = ('L','C','S','B') dfl_mmtype = 'L' - data_subdir = '' rpc_port = 8332 coin_amt = BTCAmt max_tx_fee = BTCAmt('0.003') @@ -237,7 +298,6 @@ class CoinProtocol(MMGenObject): class BitcoinTestnet(Bitcoin): addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' } wif_ver_num = { 'std': 'ef' } - data_subdir = 'testnet' daemon_data_subdir = 'testnet3' rpc_port = 18332 bech32_hrp = 'tb' @@ -268,7 +328,6 @@ class CoinProtocol(MMGenObject): rpc_port = 18442 addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' } wif_ver_num = { 'std': 'ef' } - data_subdir = 'testnet' daemon_data_subdir = 'testnet3' class BitcoinCashRegtest(BitcoinCashTestnet): @@ -289,7 +348,6 @@ class CoinProtocol(MMGenObject): class B2XTestnet(B2X): addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' } wif_ver_num = { 'std': 'ef' } - data_subdir = 'testnet' daemon_data_subdir = 'testnet5' rpc_port = 18338 @@ -313,7 +371,6 @@ class CoinProtocol(MMGenObject): # addr ver nums same as Bitcoin testnet, except for 'p2sh' addr_ver_bytes = { '6f':'p2pkh', '3a':'p2sh', 'c4':'p2sh' } wif_ver_num = { 'std': 'ef' } # same as Bitcoin testnet - data_subdir = 'testnet' daemon_data_subdir = 'testnet4' rpc_port = 19332 bech32_hrp = 'tltc' @@ -340,9 +397,10 @@ class CoinProtocol(MMGenObject): base_coin = 'ETH' pubkey_type = 'std' # required by DummyWIF - data_subdir = '' daemon_name = 'parity' daemon_family = 'parity' + daemon_data_dir = os.path.join(g.home_dir,'.local','share','io.parity.ethereum') + daemon_data_subdir = '' rpc_port = 8545 coin_amt = ETHAmt max_tx_fee = ETHAmt('0.005') @@ -353,6 +411,10 @@ class CoinProtocol(MMGenObject): base_proto = 'Ethereum' avg_bdi = 15 + @property + def dcoin(self): + return self.tokensym or self.coin + def parse_addr(self,addr): from .util import is_hex_str_lc if is_hex_str_lc(addr) and len(addr) == self.addr_len * 2: @@ -367,10 +429,12 @@ class CoinProtocol(MMGenObject): return pubkey_hash class EthereumTestnet(Ethereum): - data_subdir = 'testnet' rpc_port = 8547 # start Parity with --jsonrpc-port=8547 or --ports-shift=2 chain_name = 'kovan' + class EthereumRegtest(EthereumTestnet): + chain_name = 'developmentchain' + class EthereumClassic(Ethereum): rpc_port = 8555 # start Parity with --jsonrpc-port=8555 or --ports-shift=10 chain_name = 'ethereum_classic' # chain_id 0x3d (61) @@ -379,6 +443,9 @@ class CoinProtocol(MMGenObject): rpc_port = 8557 # start Parity with --jsonrpc-port=8557 or --ports-shift=12 chain_name = 'classic-testnet' # aka Morden, chain_id 0x3e (62) (UNTESTED) + class EthereumClassicRegtest(EthereumClassicTestnet): + chain_name = 'developmentchain' + class Zcash(Bitcoin): base_coin = 'ZEC' addr_ver_bytes = { '1cb8': 'p2pkh', '1cbd': 'p2sh', '169a': 'zcash_z', 'a8abd3': 'viewkey' } @@ -420,7 +487,6 @@ class CoinProtocol(MMGenObject): dfl_mmtype = 'M' pubkey_type = 'monero' # required by DummyWIF avg_bdi = 120 - data_subdir = '' privkey_len = 32 mmcaps = ('key','addr') @@ -455,17 +521,21 @@ class CoinProtocol(MMGenObject): class MoneroTestnet(Monero): addr_ver_bytes = { '35': 'monero', '3f': 'monero_sub' } -def init_proto(coin,testnet=False,regtest=False,network=None): +def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=None,tokensym=None): assert type(testnet) == bool, 'init_proto_chk1' assert type(regtest) == bool, 'init_proto_chk2' + assert coin or network_id, 'init_proto_chk3' + assert not (coin and network_id), 'init_proto_chk4' - if network is None: - network = 'regtest' if regtest else 'testnet' if testnet else 'mainnet' + if network_id: + coin,network = CoinProtocol.Base.parse_network_id(network_id) + elif network: + assert network in CoinProtocol.Base.networks, f'init_proto_chk5 - {network!r}: invalid network' + assert testnet == False, 'init_proto_chk6' + assert regtest == False, 'init_proto_chk7' else: - assert network in CoinProtocol.Base.networks - assert testnet == False - assert regtest == False + network = 'regtest' if regtest else 'testnet' if testnet else 'mainnet' coin = coin.lower() if coin not in CoinProtocol.coins: @@ -478,9 +548,18 @@ def init_proto(coin,testnet=False,regtest=False,network=None): proto_name = name + ('' if network == 'mainnet' else network.capitalize()) return getattr(CoinProtocol,proto_name)( - coin = coin, - name = name, - network = network ) + coin = coin, + name = name, + network = network, + tokensym = tokensym ) + +def init_proto_from_opts(): + from .opts import opt + return init_proto( + coin = g.coin, + testnet = g.testnet, + regtest = g.regtest, + tokensym = g.token ) def init_genonly_altcoins(usr_coin=None,testnet=False): """ diff --git a/mmgen/regtest.py b/mmgen/regtest.py index a2262c4a..0aadc925 100755 --- a/mmgen/regtest.py +++ b/mmgen/regtest.py @@ -23,6 +23,7 @@ regtest: Coin daemon regression test mode setup and operations for the MMGen sui import os,time,shutil,re,json from subprocess import run,PIPE from .common import * +from .protocol import init_proto from .daemon import CoinDaemon def create_data_dir(data_dir): @@ -79,6 +80,7 @@ class MMGenRegtest(MMGenObject): def __init__(self,coin): self.coin = coin.lower() + self.proto = init_proto(self.coin,regtest=True) self.test_suite = os.getenv('MMGEN_TEST_SUITE_REGTEST') self.d = CoinDaemon(self.coin+'_rt',test_suite=self.test_suite) @@ -152,12 +154,12 @@ class MMGenRegtest(MMGenObject): err = cp.stderr.decode() if err: if "couldn't connect to server" in err: - rdie(1,f'Error stopping the {g.proto.name} daemon:\n{err}') + rdie(1,f'Error stopping the {self.proto.name} daemon:\n{err}') else: msg(err) def current_user_unix(self,quiet=False): - cmd = ['pgrep','-af','{}.*--rpcport={}.*'.format(g.proto.daemon_name,self.d.rpc_port)] + cmd = ['pgrep','-af','{}.*--rpcport={}.*'.format(self.proto.daemon_name,self.d.rpc_port)] cmdout = run(cmd,stdout=PIPE).stdout.decode() if cmdout: for k in self.users: @@ -271,12 +273,11 @@ class MMGenRegtest(MMGenObject): def fork(self,coin): # currently disabled - from .protocol import init_proto - forks = init_proto(coin,False).forks - if not [f for f in forks if f[2] == g.coin.lower() and f[3] == True]: - die(1,"Coin {} is not a replayable fork of coin {}".format(g.coin,coin)) + proto = init_proto(coin,False) + if not [f for f in proto.forks if f[2] == proto.coin.lower() and f[3] == True]: + die(1,"Coin {} is not a replayable fork of coin {}".format(proto.coin,coin)) - gmsg('Creating fork from coin {} to coin {}'.format(coin,g.coin)) + gmsg('Creating fork from coin {} to coin {}'.format(coin,proto.coin)) source_rt = MMGenRegtest(coin) @@ -300,4 +301,4 @@ class MMGenRegtest(MMGenObject): self.start_daemon('miner',reindex=True) self.stop_daemon() - gmsg('Fork {} successfully created'.format(g.coin)) + gmsg('Fork {} successfully created'.format(proto.coin)) diff --git a/mmgen/rpc.py b/mmgen/rpc.py index 955a4927..9df529c3 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -228,7 +228,7 @@ class RPCClient(MMGenObject): if g.rpc_user: user,passwd = (g.rpc_user,g.rpc_password) else: - user,passwd = get_coin_daemon_cfg_options(('rpcuser','rpcpassword')).values() + user,passwd = self.get_daemon_cfg_options(('rpcuser','rpcpassword')).values() if user and passwd: self.auth = auth_data(user,passwd) @@ -318,52 +318,29 @@ class RPCClient(MMGenObject): except: m = f': {text}' raise RPCFailure(f'{s.value} {s.name}{m}') - class BitcoinRPCClient(RPCClient,metaclass=aInitMeta): auth_type = 'basic' has_auth_cookie = True - def __init__(self,*args,**kwargs): pass + def __init__(self,*args,**kwargs): + pass - async def __ainit__(self,proto,backend): - - async def check_chainfork_mismatch(block0): - try: - if block0 != self.proto.block0: - raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol') - for fork in self.proto.forks: - if fork.height == None or self.blockcount < fork.height: - break - if fork.hash != await self.call('getblockhash',fork.height): - die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?') - except Exception as e: - die(2,"{}\n'{c}' requested, but this is not the {c} chain!".format(e.args[0],c=g.coin)) - - def check_chaintype_mismatch(): - try: - if g.proto.regtest: - assert g.chain == 'regtest', '--regtest option selected, but chain is not regtest' - if g.proto.testnet: - assert g.chain != 'mainnet', '--testnet option selected, but chain is mainnet' - else: - assert g.chain == 'mainnet', 'mainnet selected, but chain is not mainnet' - except Exception as e: - die(1,'{}\nChain is {}!'.format(e.args[0],g.chain)) + async def __ainit__(self,proto,daemon,backend): self.proto = proto - user,passwd = get_coin_daemon_cfg_options(('rpcuser','rpcpassword')).values() + self.daemon_data_dir = daemon.datadir super().__init__( - host = g.rpc_host or 'localhost', - port = g.rpc_port or self.proto.rpc_port) + host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'), + port = daemon.rpc_port ) - self.set_auth() # set_auth() requires cookie, so must be called after __init__() tests socket + self.set_auth() # set_auth() requires cookie, so must be called after __init__() tests daemon is listening self.set_backend(backend) # backend requires self.auth if g.bob or g.alice: from .regtest import MMGenRegtest - MMGenRegtest(g.coin).switch_user(('alice','bob')[g.bob],quiet=True) + MMGenRegtest(self.proto.coin).switch_user(('alice','bob')[g.bob],quiet=True) self.cached = {} ( @@ -378,16 +355,27 @@ class BitcoinRPCClient(RPCClient,metaclass=aInitMeta): ('getblockhash',(0,)), )) self.daemon_version = self.cached['networkinfo']['version'] - g.chain = self.cached['blockchaininfo']['chain'] + self.chain = self.cached['blockchaininfo']['chain'] tip = await self.call('getblockhash',self.blockcount) self.cur_date = (await self.call('getblockheader',tip))['time'] - if g.chain != 'regtest': - g.chain += 'net' - assert g.chain in g.chains - check_chaintype_mismatch() + if self.chain != 'regtest': + self.chain += 'net' + assert self.chain in self.proto.networks - if g.chain == 'mainnet': # skip this for testnet, as Genesis block may change + async def check_chainfork_mismatch(block0): + try: + if block0 != self.proto.block0: + raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol') + for fork in self.proto.forks: + if fork.height == None or self.blockcount < fork.height: + break + if fork.hash != await self.call('getblockhash',fork.height): + die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?') + except Exception as e: + die(2,'{!s}\n{c!r} requested, but this is not the {c} chain!'.format(e,c=self.proto.coin)) + + if self.chain == 'mainnet': # skip this for testnet, as Genesis block may change await check_chainfork_mismatch(block0) self.caps = ('full_node',) @@ -397,16 +385,60 @@ class BitcoinRPCClient(RPCClient,metaclass=aInitMeta): if len((await self.call('help',func)).split('\n')) > 3: self.caps += (cap,) + def get_daemon_cfg_fn(self): + # Use dirname() to remove 'bob' or 'alice' component + cfg_dir = os.path.dirname(g.data_dir) if self.proto.regtest else self.daemon_data_dir + return os.path.join( + cfg_dir, + (self.proto.is_fork_of or self.proto.name).lower() + '.conf' ) + def get_daemon_auth_cookie_fn(self): - cdir = os.path.join( - self.proto.daemon_data_dir, - self.proto.daemon_data_subdir ) - return os.path.join(cdir,'.cookie') + return os.path.join( + self.daemon_data_dir, + self.proto.daemon_data_subdir, + '.cookie' ) + + def get_daemon_cfg_options(self,req_keys): + + fn = self.get_daemon_cfg_fn() + try: + lines = get_lines_from_file(fn,'',silent=not opt.verbose) + except: + vmsg(f'Warning: {fn!r} does not exist or is unreadable') + return dict((k,None) for k in req_keys) + + def gen(): + for key in req_keys: + val = None + for l in lines: + if l.startswith(key): + res = l.split('=',1) + if len(res) == 2 and not ' ' in res[1].strip(): + val = res[1].strip() + yield (key,val) + + return dict(gen()) def get_daemon_auth_cookie(self): fn = self.get_daemon_auth_cookie_fn() return get_lines_from_file(fn,'')[0] if file_is_readable(fn) else '' + def info(self,info_id): + + def segwit_is_active(): + d = self.cached['blockchaininfo'] + if d['chain'] == 'regtest': + return True + if ( 'bip9_softforks' in d + and 'segwit' in d['bip9_softforks'] + and d['bip9_softforks']['segwit']['status'] == 'active'): + return True + if g.test_suite: + return True + return False + + return locals()[info_id]() + rpcmethods = ( 'backupwallet', 'createrawtransaction', @@ -445,15 +477,16 @@ class BitcoinRPCClient(RPCClient,metaclass=aInitMeta): class EthereumRPCClient(RPCClient,metaclass=aInitMeta): - def __init__(self,*args,**kwargs): pass - - async def __ainit__(self,proto,backend): + def __init__(self,*args,**kwargs): + pass + async def __ainit__(self,proto,daemon,backend): self.proto = proto + self.daemon_data_dir = daemon.datadir super().__init__( - host = g.rpc_host or 'localhost', - port = g.rpc_port or self.proto.rpc_port ) + host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'), + port = daemon.rpc_port ) self.set_backend(backend) @@ -468,7 +501,7 @@ class EthereumRPCClient(RPCClient,metaclass=aInitMeta): self.daemon_version = vi['version'] self.cur_date = int(bh['timestamp'],16) - g.chain = ch.replace(' ','_') + self.chain = ch.replace(' ','_') self.caps = ('full_node',) if nk['capability'] == 'full' else () try: @@ -550,17 +583,25 @@ class MoneroWalletRPCClient(RPCClient): 'refresh', # start_height ) -async def rpc_init(proto=None,backend=None): - - proto = proto or g.proto - backend = backend or opt.rpc_backend +async def rpc_init(proto,backend=None): if not 'rpc' in proto.mmcaps: die(1,f'Coin daemon operations not supported for {proto.name} protocol!') - g.rpc = await { + from .daemon import CoinDaemon + rpc = await { 'bitcoind': BitcoinRPCClient, 'parity': EthereumRPCClient, - }[proto.daemon_family](proto=proto,backend=backend) + }[proto.daemon_family]( + proto = proto, + daemon = CoinDaemon(proto=proto,test_suite=g.test_suite), + backend = backend or opt.rpc_backend ) - return g.rpc + if proto.chain_name != rpc.chain: + raise RPCChainMismatch( + '{} protocol chain is {}, but coin daemon chain is {}'.format( + proto.cls_name, + proto.chain_name.upper(), + rpc.chain.upper() )) + + return rpc diff --git a/mmgen/share/Opts.py b/mmgen/share/Opts.py index 6fd8f392..884ba916 100755 --- a/mmgen/share/Opts.py +++ b/mmgen/share/Opts.py @@ -29,7 +29,9 @@ def usage(opts_data): print('USAGE: {} {}'.format(opts_data['prog_name'], opts_data['usage'])) sys.exit(2) -def print_help(po,opts_data,opt_filter): +def print_help(proto,po,opts_data,opt_filter): + + from mmgen.util import pdie # DEBUG def parse_lines(text): filtered = False @@ -47,21 +49,40 @@ def print_help(po,opts_data,opt_filter): c = opts_data['code'] nl = '\n ' - text = nl.join(parse_lines(t[opts_type])) - pn = opts_data['prog_name'] - out = ( - ' {:<{p}} {}'.format(pn.upper()+':',t['desc'].strip(),p=len(pn)+1) - + nl + '{:<{p}} {} {}'.format('USAGE:',pn,t['usage'].strip(),p=len(pn)+1) - + nl + opts_type.upper().replace('_',' ') + ':' - + nl + (c[opts_type](text) if opts_type in c else text) - ) - if opts_type == 'options' and 'notes' in t: - ntext = c['notes'](t['notes']) if 'notes' in c else t['notes'] - out += nl + nl.join(ntext.rstrip().splitlines()) + from mmgen.help import help_notes_func + def help_notes(k): + return help_notes_func(proto,k) - print(out) + def gen_arg_tuple(func,text): + d = {'proto': proto,'help_notes':help_notes} + for arg in func.__code__.co_varnames: + yield d[arg] if arg in d else text + + def gen_text(): + yield ' {:<{p}} {}'.format(pn.upper()+':',t['desc'].strip(),p=len(pn)+1) + yield '{:<{p}} {} {}'.format('USAGE:',pn,t['usage'].strip(),p=len(pn)+1) + yield opts_type.upper().replace('_',' ') + ':' + + # process code for options + opts_text = nl.join(parse_lines(t[opts_type])) + if opts_type in c: + arg_tuple = tuple(gen_arg_tuple(c[opts_type],opts_text)) + yield c[opts_type](*arg_tuple) + else: + yield opts_text + + # process code for notes + if opts_type == 'options' and 'notes' in t: + notes_text = t['notes'] + if 'notes' in c: + arg_tuple = tuple(gen_arg_tuple(c['notes'],notes_text)) + notes_text = c['notes'](*arg_tuple) + for line in notes_text.splitlines(): + yield line + + print(nl.join(gen_text())) sys.exit(0) def process_uopts(opts_data,short_opts,long_opts): diff --git a/mmgen/tool.py b/mmgen/tool.py index fbbb70b2..c6b0260c 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -217,13 +217,6 @@ def _process_result(ret,pager=False,print_result=False): from .obj import MMGenAddrType -def init_generators(arg=None): - global at,kg,ag - at = MMGenAddrType((hasattr(opt,'type') and opt.type) or g.proto.dfl_mmtype) - if arg != 'at': - kg = KeyGenerator(at) - ag = AddrGenerator(at) - def conv_cls_bip39(): from .bip39 import bip39 return bip39 @@ -274,7 +267,22 @@ class MMGenToolCmdMeta(type): def user_commands(cls): return {k:v for k,v in cls.__dict__.items() if k in cls.methods} -class MMGenToolCmds(metaclass=MMGenToolCmdMeta): pass +class MMGenToolCmds(metaclass=MMGenToolCmdMeta): + + def __init__(self,proto=None): + from .protocol import init_proto_from_opts + self.proto = proto or init_proto_from_opts() + if g.token: + self.proto.tokensym = g.token.upper() + + def init_generators(self,arg=None): + global at,kg,ag + at = MMGenAddrType( + proto = self.proto, + id_str = getattr(opt,'type',None) or self.proto.dfl_mmtype ) + if arg != 'at': + kg = KeyGenerator(self.proto,at) + ag = AddrGenerator(self.proto,at) class MMGenToolCmdMisc(MMGenToolCmds): "miscellaneous commands" @@ -408,16 +416,18 @@ class MMGenToolCmdCoin(MMGenToolCmds): """ def randwif(self): "generate a random private key in WIF format" - init_generators('at') + self.init_generators('at') return PrivKey( + self.proto, get_random(32), pubkey_type = at.pubkey_type, compressed = at.compressed ).wif def randpair(self): "generate a random private key/address pair" - init_generators() + self.init_generators() privhex = PrivKey( + self.proto, get_random(32), pubkey_type = at.pubkey_type, compressed = at.compressed ) @@ -427,20 +437,23 @@ class MMGenToolCmdCoin(MMGenToolCmds): def wif2hex(self,wifkey:'sstr'): "convert a private key from WIF to hex format" return PrivKey( + self.proto, wif = wifkey ) def hex2wif(self,privhex:'sstr'): "convert a private key from hex to WIF format" - init_generators('at') + self.init_generators('at') return PrivKey( + self.proto, bytes.fromhex(privhex), pubkey_type = at.pubkey_type, compressed = at.compressed ).wif def wif2addr(self,wifkey:'sstr'): "generate a coin address from a key in WIF format" - init_generators() + self.init_generators() privhex = PrivKey( + self.proto, wif = wifkey ) addr = ag.to_addr(kg.to_pubhex(privhex)) return addr @@ -448,16 +461,18 @@ class MMGenToolCmdCoin(MMGenToolCmds): def wif2redeem_script(self,wifkey:'sstr'): # new "convert a WIF private key to a Segwit P2SH-P2WPKH redeem script" assert opt.type == 'segwit','This command is meaningful only for --type=segwit' - init_generators() + self.init_generators() privhex = PrivKey( + self.proto, wif = wifkey ) return ag.to_segwit_redeem_script(kg.to_pubhex(privhex)) def wif2segwit_pair(self,wifkey:'sstr'): "generate both a Segwit P2SH-P2WPKH redeem script and address from WIF" assert opt.type == 'segwit','This command is meaningful only for --type=segwit' - init_generators() + self.init_generators() pubhex = kg.to_pubhex(PrivKey( + self.proto, wif = wifkey )) addr = ag.to_addr(pubhex) rs = ag.to_segwit_redeem_script(pubhex) @@ -465,8 +480,9 @@ class MMGenToolCmdCoin(MMGenToolCmds): def privhex2addr(self,privhex:'sstr',output_pubhex=False): "generate coin address from raw private key data in hexadecimal format" - init_generators() + self.init_generators() pk = PrivKey( + self.proto, bytes.fromhex(privhex), compressed = at.compressed, pubkey_type = at.pubkey_type ) @@ -480,14 +496,14 @@ class MMGenToolCmdCoin(MMGenToolCmds): def pubhex2addr(self,pubkeyhex:'sstr'): "convert a hex pubkey to an address" if opt.type == 'segwit': - return g.proto.pubhex2segwitaddr(pubkeyhex) + return self.proto.pubhex2segwitaddr(pubkeyhex) else: return self.pubhash2addr(hash160(pubkeyhex)) def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new "convert a hex pubkey to a Segwit P2SH-P2WPKH redeem script" assert opt.type == 'segwit','This command is meaningful only for --type=segwit' - return g.proto.pubhex2redeem_script(pubkeyhex) + return self.proto.pubhex2redeem_script(pubkeyhex) def redeem_script2addr(self,redeem_scripthex:'sstr'): # new "convert a Segwit P2SH-P2WPKH redeem script to an address" @@ -499,25 +515,25 @@ class MMGenToolCmdCoin(MMGenToolCmds): def pubhash2addr(self,pubhashhex:'sstr'): "convert public key hash to address" if opt.type == 'bech32': - return g.proto.pubhash2bech32addr(pubhashhex) + return self.proto.pubhash2bech32addr(pubhashhex) else: - init_generators('at') - return g.proto.pubhash2addr(pubhashhex,at.addr_fmt=='p2sh') + self.init_generators('at') + return self.proto.pubhash2addr(pubhashhex,at.addr_fmt=='p2sh') def addr2pubhash(self,addr:'sstr'): "convert coin address to public key hash" from .tx import addr2pubhash - return addr2pubhash(CoinAddr(addr)) + return addr2pubhash(self.proto,CoinAddr(self.proto,addr)) def addr2scriptpubkey(self,addr:'sstr'): "convert coin address to scriptPubKey" from .tx import addr2scriptPubKey - return addr2scriptPubKey(CoinAddr(addr)) + return addr2scriptPubKey(self.proto,CoinAddr(self.proto,addr)) def scriptpubkey2addr(self,hexstr:'sstr'): "convert scriptPubKey to coin address" from .tx import scriptPubKey2addr - return scriptPubKey2addr(hexstr)[0] + return scriptPubKey2addr(self.proto,hexstr)[0] class MMGenToolCmdMnemonic(MMGenToolCmds): """ @@ -623,13 +639,13 @@ class MMGenToolCmdFile(MMGenToolCmds): opt.yes = True opt.quiet = True from .addr import AddrList,KeyAddrList,PasswordList - ret = locals()[objname](mmgen_addrfile) + ret = locals()[objname](self.proto,mmgen_addrfile) if opt.verbose: if ret.al_id.mmtype.name == 'password': fs = 'Passwd fmt: {}\nPasswd len: {}\nID string: {}' msg(fs.format(capfirst(ret.pw_info[ret.pw_fmt].desc),ret.pw_len,ret.pw_id_str)) else: - msg('Base coin: {} {}'.format(ret.base_coin,('Mainnet','Testnet')[ret.is_testnet])) + msg(f'Base coin: {ret.base_coin} {capfirst(ret.network)}') msg('MMType: {}'.format(capfirst(ret.al_id.mmtype.name))) msg('List length: {}'.format(len(ret.data))) return ret.chksum @@ -646,7 +662,7 @@ class MMGenToolCmdFile(MMGenToolCmds): "compute checksum for MMGen password file" return self._file_chksum(mmgen_passwdfile,'PasswordList') - def txview( varargs_call_sig = { # hack to allow for multiple filenames + async def txview( varargs_call_sig = { # hack to allow for multiple filenames 'args': ( 'mmgen_tx_file(s)', 'pager', @@ -667,15 +683,23 @@ class MMGenToolCmdFile(MMGenToolCmds): file_sort = kwargs.get('filesort') or 'mtime' from .filename import MMGenFileList - from .tx import MMGenTX,MMGenTxForSigning + from .tx import MMGenTX flist = MMGenFileList(infiles,ftype=MMGenTX) flist.sort_by_age(key=file_sort) # in-place sort - def gen(): - for fn in flist.names(): - yield (MMGenTxForSigning,MMGenTX)[fn.endswith('.sigtx')](fn).format_view(terse=terse,sort=tx_sort) + async def process_file(fn): + if fn.endswith(MMGenTX.Signed.ext): + tx = MMGenTX.Signed( + filename = fn, + quiet_open = True, + tw = await MMGenTX.Signed.get_tracking_wallet(fn) ) + else: + tx = MMGenTX.Unsigned( + filename = fn, + quiet_open = True ) + return tx.format_view(terse=terse,sort=tx_sort) - return ('—'*77+'\n').join(gen()).rstrip() + return ('—'*77+'\n').join([await process_file(fn) for fn in flist.names()]).rstrip() class MMGenToolCmdFileCrypt(MMGenToolCmds): """ @@ -841,7 +865,7 @@ class MMGenToolCmdWallet(MMGenToolCmds): def gen_addr(self,mmgen_addr:str,wallet='',target='addr'): "generate a single MMGen address from default or specified wallet" - addr = MMGenID(mmgen_addr) + addr = MMGenID(self.proto,mmgen_addr) opt.quiet = True sf = get_seed_file([wallet] if wallet else [],1) from .wallet import Wallet @@ -850,6 +874,7 @@ class MMGenToolCmdWallet(MMGenToolCmds): m = 'Seed ID of requested address ({}) does not match wallet ({})' die(1,m.format(addr.sid,ss.seed.sid)) al = AddrList( + proto = self.proto, seed = ss.seed, addr_idxs = AddrIdxList(str(addr.idx)), mmtype = addr.mmtype ) @@ -865,7 +890,7 @@ class MMGenToolCmdRPC(MMGenToolCmds): async def getbalance(self,minconf=1,quiet=False,pager=False): "list confirmed/unconfirmed, spendable/unspendable balances in tracking wallet" from .tw import TwGetBalance - return (await TwGetBalance(minconf,quiet)).format() + return (await TwGetBalance(self.proto,minconf,quiet)).format() async def listaddress(self, mmgen_addr:str, @@ -909,9 +934,9 @@ class MMGenToolCmdRPC(MMGenToolCmds): if len(a) != 2: m = "'{}': invalid address list argument (must be in form :[:])" die(1,m.format(mmgen_addrs)) - usr_addr_list = [MMGenID('{}:{}'.format(a[0],i)) for i in AddrIdxList(a[1])] + usr_addr_list = [MMGenID(self.proto,f'{a[0]}:{i}') for i in AddrIdxList(a[1])] - al = await TwAddrList(usr_addr_list,minconf,showempty,showbtcaddrs,all_labels) + al = await TwAddrList(self.proto,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels) if not al: die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty]) return await al.format(showbtcaddrs,sort,show_age,age_fmt or 'confs') @@ -926,7 +951,7 @@ class MMGenToolCmdRPC(MMGenToolCmds): show_mmid = True, wide_show_confs = True): "view tracking wallet" - twuo = await TwUnspentOutputs(minconf=minconf) + twuo = await TwUnspentOutputs(self.proto,minconf=minconf) await twuo.get_unspent_data(reverse_sort=reverse) twuo.age_fmt = age_fmt twuo.show_mmid = show_mmid @@ -940,7 +965,7 @@ class MMGenToolCmdRPC(MMGenToolCmds): async def add_label(self,mmgen_or_coin_addr:str,label:str): "add descriptive label for address in tracking wallet" from .tw import TrackingWallet - await (await TrackingWallet(mode='w')).add_label(mmgen_or_coin_addr,label,on_fail='raise') + await (await TrackingWallet(self.proto,mode='w')).add_label(mmgen_or_coin_addr,label,on_fail='raise') return True async def remove_label(self,mmgen_or_coin_addr:str): @@ -951,7 +976,7 @@ class MMGenToolCmdRPC(MMGenToolCmds): async def remove_address(self,mmgen_or_coin_addr:str): "remove an address from tracking wallet" from .tw import TrackingWallet - ret = await (await TrackingWallet(mode='w')).remove_address(mmgen_or_coin_addr) # returns None on failure + ret = await (await TrackingWallet(self.proto,mode='w')).remove_address(mmgen_or_coin_addr) # returns None on failure if ret: msg("Address '{}' deleted from tracking wallet".format(ret)) return ret @@ -1083,9 +1108,9 @@ class MMGenToolCmdMonero(MMGenToolCmds): async def process_wallets(op): opt.accept_defaults = opt.accept_defaults or op.accept_defaults from .protocol import init_proto - g.proto = init_proto('xmr') + proto = init_proto('xmr',network='mainnet') from .addr import AddrList - al = KeyAddrList(infile) + al = KeyAddrList(proto,infile) data = [d for d in al.data if addrs == '' or d.idx in AddrIdxList(addrs)] dl = len(data) assert dl,"No addresses in addrfile within range '{}'".format(addrs) @@ -1139,7 +1164,7 @@ class MMGenToolCmdMonero(MMGenToolCmds): 'create': wo('create', 'Creat', 'Generat', create, False), 'sync': wo('sync', 'Sync', 'Sync', sync, True) }[op] try: - run_session(process_wallets(op),do_rpc_init=False) + run_session(process_wallets(op)) except KeyboardInterrupt: rdie(1,'\nUser interrupt\n') except EOFError: @@ -1191,6 +1216,7 @@ class tool_api( """ Initializer - takes no arguments """ + super().__init__() if not hasattr(opt,'version'): opts.init() opt.use_old_ed25519 = None @@ -1205,7 +1231,8 @@ class tool_api( from .protocol import init_proto,init_genonly_altcoins altcoin_trust_level = init_genonly_altcoins(coinsym,testnet=network in ('testnet','regtest')) warn_altcoins(coinsym,altcoin_trust_level) - return init_proto(coinsym,network=network) + self.proto = init_proto(coinsym,network=network) # FIXME + return self.proto @property def coins(self): @@ -1214,18 +1241,18 @@ class tool_api( from .altcoin import CoinInfo return sorted(set( [c.upper() for c in CoinProtocol.coins] - + [c.symbol for c in CoinInfo.get_supported_coins(g.proto.network)] + + [c.symbol for c in CoinInfo.get_supported_coins(self.proto.network)] )) @property def coin(self): """The currently configured coin""" - return g.coin + return self.proto.coin @property def network(self): """The currently configured network""" - return g.proto.network + return self.proto.network @property def addrtypes(self): @@ -1233,14 +1260,14 @@ class tool_api( The available address types for current coin/network pair. The first-listed is the default """ - return [MMGenAddrType(t).name for t in g.proto.mmtypes] + return [MMGenAddrType(proto=proto,id_str=id_str).name for id_str in self.proto.mmtypes] def print_addrtypes(self): """ Print the available address types for current coin/network pair along with a description. The first-listed is the default """ - for t in [MMGenAddrType(s) for s in g.proto.mmtypes]: + for t in [MMGenAddrType(proto=proto,id_str=id_str).name for id_str in self.proto.mmtypes]: print('{:<12} - {}'.format(t.name,t.desc)) @property diff --git a/mmgen/tw.py b/mmgen/tw.py index 531f44d0..e780e2ed 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -21,43 +21,45 @@ tw: Tracking wallet methods for the MMGen suite """ import json +from collections import namedtuple from .exception import * from .common import * from .obj import * -from .tx import is_mmgen_id +from .tx import is_mmgen_id,is_coin_addr +from .rpc import rpc_init CUR_HOME,ERASE_ALL = '\033[H','\033[0J' def CUR_RIGHT(n): return '\033[{}C'.format(n) -def get_tw_label(s): - try: return TwLabel(s,on_fail='raise') +def get_tw_label(proto,s): + try: return TwLabel(proto,s,on_fail='raise') except BadTwComment: raise except: return None _date_formatter = { - 'days': lambda secs: (g.rpc.cur_date - secs) // 86400, - 'date': lambda secs: '{}-{:02}-{:02}'.format(*time.gmtime(secs)[:3])[2:], - 'date_time': lambda secs: '{}-{:02}-{:02} {:02}:{:02}'.format(*time.gmtime(secs)[:5]), + 'days': lambda rpc,secs: (rpc.cur_date - secs) // 86400, + 'date': lambda rpc,secs: '{}-{:02}-{:02}'.format(*time.gmtime(secs)[:3])[2:], + 'date_time': lambda rpc,secs: '{}-{:02}-{:02} {:02}:{:02}'.format(*time.gmtime(secs)[:5]), } -async def _set_dates(us): +async def _set_dates(rpc,us): if us and us[0].date is None: # 'blocktime' differs from 'time', is same as getblockheader['time'] - dates = [o['blocktime'] for o in await g.rpc.gathered_call('gettransaction',[(o.txid,) for o in us])] - for o,date in zip(us,dates): - o.date = date + dates = [o['blocktime'] for o in await rpc.gathered_call('gettransaction',[(o.txid,) for o in us])] + for idx,o in enumerate(us): + o.date = dates[idx] if os.getenv('MMGEN_BOGUS_WALLET_DATA'): # 1831006505 (09 Jan 2028) = projected time of block 1000000 - _date_formatter['days'] = lambda date: (1831006505 - date) // 86400 - async def _set_dates(us): + _date_formatter['days'] = lambda rpc,secs: (1831006505 - secs) // 86400 + async def _set_dates(rpc,us): for o in us: o.date = 1831006505 - int(9.7 * 60 * (o.confs - 1)) class TwUnspentOutputs(MMGenObject,metaclass=aInitMeta): - def __new__(cls,*args,**kwargs): - return MMGenObject.__new__(altcoin_subclass(cls,'tw','TwUnspentOutputs')) + def __new__(cls,proto,*args,**kwargs): + return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw')) txid_w = 64 disp_type = 'btc' @@ -87,16 +89,30 @@ Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view, add [l]abel: class MMGenTwUnspentOutput(MMGenListItem): txid = ListItemAttr('CoinTxID') vout = ListItemAttr(int,typeconv=False) - amt = ImmutableAttr(lambda val:g.proto.coin_amt(val),typeconv=False) - amt2 = ListItemAttr(lambda val:g.proto.coin_amt(val),typeconv=False) + amt = ImmutableAttr(None) + amt2 = ListItemAttr(None) label = ListItemAttr('TwComment',reassign_ok=True) - twmmid = ImmutableAttr('TwMMGenID') - addr = ImmutableAttr('CoinAddr') + twmmid = ImmutableAttr('TwMMGenID',include_proto=True) + addr = ImmutableAttr('CoinAddr',include_proto=True) confs = ImmutableAttr(int,typeconv=False) date = ListItemAttr(int,typeconv=False,reassign_ok=True) scriptPubKey = ImmutableAttr('HexStr') skip = ListItemAttr(str,typeconv=False,reassign_ok=True) + # required by gen_unspent(); setting valid_attrs explicitly is also more efficient + valid_attrs = {'txid','vout','amt','amt2','label','twmmid','addr','confs','date','scriptPubKey','skip'} + invalid_attrs = {'proto'} + + def __init__(self,proto,**kwargs): + self.__dict__['proto'] = proto + MMGenListItem.__init__(self,**kwargs) + + class conv_funcs: + def amt(self,value): + return self.proto.coin_amt(value) + def amt2(self,value): + return self.proto.coin_amt(value) + wmsg = { 'no_spendable_outputs': """ No spendable outputs found! Import addresses with balances into your @@ -104,7 +120,8 @@ watch-only wallet using '{}-addrimport' and then re-run this program. """.strip().format(g.proj_name.lower()) } - async def __ainit__(self,minconf=1,addrs=[]): + async def __ainit__(self,proto,minconf=1,addrs=[]): + self.proto = proto self.unspent = self.MMGenTwOutputList() self.fmt_display = '' self.fmt_print = '' @@ -116,8 +133,11 @@ watch-only wallet using '{}-addrimport' and then re-run this program. self.addrs = addrs self.sort_key = 'age' self.disp_prec = self.get_display_precision() + self.rpc = await rpc_init(proto) - self.wallet = await TrackingWallet(mode='w') + self.wallet = await TrackingWallet(proto,mode='w') + if self.disp_type == 'token': + self.proto.tokensym = self.wallet.symbol @property def age_fmt(self): @@ -130,7 +150,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program. self._age_fmt = val def get_display_precision(self): - return g.proto.coin_amt.max_prec + return self.proto.coin_amt.max_prec @property def total(self): @@ -147,42 +167,40 @@ watch-only wallet using '{}-addrimport' and then re-run this program. # for now, self.addrs is just an empty list for Bitcoin and friends add_args = (9999999,self.addrs) if self.addrs else () - return await g.rpc.call('listunspent',self.minconf,*add_args) + return await self.rpc.call('listunspent',self.minconf,*add_args) async def get_unspent_data(self,sort_key=None,reverse_sort=False): if g.bogus_wallet_data: # for debugging purposes only - us_rpc = eval(get_data_from_file(g.bogus_wallet_data)) # testing, so ok + us_raw = eval(get_data_from_file(g.bogus_wallet_data)) # testing, so ok else: - us_rpc = await self.get_unspent_rpc() + us_raw = await self.get_unspent_rpc() - if not us_rpc: + if not us_raw: die(0,self.wmsg['no_spendable_outputs']) - tr_rpc = [] - lbl_id = ('account','label')['label_api' in g.rpc.caps] + lbl_id = ('account','label')['label_api' in self.rpc.caps] - for o in us_rpc: - if not lbl_id in o: - continue # coinbase outputs have no account field - l = get_tw_label(o[lbl_id]) - if l: - o.update({ - 'twmmid': l.mmid, - 'label': l.comment, - 'amt': g.proto.coin_amt(o['amount']), - 'addr': CoinAddr(o['address']), - 'confs': o['confirmations'] - }) - tr_rpc.append(o) + def gen_unspent(): + for o in us_raw: + if not lbl_id in o: + continue # coinbase outputs have no account field + l = get_tw_label(self.proto,o[lbl_id]) + if l: + o.update({ + 'twmmid': l.mmid, + 'label': l.comment or '', + 'amt': self.proto.coin_amt(o['amount']), + 'addr': CoinAddr(self.proto,o['address']), + 'confs': o['confirmations'] + }) + yield self.MMGenTwUnspentOutput( + self.proto, + **{ k:v for k,v in o.items() if k in self.MMGenTwUnspentOutput.valid_attrs } ) + + self.unspent = self.MMGenTwOutputList(gen_unspent()) - self.unspent = self.MMGenTwOutputList( - self.MMGenTwUnspentOutput( - **{k:v for k,v in o.items() if k in dir(self.MMGenTwUnspentOutput)} - ) for o in tr_rpc) - for u in self.unspent: - if u.label == None: u.label = '' if not self.unspent: - die(1,'No tracked {}s in tracking wallet!'.format(self.item_desc)) + die(1, f'No tracked {self.item_desc}s in tracking wallet!') self.do_sort(key=sort_key,reverse=reverse_sort) @@ -217,11 +235,10 @@ watch-only wallet using '{}-addrimport' and then re-run this program. m2 = 'Please resize your screen to at least {} characters and hit ENTER ' my_raw_input((m1+m2).format(g.min_screen_width)) - async def format_for_display(self): + def get_display_constants(self): unsp = self.unspent - if self.age_fmt in self.age_fmts_date_dependent: - await _set_dates(unsp) - self.set_term_columns() + for i in unsp: + i.skip = '' # allow for 7-digit confirmation nums col1_w = max(3,len(str(len(unsp)))+1) # num + ')' @@ -236,117 +253,148 @@ watch-only wallet using '{}-addrimport' and then re-run this program. tx_w = min(self.txid_w,self.cols-addr_w-29-col1_w) # min=6 TODO txdots = ('','..')[tx_w < self.txid_w] - for i in unsp: i.skip = '' + dc = namedtuple('display_constants',['col1_w','mmid_w','addr_w','btaddr_w','label_w','tx_w','txdots']) + return dc(col1_w,mmid_w,addr_w,btaddr_w,label_w,tx_w,txdots) + + async def format_for_display(self): + unsp = self.unspent + if self.age_fmt in self.age_fmts_date_dependent: + await _set_dates(self.rpc,unsp) + self.set_term_columns() + + c = getattr(self,'display_constants',None) + if not c: + c = self.display_constants = self.get_display_constants() + if self.group and (self.sort_key in ('addr','txid','twmmid')): for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]: for k in ('addr','txid','twmmid'): if self.sort_key == k and getattr(a,k) == getattr(b,k): b.skip = (k,'addr')[k=='twmmid'] - out = [self.hdr_fmt.format(' '.join(self.sort_info()),g.dcoin,self.total.hl())] - if g.chain != 'mainnet': out += ['Chain: '+green(g.chain.upper())] - fs = { 'btc': ' {n:%s} {t:%s} {v:2} {a} {A} {c:<}' % (col1_w,tx_w), - 'eth': ' {n:%s} {a} {A}' % col1_w, - 'token': ' {n:%s} {a} {A} {A2}' % col1_w }[self.disp_type] - fs_hdr = ' {n:%s} {t:%s} {a} {A} {c:<}' % (col1_w,tx_w) if self.disp_type == 'btc' else fs - date_hdr = { - 'confs': 'Confs', - 'block': 'Block', - 'days': 'Age(d)', - 'date': 'Date', - 'date_time': 'Date', - } - out += [fs_hdr.format( - n='Num', - t='TXid'.ljust(tx_w - 2) + ' Vout', - a='Address'.ljust(addr_w), - A='Amt({})'.format(g.dcoin).ljust(self.disp_prec+5), - A2=' Amt({})'.format(g.coin).ljust(self.disp_prec+4), - c = date_hdr[self.age_fmt], - ).rstrip()] + def gen_output(): + yield self.hdr_fmt.format(' '.join(self.sort_info()),self.proto.dcoin,self.total.hl()) + if self.proto.chain_name != 'mainnet': + yield 'Chain: '+green(self.proto.chain_name.upper()) + fs = { 'btc': ' {n:%s} {t:%s} {v:2} {a} {A} {c:<}' % (c.col1_w,c.tx_w), + 'eth': ' {n:%s} {a} {A}' % c.col1_w, + 'token': ' {n:%s} {a} {A} {A2}' % c.col1_w }[self.disp_type] + fs_hdr = ' {n:%s} {t:%s} {a} {A} {c:<}' % (c.col1_w,c.tx_w) if self.disp_type == 'btc' else fs + date_hdr = { + 'confs': 'Confs', + 'block': 'Block', + 'days': 'Age(d)', + 'date': 'Date', + 'date_time': 'Date', + } + yield fs_hdr.format( + n = 'Num', + t = 'TXid'.ljust(c.tx_w - 2) + ' Vout', + a = 'Address'.ljust(c.addr_w), + A = 'Amt({})'.format(self.proto.dcoin).ljust(self.disp_prec+5), + A2 = ' Amt({})'.format(self.proto.coin).ljust(self.disp_prec+4), + c = date_hdr[self.age_fmt], + ).rstrip() - for n,i in enumerate(unsp): - addr_dots = '|' + '.'*(addr_w-1) - mmid_disp = MMGenID.fmtc('.'*mmid_w if i.skip=='addr' - else i.twmmid if i.twmmid.type=='mmgen' - else 'Non-{}'.format(g.proj_name),width=mmid_w,color=True) - if self.show_mmid: - addr_out = '{} {}'.format( - type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \ - else i.addr.fmt(width=btaddr_w,color=True), - '{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True) \ - if label_w > 0 else '')) - else: - addr_out = type(i.addr).fmtc(addr_dots,width=addr_w,color=True) \ - if i.skip=='addr' else i.addr.fmt(width=addr_w,color=True) + for n,i in enumerate(unsp): + addr_dots = '|' + '.'*(c.addr_w-1) + mmid_disp = MMGenID.fmtc('.'*c.mmid_w if i.skip=='addr' + else i.twmmid if i.twmmid.type=='mmgen' + else 'Non-{}'.format(g.proj_name),width=c.mmid_w,color=True) - out.append(fs.format( n=str(n+1)+')', - t='' if not i.txid else \ - ' ' * (tx_w-4) + '|...' if i.skip == 'txid' \ - else i.txid[:tx_w-len(txdots)] + txdots, - v=i.vout, - a=addr_out, - A=i.amt.fmt(color=True,prec=self.disp_prec), - A2=(i.amt2.fmt(color=True,prec=self.disp_prec) if i.amt2 is not None else ''), - c=self.age_disp(i,self.age_fmt), - ).rstrip()) + if self.show_mmid: + addr_out = '{} {}{}'.format(( + type(i.addr).fmtc(addr_dots,width=c.btaddr_w,color=True) if i.skip == 'addr' else + i.addr.fmt(width=c.btaddr_w,color=True) + ), + mmid_disp, + (' ' + i.label.fmt(width=c.label_w,color=True)) if c.label_w > 0 else '' + ) + else: + addr_out = ( + type(i.addr).fmtc(addr_dots,width=c.addr_w,color=True) if i.skip=='addr' else + i.addr.fmt(width=c.addr_w,color=True) ) - self.fmt_display = '\n'.join(out) + '\n' + yield fs.format( + n = str(n+1)+')', + t = ( + '' if not i.txid else + ' ' * (c.tx_w-4) + '|...' if i.skip == 'txid' else + i.txid[:c.tx_w-len(c.txdots)] + c.txdots ), + v = i.vout, + a = addr_out, + A = i.amt.fmt(color=True,prec=self.disp_prec), + A2 = (i.amt2.fmt(color=True,prec=self.disp_prec) if i.amt2 is not None else ''), + c = self.age_disp(i,self.age_fmt), + ).rstrip() + + self.fmt_display = '\n'.join(gen_output()) + '\n' return self.fmt_display async def format_for_printing(self,color=False,show_confs=True): if self.age_fmt in self.age_fmts_date_dependent: - await _set_dates(self.unspent) + await _set_dates(self.rpc,self.unspent) addr_w = max(len(i.addr) for i in self.unspent) mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in self.unspent) or 12 # DEADBEEF:S:1 - amt_w = g.proto.coin_amt.max_prec + 5 + amt_w = self.proto.coin_amt.max_prec + 5 cfs = '{c:<8} ' if show_confs else '' - fs = { 'btc': (' {n:4} {t:%s} {a} {m} {A:%s} ' + cfs + '{b:<8} {D:<19} {l}') % (self.txid_w+3,amt_w), - 'eth': ' {n:4} {a} {m} {A:%s} {l}' % amt_w, - 'token': ' {n:4} {a} {m} {A:%s} {A2:%s} {l}' % (amt_w,amt_w) - }[self.disp_type] - out = [fs.format( n='Num', - t='Tx ID,Vout', - a='Address'.ljust(addr_w), - m='MMGen ID'.ljust(mmid_w), - A='Amount({})'.format(g.dcoin), - A2='Amount({})'.format(g.coin), - c='Confs', # skipped for eth - b='Block', # skipped for eth - D='Date', - l='Label')] + fs = { + 'btc': (' {n:4} {t:%s} {a} {m} {A:%s} ' + cfs + '{b:<8} {D:<19} {l}') % (self.txid_w+3,amt_w), + 'eth': ' {n:4} {a} {m} {A:%s} {l}' % amt_w, + 'token': ' {n:4} {a} {m} {A:%s} {A2:%s} {l}' % (amt_w,amt_w) + }[self.disp_type] - max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [2]) - for n,i in enumerate(self.unspent): - addr = '|'+'.' * addr_w if i.skip == 'addr' and self.group else i.addr.fmt(color=color,width=addr_w) - out.append(fs.format( - n=str(n+1)+')', - t='{},{}'.format('|'+'.'*63 if i.skip == 'txid' and self.group else i.txid,i.vout), - a=addr, - m=MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen' - else 'Non-{}'.format(g.proj_name),width=mmid_w,color=color), - A=i.amt.fmt(color=color), - A2=(i.amt2.fmt(color=color) if i.amt2 is not None else ''), - c=i.confs, - b=g.rpc.blockcount - (i.confs - 1), - D=self.age_disp(i,'date_time'), - l=i.label.hl(color=color) if i.label else - TwComment.fmtc('',color=color,nullrepl='-',width=max_lbl_len)).rstrip()) + def gen_output(): + yield fs.format( + n = 'Num', + t = 'Tx ID,Vout', + a = 'Address'.ljust(addr_w), + m = 'MMGen ID'.ljust(mmid_w), + A = 'Amount({})'.format(self.proto.dcoin), + A2 = 'Amount({})'.format(self.proto.coin), + c = 'Confs', # skipped for eth + b = 'Block', # skipped for eth + D = 'Date', + l = 'Label' ) + + max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [2]) + for n,i in enumerate(self.unspent): + yield fs.format( + n = str(n+1)+')', + t = '{},{}'.format('|'+'.'*63 if i.skip == 'txid' and self.group else i.txid,i.vout), + a = ( + '|'+'.' * addr_w if i.skip == 'addr' and self.group else + i.addr.fmt(color=color,width=addr_w) ), + m = MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen' else + 'Non-{}'.format(g.proj_name),width = mmid_w,color=color), + A = i.amt.fmt(color=color), + A2 = ( i.amt2.fmt(color=color) if i.amt2 is not None else '' ), + c = i.confs, + b = self.rpc.blockcount - (i.confs - 1), + D = self.age_disp(i,'date_time'), + l = i.label.hl(color=color) if i.label else + TwComment.fmtc('',color = color,nullrepl='-',width=max_lbl_len) ).rstrip() + + fs2 = '{} (block #{}, {} UTC)\n{}Sort order: {}\n{}\n\nTotal {}: {}\n' + self.fmt_print = fs2.format( + capfirst(self.desc), + self.rpc.blockcount, + make_timestr(self.rpc.cur_date), + ('' if self.proto.chain_name == 'mainnet' else + 'Chain: {}\n'.format(green(self.proto.chain_name.upper())) ), + ' '.join(self.sort_info(include_group=False)), + '\n'.join(gen_output()), + self.proto.dcoin, + self.total.hl(color=color) ) - fs = '{} (block #{}, {} UTC)\nSort order: {}\n{}\n\nTotal {}: {}\n' - self.fmt_print = fs.format( - capfirst(self.desc), - g.rpc.blockcount, - make_timestr(g.rpc.cur_date), - ' '.join(self.sort_info(include_group=False)), - '\n'.join(out), - g.dcoin, - self.total.hl(color=color)) return self.fmt_print def display_total(self): - fs = '\nTotal unspent: {} {} ({} output%s)' % suf(self.unspent) - msg(fs.format(self.total.hl(),g.dcoin,len(self.unspent))) + msg('\nTotal unspent: {} {} ({} output{})'.format( + self.total.hl(), + self.proto.dcoin, + len(self.unspent), + suf(self.unspent) )) def get_idx_from_user(self,action): msg('') @@ -420,7 +468,8 @@ watch-only wallet using '{}-addrimport' and then re-run this program. e = self.unspent[idx-1] bal = await self.wallet.get_balance(e.addr,force_rpc=True) await self.get_unspent_data() - oneshot_msg = yellow('{} balance for account #{} refreshed\n\n'.format(g.dcoin,idx)) + oneshot_msg = yellow('{} balance for account #{} refreshed\n\n'.format(self.proto.dcoin,idx)) + self.display_constants = self.get_display_constants() elif action == 'a_lbl_add': idx,lbl = self.get_idx_from_user(action) if idx: @@ -431,6 +480,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program. oneshot_msg = yellow("Label {} {} #{}\n\n".format(a,self.item_desc,idx)) else: oneshot_msg = red('Label could not be added\n\n') + self.display_constants = self.get_display_constants() elif action == 'a_addr_delete': idx = self.get_idx_from_user(action) if idx: @@ -440,8 +490,9 @@ watch-only wallet using '{}-addrimport' and then re-run this program. oneshot_msg = yellow("{} #{} removed\n\n".format(capfirst(self.item_desc),idx)) else: oneshot_msg = red('Address could not be removed\n\n') + self.display_constants = self.get_display_constants() elif action == 'a_print': - of = '{}-{}[{}].out'.format(self.dump_fn_pfx,g.dcoin, + of = '{}-{}[{}].out'.format(self.dump_fn_pfx,self.proto.dcoin, ','.join(self.sort_info(include_group=False)).lower()) msg('') try: @@ -460,22 +511,22 @@ watch-only wallet using '{}-addrimport' and then re-run this program. if age_fmt == 'confs': return o.confs elif age_fmt == 'block': - return g.rpc.blockcount - (o.confs - 1) + return self.rpc.blockcount - (o.confs - 1) else: - return _date_formatter[age_fmt](o.date) + return _date_formatter[age_fmt](self.rpc,o.date) class TwAddrList(MMGenDict,metaclass=aInitMeta): has_age = True age_fmts = TwUnspentOutputs.age_fmts age_disp = TwUnspentOutputs.age_disp - def __new__(cls,*args,**kwargs): - return MMGenDict.__new__(altcoin_subclass(cls,'tw','TwAddrList'),*args,**kwargs) + def __new__(cls,proto,*args,**kwargs): + return MMGenDict.__new__(altcoin_subclass(cls,proto,'tw'),*args,**kwargs) - def __init__(self,*args,**kwargs): + def __init__(self,proto,*args,**kwargs): pass - async def __ainit__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None): + async def __ainit__(self,proto,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None): def check_dup_mmid(acct_labels): mmid_prev,err = None,False @@ -493,18 +544,20 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta): if len(addrs) != 1: err = True if len(addrs) == 0: - msg("Label '{}': has no associated address!".format(label)) + msg(f'Label {label!r}: has no associated address!') else: - msg("'{}': more than one {} address in account!".format(addrs,g.coin)) + msg(f'{addrs!r}: more than one {proto.coin} address in account!') if err: rdie(3,'Tracking wallet is corrupted!') - self.total = g.proto.coin_amt('0') + self.rpc = await rpc_init(proto) + self.total = proto.coin_amt('0') + self.proto = proto - lbl_id = ('account','label')['label_api' in g.rpc.caps] - for d in await g.rpc.call('listunspent',0): + lbl_id = ('account','label')['label_api' in self.rpc.caps] + for d in await self.rpc.call('listunspent',0): if not lbl_id in d: continue # skip coinbase outputs with missing account if d['confirmations'] < minconf: continue - label = get_tw_label(d[lbl_id]) + label = get_tw_label(proto,d[lbl_id]) if label: lm = label.mmid if usr_addr_list and (lm not in usr_addr_list): @@ -512,14 +565,16 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta): if lm in self: if self[lm]['addr'] != d['address']: die(2,'duplicate {} address ({}) for this MMGen address! ({})'.format( - g.coin,d['address'],self[lm]['addr'])) + proto.coin, + d['address'], + self[lm]['addr']) ) else: lm.confs = d['confirmations'] lm.txid = d['txid'] lm.date = None - self[lm] = {'amt': g.proto.coin_amt('0'), + self[lm] = {'amt': proto.coin_amt('0'), 'lbl': label, - 'addr': CoinAddr(d['address'])} + 'addr': CoinAddr(proto,d['address'])} self[lm]['amt'] += d['amount'] self.total += d['amount'] @@ -527,14 +582,14 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta): if showempty or all_labels: # for compatibility with old mmids, must use raw RPC rather than native data for matching # args: minconf,watchonly, MUST use keys() so we get list, not dict - if 'label_api' in g.rpc.caps: - acct_list = await g.rpc.call('listlabels') - aa = await g.rpc.batch_call('getaddressesbylabel',[(k,) for k in acct_list]) + if 'label_api' in self.rpc.caps: + acct_list = await self.rpc.call('listlabels') + aa = await self.rpc.batch_call('getaddressesbylabel',[(k,) for k in acct_list]) acct_addrs = [list(a.keys()) for a in aa] else: - acct_list = list((await g.rpc.call('listaccounts',0,True)).keys()) # raw list, no 'L' - acct_addrs = await g.rpc.batch_call('getaddressesbyaccount',[(a,) for a in acct_list]) # use raw list here - acct_labels = MMGenList([get_tw_label(a) for a in acct_list]) + acct_list = list((await self.rpc.call('listaccounts',0,True)).keys()) # raw list, no 'L' + acct_addrs = await self.rpc.batch_call('getaddressesbyaccount',[(a,) for a in acct_list]) # use raw list here + acct_labels = MMGenList([get_tw_label(proto,a) for a in acct_list]) check_dup_mmid(acct_labels) assert len(acct_list) == len(acct_addrs),( 'listaccounts() and getaddressesbyaccount() not equal in length') @@ -545,9 +600,9 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta): if all_labels and not showempty and not label.comment: continue if usr_addr_list and (label.mmid not in usr_addr_list): continue if label.mmid not in self: - self[label.mmid] = { 'amt':g.proto.coin_amt('0'), 'lbl':label, 'addr':'' } + self[label.mmid] = { 'amt':proto.coin_amt('0'), 'lbl':label, 'addr':'' } if showbtcaddrs: - self[label.mmid]['addr'] = CoinAddr(addr_arr[0]) + self[label.mmid]['addr'] = CoinAddr(proto,addr_arr[0]) def raw_list(self): return [((k if k.type == 'mmgen' else 'Non-MMGen'),self[k]['addr'],self[k]['amt']) for k in self] @@ -560,22 +615,13 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta): show_age = False if age_fmt not in self.age_fmts: raise BadAgeFormat("'{}': invalid age format (must be one of {!r})".format(age_fmt,self.age_fmts)) - out = ['Chain: '+green(g.chain.upper())] if g.chain != 'mainnet' else [] fs = '{mid}' + ('',' {addr}')[showbtcaddrs] + ' {cmt} {amt}' + ('',' {age}')[show_age] mmaddrs = [k for k in self.keys() if k.type == 'mmgen'] max_mmid_len = max(len(k) for k in mmaddrs) + 2 if mmaddrs else 10 max_cmt_width = max(max(v['lbl'].comment.screen_width for v in self.values()),7) addr_width = max(len(self[mmid]['addr']) for mmid in self) - # fp: fractional part max_fp_len = max([len(a.split('.')[1]) for a in [str(v['amt']) for v in self.values()] if '.' in a] or [1]) - out += [fs.format( - mid=MMGenID.fmtc('MMGenID',width=max_mmid_len), - addr=(CoinAddr.fmtc('ADDRESS',width=addr_width) if showbtcaddrs else None), - cmt=TwComment.fmtc('COMMENT',width=max_cmt_width+1), - amt='BALANCE'.ljust(max_fp_len+4), - age=age_fmt.upper(), - ).rstrip()] def sort_algo(j): if sort and 'age' in sort: @@ -587,31 +633,47 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta): else: return j.sort_key - al_id_save = None mmids = sorted(self,key=sort_algo,reverse=bool(sort and 'reverse' in sort)) if show_age: - await _set_dates([o for o in mmids if hasattr(o,'confs')]) - for mmid in mmids: - if mmid.type == 'mmgen': - if al_id_save and al_id_save != mmid.obj.al_id: - out.append('') - al_id_save = mmid.obj.al_id - mmid_disp = mmid - else: - if al_id_save: - out.append('') - al_id_save = None - mmid_disp = 'Non-MMGen' - e = self[mmid] - out.append(fs.format( - mid=MMGenID.fmtc(mmid_disp,width=max_mmid_len,color=True), - addr=(e['addr'].fmt(color=True,width=addr_width) if showbtcaddrs else None), - cmt=e['lbl'].comment.fmt(width=max_cmt_width,color=True,nullrepl='-'), - amt=e['amt'].fmt('4.{}'.format(max(max_fp_len,3)),color=True), - age=self.age_disp(mmid,age_fmt) if show_age and hasattr(mmid,'confs') else '-' - ).rstrip()) + await _set_dates(self.rpc,[o for o in mmids if hasattr(o,'confs')]) - return '\n'.join(out + ['\nTOTAL: {} {}'.format(self.total.hl(color=True),g.dcoin)]) + def gen_output(): + + if self.proto.chain_name != 'mainnet': + yield 'Chain: '+green(self.proto.chain_name.upper()) + + yield fs.format( + mid=MMGenID.fmtc('MMGenID',width=max_mmid_len), + addr=(CoinAddr.fmtc('ADDRESS',width=addr_width) if showbtcaddrs else None), + cmt=TwComment.fmtc('COMMENT',width=max_cmt_width+1), + amt='BALANCE'.ljust(max_fp_len+4), + age=age_fmt.upper(), + ).rstrip() + + al_id_save = None + for mmid in mmids: + if mmid.type == 'mmgen': + if al_id_save and al_id_save != mmid.obj.al_id: + yield '' + al_id_save = mmid.obj.al_id + mmid_disp = mmid + else: + if al_id_save: + yield '' + al_id_save = None + mmid_disp = 'Non-MMGen' + e = self[mmid] + yield fs.format( + mid=MMGenID.fmtc(mmid_disp,width=max_mmid_len,color=True), + addr=(e['addr'].fmt(color=True,width=addr_width) if showbtcaddrs else None), + cmt=e['lbl'].comment.fmt(width=max_cmt_width,color=True,nullrepl='-'), + amt=e['amt'].fmt('4.{}'.format(max(max_fp_len,3)),color=True), + age=self.age_disp(mmid,age_fmt) if show_age and hasattr(mmid,'confs') else '-' + ).rstrip() + + yield '\nTOTAL: {} {}'.format(self.total.hl(color=True),self.proto.dcoin) + + return '\n'.join(gen_output()) class TrackingWallet(MMGenObject,metaclass=aInitMeta): @@ -621,10 +683,10 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): aggressive_sync = False importing = False - def __new__(cls,*args,**kwargs): - return MMGenObject.__new__(altcoin_subclass(cls,'tw','TrackingWallet')) + def __new__(cls,proto,*args,**kwargs): + return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw')) - async def __ainit__(self,mode='r'): + async def __ainit__(self,proto,mode='r',token_addr=None): assert mode in ('r','w','i'), "{!r}: wallet mode must be 'r','w' or 'i'".format(mode) if mode == 'i': @@ -634,33 +696,35 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): if g.debug: print_stack_trace('TW INIT {!r} {!r}'.format(mode,self)) + self.rpc = await rpc_init(proto) # TODO: create on demand - only certain ops require RPC + self.proto = proto self.mode = mode - self.desc = self.base_desc = f'{g.proto.name} tracking wallet' + self.desc = self.base_desc = f'{self.proto.name} tracking wallet' if self.use_tw_file: self.init_from_wallet_file() else: self.init_empty() - if self.data['coin'] != g.coin: + if self.data['coin'] != self.proto.coin: # TODO remove? m = 'Tracking wallet coin ({}) does not match current coin ({})!' - raise WalletFileError(m.format(self.data['coin'],g.coin)) + raise WalletFileError(m.format(self.data['coin'],self.proto.coin)) self.conv_types(self.data[self.data_key]) self.cur_balances = {} # cache balances to prevent repeated lookups per program invocation def init_empty(self): - self.data = { 'coin': g.coin, 'addresses': {} } + self.data = { 'coin': self.proto.coin, 'addresses': {} } def init_from_wallet_file(self): - tw_dir = ( - os.path.join(g.data_dir,g.proto.data_subdir) if g.coin == 'BTC' else + os.path.join(g.data_dir) if self.proto.coin == 'BTC' else os.path.join( g.data_dir_root, 'altcoins', - g.coin.lower(), - g.proto.data_subdir) ) + self.proto.coin.lower(), + ('' if self.proto.network == 'mainnet' else 'testnet') + )) self.tw_fn = os.path.join(tw_dir,'tracking-wallet.json') check_or_create_dir(tw_dir) @@ -712,11 +776,10 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): def upgrade_wallet_maybe(self): pass - @staticmethod - def conv_types(ad): + def conv_types(self,ad): for k,v in ad.items(): if k not in ('params','coin'): - v['mmid'] = TwMMGenID(v['mmid'],on_fail='raise') + v['mmid'] = TwMMGenID(self.proto,v['mmid'],on_fail='raise') v['comment'] = TwComment(v['comment'],on_fail='raise') @property @@ -737,11 +800,11 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): def get_cached_balance(self,addr,session_cache,data_root): if addr in session_cache: - return g.proto.coin_amt(session_cache[addr]) + return self.proto.coin_amt(session_cache[addr]) if not g.use_cached_balances: return None if addr in data_root and 'balance' in data_root[addr]: - return g.proto.coin_amt(data_root[addr]['balance']) + return self.proto.coin_amt(data_root[addr]['balance']) async def get_balance(self,addr,force_rpc=False): ret = None if force_rpc else self.get_cached_balance(addr,self.cur_balances,self.data_root) @@ -768,11 +831,11 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): @write_mode async def import_address(self,addr,label,rescan): - return await g.rpc.call('importaddress',addr,label,rescan,timeout=(False,3600)[rescan]) + return await self.rpc.call('importaddress',addr,label,rescan,timeout=(False,3600)[rescan]) @write_mode def batch_import_address(self,arg_list): - return g.rpc.batch_call('importaddress',arg_list) + return self.rpc.batch_call('importaddress',arg_list) def force_write(self): mode_save = self.mode @@ -805,13 +868,13 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): msg('Data is unchanged\n') async def is_in_wallet(self,addr): - return addr in (await TwAddrList([],0,True,True,True,wallet=self)).coinaddr_list() + return addr in (await TwAddrList(self.proto,[],0,True,True,True,wallet=self)).coinaddr_list() @write_mode async def set_label(self,coinaddr,lbl): # bitcoin-abc 'setlabel' RPC is broken, so use old 'importaddress' method to set label # broken behavior: new label is set OK, but old label gets attached to another address - if 'label_api' in g.rpc.caps and g.coin != 'BCH': + if 'label_api' in self.rpc.caps and self.proto.coin != 'BCH': args = ('setlabel',coinaddr,lbl) else: # NOTE: this works because importaddress() removes the old account before @@ -820,7 +883,7 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): args = ('importaddress',coinaddr,lbl,False) try: - return await g.rpc.call(*args) + return await self.rpc.call(*args) except Exception as e: rmsg(e.args[0]) return False @@ -828,19 +891,18 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): # returns on failure @write_mode async def add_label(self,arg1,label='',addr=None,silent=False,on_fail='return'): - from .tx import is_mmgen_id,is_coin_addr mmaddr,coinaddr = None,None - if is_coin_addr(addr or arg1): - coinaddr = CoinAddr(addr or arg1,on_fail='return') - if is_mmgen_id(arg1): - mmaddr = TwMMGenID(arg1) + if is_coin_addr(self.proto,addr or arg1): + coinaddr = CoinAddr(self.proto,addr or arg1,on_fail='return') + if is_mmgen_id(self.proto,arg1): + mmaddr = TwMMGenID(self.proto,arg1) if mmaddr and not coinaddr: from .addr import TwAddrData - coinaddr = (await TwAddrData()).mmaddr2coinaddr(mmaddr) + coinaddr = (await TwAddrData(self.proto)).mmaddr2coinaddr(mmaddr) try: - if not is_mmgen_id(arg1): + if not is_mmgen_id(self.proto,arg1): assert coinaddr,"Invalid coin address for this chain: {}".format(arg1) assert coinaddr,"{pn} address '{ma}' not found in tracking wallet" assert await self.is_in_wallet(coinaddr),"Address '{ca}' not found in tracking wallet" @@ -852,18 +914,18 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): # Do reverse lookup, so that MMGen addr will not be marked as non-MMGen. if not mmaddr: from .addr import TwAddrData - mmaddr = (await TwAddrData()).coinaddr2mmaddr(coinaddr) + mmaddr = (await TwAddrData(proto=self.proto)).coinaddr2mmaddr(coinaddr) if not mmaddr: - mmaddr = '{}:{}'.format(g.proto.base_coin.lower(),coinaddr) + mmaddr = '{}:{}'.format(self.proto.base_coin.lower(),coinaddr) - mmaddr = TwMMGenID(mmaddr) + mmaddr = TwMMGenID(self.proto,mmaddr) cmt = TwComment(label,on_fail=on_fail) if cmt in (False,None): return False - lbl = TwLabel(mmaddr + ('',' '+cmt)[bool(cmt)],on_fail=on_fail) + lbl = TwLabel(self.proto,mmaddr + ('',' '+cmt)[bool(cmt)],on_fail=on_fail) if await self.set_label(coinaddr,lbl) == False: if not silent: @@ -871,7 +933,7 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): return False else: m = mmaddr.type.replace('mmg','MMG') - a = mmaddr.replace(g.proto.base_coin.lower()+':','') + a = mmaddr.replace(self.proto.base_coin.lower()+':','') s = '{} address {} in tracking wallet'.format(m,a) if label: msg("Added label '{}' to {}".format(label,s)) else: msg('Removed label from {}'.format(s)) @@ -883,32 +945,34 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta): @write_mode async def remove_address(self,addr): - raise NotImplementedError('address removal not implemented for coin {}'.format(g.coin)) + raise NotImplementedError('address removal not implemented for coin {}'.format(self.proto.coin)) class TwGetBalance(MMGenObject,metaclass=aInitMeta): - fs = '{w:13} {u:<16} {p:<16} {c}\n' + fs = '{w:13} {u:<16} {p:<16} {c}' - def __new__(cls,*args,**kwargs): - return MMGenObject.__new__(altcoin_subclass(cls,'tw','TwGetBalance')) + def __new__(cls,proto,*args,**kwargs): + return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw')) - async def __ainit__(self,minconf,quiet): + async def __ainit__(self,proto,minconf,quiet): self.minconf = minconf self.quiet = quiet - self.data = {k:[g.proto.coin_amt('0')] * 4 for k in ('TOTAL','Non-MMGen','Non-wallet')} + self.data = {k:[proto.coin_amt('0')] * 4 for k in ('TOTAL','Non-MMGen','Non-wallet')} + self.rpc = await rpc_init(proto) + self.proto = proto await self.create_data() async def create_data(self): # 0: unconfirmed, 1: below minconf, 2: confirmed, 3: spendable (privkey in wallet) - lbl_id = ('account','label')['label_api' in g.rpc.caps] - for d in await g.rpc.call('listunspent',0): - lbl = get_tw_label(d[lbl_id]) + lbl_id = ('account','label')['label_api' in self.rpc.caps] + for d in await self.rpc.call('listunspent',0): + lbl = get_tw_label(self.proto,d[lbl_id]) if lbl: if lbl.mmid.type == 'mmgen': key = lbl.mmid.obj.sid if key not in self.data: - self.data[key] = [g.proto.coin_amt('0')] * 4 + self.data[key] = [self.proto.coin_amt('0')] * 4 else: key = 'Non-MMGen' else: @@ -927,22 +991,31 @@ class TwGetBalance(MMGenObject,metaclass=aInitMeta): self.data[key][3] += d['amount'] def format(self): - if self.quiet: - o = str(self.data['TOTAL'][2] if self.data else 0) + '\n' - else: - o = self.fs.format( w='Wallet', - u=' Unconfirmed', - p=' <{} confirms'.format(self.minconf), - c=' >={} confirms'.format(self.minconf)) - for key in sorted(self.data): - if not any(self.data[key]): continue - o += self.fs.format(**dict(zip( - ('w','u','p','c'), - [key+':'] + [a.fmt(color=True,suf=' '+g.dcoin) for a in self.data[key]] - ))) + def gen_output(): + if self.proto.chain_name != 'mainnet': + yield 'Chain: ' + green(self.proto.chain_name.upper()) - for key,vals in list(self.data.items()): - if key == 'TOTAL': continue - if vals[3]: - o += red('Warning: this wallet contains PRIVATE KEYS for {} outputs!\n'.format(key)) - return o.rstrip() + if self.quiet: + yield str(self.data['TOTAL'][2] if self.data else 0) + else: + yield self.fs.format( + w = 'Wallet', + u = ' Unconfirmed', + p = f' <{self.minconf} confirms', + c = f' >={self.minconf} confirms' ) + + for key in sorted(self.data): + if not any(self.data[key]): + continue + yield self.fs.format(**dict(zip( + ('w','u','p','c'), + [key+':'] + [a.fmt(color=True,suf=' '+self.proto.dcoin) for a in self.data[key]] + ))) + + for key,vals in list(self.data.items()): + if key == 'TOTAL': + continue + if vals[3]: + yield red(f'Warning: this wallet contains PRIVATE KEYS for {key} outputs!') + + return '\n'.join(gen_output()).rstrip() diff --git a/mmgen/tx.py b/mmgen/tx.py index 67222fbb..bdc97dd6 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -60,9 +60,9 @@ def strfmt_locktime(num,terse=False): elif num > 0: return '{}{}'.format(('block height ','')[terse],num) else: - die(2,"'{}': invalid locktime value!".format(num)) + die(2,"'{}': invalid nLockTime value!".format(num)) -def mmaddr2coinaddr(mmaddr,ad_w,ad_f): +def mmaddr2coinaddr(mmaddr,ad_w,ad_f,proto): # assume mmaddr has already been checked coin_addr = ad_w.mmaddr2coinaddr(mmaddr) @@ -79,42 +79,27 @@ def mmaddr2coinaddr(mmaddr,ad_w,ad_f): else: die(2,wmsg('addr_not_found_no_addrfile').format(mmaddr)) - return CoinAddr(coin_addr) + return CoinAddr(proto,coin_addr) -def segwit_is_active(exit_on_error=False): - d = g.rpc.cached['blockchaininfo'] - if d['chain'] == 'regtest': - return True - if ( 'bip9_softforks' in d - and 'segwit' in d['bip9_softforks'] - and d['bip9_softforks']['segwit']['status'] == 'active'): - return True - if g.test_suite: - return True - if exit_on_error: - die(2,'Segwit not active on this chain. Exiting') - else: - return False - -def addr2pubhash(addr): - ap = g.proto.parse_addr(addr) +def addr2pubhash(proto,addr): + ap = proto.parse_addr(addr) assert ap,'coin address {!r} could not be parsed'.format(addr) return ap.bytes.hex() -def addr2scriptPubKey(addr): +def addr2scriptPubKey(proto,addr): return { - 'p2pkh': '76a914' + addr2pubhash(addr) + '88ac', - 'p2sh': 'a914' + addr2pubhash(addr) + '87', - 'bech32': g.proto.witness_vernum_hex + '14' + addr2pubhash(addr) + 'p2pkh': '76a914' + addr2pubhash(proto,addr) + '88ac', + 'p2sh': 'a914' + addr2pubhash(proto,addr) + '87', + 'bech32': proto.witness_vernum_hex + '14' + addr2pubhash(proto,addr) }[addr.addr_fmt] -def scriptPubKey2addr(s): +def scriptPubKey2addr(proto,s): if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac': - return g.proto.pubhash2addr(s[6:-4],p2sh=False),'p2pkh' + return proto.pubhash2addr(s[6:-4],p2sh=False),'p2pkh' elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87': - return g.proto.pubhash2addr(s[4:-2],p2sh=True),'p2sh' - elif len(s) == 44 and s[:4] == g.proto.witness_vernum_hex + '14': - return g.proto.pubhash2bech32addr(s[4:]),'bech32' + return proto.pubhash2addr(s[4:-2],p2sh=True),'p2sh' + elif len(s) == 44 and s[:4] == proto.witness_vernum_hex + '14': + return proto.pubhash2bech32addr(s[4:]),'bech32' else: raise NotImplementedError('Unknown scriptPubKey ({})'.format(s)) @@ -123,7 +108,7 @@ class DeserializedTX(dict,MMGenObject): Parse a serialized Bitcoin transaction For checking purposes, additionally reconstructs the raw (unsigned) tx hex from signed tx hex """ - def __init__(self,txhex): + def __init__(self,proto,txhex): def bytes2int(bytes_le): if bytes_le[-1] & 0x80: # sign bit is set @@ -131,7 +116,7 @@ class DeserializedTX(dict,MMGenObject): return int(bytes_le[::-1].hex(),16) def bytes2coin_amt(bytes_le): - return g.proto.coin_amt(bytes2int(bytes_le) * g.proto.coin_amt.min_coin_unit) + return proto.coin_amt(bytes2int(bytes_le) * proto.coin_amt.min_coin_unit) def bshift(n,skip=False,sub_null=False): ret = tx[self.idx:self.idx+n] @@ -193,7 +178,7 @@ class DeserializedTX(dict,MMGenObject): } for i in range(d['num_txouts'])]) for o in d['txouts']: - o['address'] = scriptPubKey2addr(o['scriptPubKey'])[0] + o['address'] = scriptPubKey2addr(proto,o['scriptPubKey'])[0] if has_witness: # https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki @@ -223,1337 +208,1419 @@ class DeserializedTX(dict,MMGenObject): class MMGenTxIO(MMGenListItem): vout = ListItemAttr(int,typeconv=False) - amt = ImmutableAttr(lambda val:g.proto.coin_amt(val),typeconv=False) + amt = ImmutableAttr(None) label = ListItemAttr('TwComment',reassign_ok=True) - mmid = ListItemAttr('MMGenID') - addr = ImmutableAttr('CoinAddr') - confs = ListItemAttr(int,typeconv=True) # confs of type long exist in the wild, so convert + mmid = ListItemAttr('MMGenID',include_proto=True) + addr = ImmutableAttr('CoinAddr',include_proto=True) + confs = ListItemAttr(int) # confs of type long exist in the wild, so convert txid = ListItemAttr('CoinTxID') have_wif = ListItemAttr(bool,typeconv=False,delete_ok=True) + invalid_attrs = {'proto','tw_copy_attrs'} + + def __init__(self,proto,**kwargs): + self.__dict__['proto'] = proto + MMGenListItem.__init__(self,**kwargs) + + class conv_funcs: + def amt(self,value): + return self.proto.coin_amt(value) + class MMGenTxInput(MMGenTxIO): scriptPubKey = ListItemAttr('HexStr') sequence = ListItemAttr(int,typeconv=False) - # required by copy_inputs_from_tw() - copy_attrs = { 'scriptPubKey','vout','amt','label','mmid','addr','confs','txid' } + tw_copy_attrs = { 'scriptPubKey','vout','amt','label','mmid','addr','confs','txid' } class MMGenTxOutput(MMGenTxIO): is_chg = ListItemAttr(bool,typeconv=False) -class MMGenTxInputList(list,MMGenObject): +class MMGenTxIOList(MMGenObject): + + def __init__(self,parent,data=None): + self.parent = parent + if data: + assert isinstance(data,list), 'MMGenTxIOList_check1' + self.data = data + else: + self.data = list() + + def __getitem__(self,val): return self.data.__getitem__(val) + def __setitem__(self,key,val): return self.data.__setitem__(key,val) + def __delitem__(self,val): return self.data.__delitem__(val) + def __contains__(self,val): return self.data.__contains__(val) + def __iter__(self): return self.data.__iter__() + def __len__(self): return self.data.__len__() + def __add__(self,val): return self.data.__add__(val) + def __eq__(self,val): return self.data.__eq__(val) + def append(self,val): return self.data.append(val) + def sort(self,*args,**kwargs): return self.data.sort(*args,**kwargs) + +class MMGenTxInputList(MMGenTxIOList): desc = 'transaction inputs' member_type = 'MMGenTxInput' - def convert_coin(self,verbose=False): - if verbose: - msg(f'{self.desc}:') - for i in self: - setattr(i,'amt',g.proto.coin_amt(i.amt)) - - def check_coin_mismatch(self): - for i in self: - if type(i.amt) != g.proto.coin_amt: - die(2,f'Coin mismatch in transaction: amount {i.amt} not of type {g.proto.coin_amt}!') +# def convert_coin(self,verbose=False): +# if verbose: +# msg(f'{self.desc}:') +# for i in self: +# i.amt = self.parent.proto.coin_amt(i.amt) # Lexicographical Indexing of Transaction Inputs and Outputs # https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki def sort_bip69(self): - from struct import pack - self.sort(key=lambda a: bytes.fromhex(a.txid) + pack('>i',a.vout)) + def sort_func(a): + return ( + bytes.fromhex(a.txid) + + int.to_bytes(a.vout,4,'big') ) + self.sort(key=sort_func) -class MMGenTxOutputList(MMGenTxInputList): +class MMGenTxOutputList(MMGenTxIOList): desc = 'transaction outputs' member_type = 'MMGenTxOutput' def sort_bip69(self): - from struct import pack - self.sort(key=lambda a: pack('>q',a.amt.toSatoshi()) + bytes.fromhex(addr2scriptPubKey(a.addr))) + def sort_func(a): + return ( + int.to_bytes(a.amt.toSatoshi(),8,'big') + + bytes.fromhex(addr2scriptPubKey(self.parent.proto,a.addr)) ) + self.sort(key=sort_func) -class MMGenTX(MMGenObject): +class MMGenTX: - def __new__(cls,*args,**kwargs): - return MMGenObject.__new__(altcoin_subclass(cls,'tx','MMGenTX')) + class Base(MMGenObject): + desc = 'transaction' + hex = '' # raw serialized hex transaction + label = MMGenTxLabel('') + txid = '' + coin_txid = '' + timestamp = '' + blockcount = 0 + coin = None + dcoin = None + locktime = None + chain = None + rel_fee_desc = 'satoshis per byte' + rel_fee_disp = 'satoshis per byte' + non_mmgen_inputs_msg = f""" + This transaction includes inputs with non-{g.proj_name} addresses. When + signing the transaction, private keys for the addresses must be supplied using + the --keys-from-file option. The key file must contain one key per line. + Please note that this transaction cannot be autosigned, as autosigning does + not support the use of key files. - ext = 'rawtx' - raw_ext = 'rawtx' - sig_ext = 'sigtx' - txid_ext = 'txid' - desc = 'transaction' - hexdata_type = 'hex' - fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})' - no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change' - rel_fee_desc = 'satoshis per byte' - rel_fee_disp = 'satoshis per byte' - txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n' - txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n' - txview_ftr_fs = 'Total input: {i} {d}\nTotal output: {o} {d}\nTX fee: {a} {c}{r}\n' - txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n' - usr_fee_prompt = 'Enter transaction fee: ' - fee_is_approximate = False - fn_fee_unit = 'satoshi' - view_sort_orders = ('addr','raw') - dfl_view_sort_order = 'addr' + Non-{g.proj_name} addresses found in inputs: + {{}} + """ - msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)' - msg_low_coin = 'Selected outputs insufficient to fund this transaction ({} {} needed)' - msg_no_change_output = fmt(""" - ERROR: No change address specified. If you wish to create a transaction with - only one output, specify a single output address with no {} amount - """).strip() - msg_non_mmgen_inputs = fmt(f""" - NOTE: This transaction includes non-{g.proj_name} inputs, which makes the signing - process more complicated. When signing the transaction, keys for non-{g.proj_name} - inputs must be supplied using the '--keys-from-file' option. The key file - must contain exactly one key per line. - Selected non-{g.proj_name} inputs: {{}} - """).strip() - - def __init__(self,filename=None,metadata_only=False,caller=None,quiet_open=False,data=None,tw=None): - if data: - assert type(data) is dict, type(data) - self.__dict__ = data - return - self.inputs = MMGenTxInputList() - self.outputs = MMGenTxOutputList() - self.send_amt = g.proto.coin_amt('0') # total amt minus change - self.fee = g.proto.coin_amt('0') - self.hex = '' # raw serialized hex transaction - self.label = MMGenTxLabel('') - self.txid = '' - self.coin_txid = '' - self.timestamp = '' - self.blockcount = 0 - self.chain = None - self.coin = None - self.dcoin = None - self.caller = caller - self.locktime = None - self.tw = tw - - if filename: - from .txfile import MMGenTxFile - MMGenTxFile(self).parse(filename,metadata_only=metadata_only,quiet_open=quiet_open) - if metadata_only: - return - self.check_pubkey_scripts() - self.check_sigs() # marks the tx as signed - - # repeat with sign and send, because coin daemon could be restarted - self.check_correct_chain() - - def write_to_file(self,*args,**kwargs): - from .txfile import MMGenTxFile - MMGenTxFile(self).write(*args,**kwargs) - - def check_correct_chain(self): - bad = self.chain and g.chain and self.chain != g.chain - if bad and hasattr(g.proto,'chain_name'): - bad = self.chain != g.proto.chain_name - if bad: - raise TransactionChainMismatch(f'Transaction is for {self.chain}, but current chain is {g.chain}!') - - def add_output(self,coinaddr,amt,is_chg=None): - self.outputs.append(MMGenTxOutput(addr=coinaddr,amt=amt,is_chg=is_chg)) - - def get_chg_output_idx(self): - ch_ops = [x.is_chg for x in self.outputs] - try: - return ch_ops.index(True) - except ValueError: - return None - - def update_output_amt(self,idx,amt): - o = self.outputs[idx].__dict__ - o['amt'] = amt - self.outputs[idx] = MMGenTxOutput(**o) - - def update_change_output(self,change_amt): - chg_idx = self.get_chg_output_idx() - if change_amt == 0: - msg(self.no_chg_msg) - self.del_output(chg_idx) - else: - self.update_output_amt(chg_idx,g.proto.coin_amt(change_amt)) - - def del_output(self,idx): - self.outputs.pop(idx) - - def sum_outputs(self,exclude=None): - if exclude == None: - olist = self.outputs - else: - olist = self.outputs[:exclude] + self.outputs[exclude+1:] - if not olist: - return g.proto.coin_amt('0') - return g.proto.coin_amt(sum(e.amt for e in olist)) - - def add_mmaddrs_to_outputs(self,ad_w,ad_f): - a = [e.addr for e in self.outputs] - d = ad_w.make_reverse_dict(a) - if ad_f: - d.update(ad_f.make_reverse_dict(a)) - for e in self.outputs: - if e.addr and e.addr in d: - e.mmid,f = d[e.addr] - if f: - e.label = f - - def check_dup_addrs(self,io_str): - assert io_str in ('inputs','outputs') - addrs = [e.addr for e in getattr(self,io_str)] - if len(addrs) != len(set(addrs)): - die(2,f'{addrs}: duplicate address in transaction {io_str}') - - def update_txid(self): - self.txid = MMGenTxID(make_chksum_6(bytes.fromhex(self.hex)).upper()) - - async def create_raw(self): - i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs] - if self.inputs[0].sequence: - i[0]['sequence'] = self.inputs[0].sequence - o = {e.addr:e.amt for e in self.outputs} - self.hex = HexStr(await g.rpc.call('createrawtransaction',i,o)) - self.update_txid() - - def print_contract_addr(self): - pass - - # returns true if comment added or changed - def add_comment(self,infile=None): - if infile: - self.label = MMGenTxLabel(get_data_from_file(infile,'transaction comment')) - else: # get comment from user, or edit existing comment - m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.label)] - if keypress_confirm(m,default_yes=False): - while True: - s = MMGenTxLabel(my_raw_input('Comment: ',insert_txt=self.label)) - if s: - lbl_save = self.label - self.label = s - return (True,False)[lbl_save == self.label] - else: - msg('Invalid comment') - return False - - def edit_comment(self): - return self.add_comment(self) - - def get_fee(self): - return self.sum_inputs() - self.sum_outputs() - - def has_segwit_inputs(self): - return any(i.mmid and i.mmid.mmtype in ('S','B') for i in self.inputs) - - def compare_size_and_estimated_size(self,tx_decoded): - est_vsize = self.estimate_size() - d = tx_decoded - vsize = d['vsize'] if 'vsize' in d else d['size'] - vmsg(f'\nVsize: {vsize} (true) {est_vsize} (estimated)') - ratio = float(est_vsize) / vsize - if not (0.95 < ratio < 1.05): # allow for 5% error - raise BadTxSizeEstimate(fmt(f""" - Estimated transaction vsize is {ratio:1.2f} times the true vsize - Your transaction fee estimates will be inaccurate - Please re-create and re-sign the transaction using the option --vsize-adj={1/ratio:1.2f} - """).strip()) - - # https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending - # 180: uncompressed, 148: compressed - def estimate_size_old(self): - if not self.inputs or not self.outputs: - return None - return len(self.inputs)*180 + len(self.outputs)*34 + 10 - - # https://bitcoincore.org/en/segwit_wallet_dev/ - # vsize: 3 times of the size with original serialization, plus the size with new - # serialization, divide the result by 4 and round up to the next integer. - - # TODO: results differ slightly from actual transaction size - def estimate_size(self): - if not self.inputs or not self.outputs: - return None - - sig_size = 72 # sig in DER format - pubkey_size_uncompressed = 65 - pubkey_size_compressed = 33 - - def get_inputs_size(): - # txid vout [scriptSig size (vInt)] scriptSig ( ) nSeq - isize_common = 32 + 4 + 1 + 4 # txid vout [scriptSig size] nSeq = 41 - input_size = { - 'L': isize_common + sig_size + pubkey_size_uncompressed, # = 180 - 'C': isize_common + sig_size + pubkey_size_compressed, # = 148 - 'S': isize_common + 23, # = 64 - 'B': isize_common + 0 # = 41 - } - ret = sum(input_size[i.mmid.mmtype] for i in self.inputs if i.mmid) - - # We have no way of knowing whether a non-MMGen addr is compressed or uncompressed until - # we see the key, so assume compressed for fee-estimation purposes. If fee estimate is - # off by more than 5%, sign() aborts and user is instructed to use --vsize-adj option - return ret + sum(input_size['C'] for i in self.inputs if not i.mmid) - - def get_outputs_size(): - # output bytes = amt: 8, byte_count: 1+, pk_script - # pk_script bytes: p2pkh: 25, p2sh: 23, bech32: 22 - return sum({'p2pkh':34,'p2sh':32,'bech32':31}[o.addr.addr_fmt] for o in self.outputs) - - # https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki - # The witness is a serialization of all witness data of the transaction. Each txin is - # associated with a witness field. A witness field starts with a var_int to indicate the - # number of stack items for the txin. It is followed by stack items, with each item starts - # with a var_int to indicate the length. Witness data is NOT script. - - # A non-witness program txin MUST be associated with an empty witness field, represented - # by a 0x00. If all txins are not witness program, a transaction's wtxid is equal to its txid. - def get_witness_size(): - if not self.has_segwit_inputs(): - return 0 - wf_size = 1 + 1 + sig_size + 1 + pubkey_size_compressed # vInt vInt sig vInt pubkey = 108 - return sum((1,wf_size)[bool(i.mmid) and i.mmid.mmtype in ('S','B')] for i in self.inputs) - - isize = get_inputs_size() - osize = get_outputs_size() - wsize = get_witness_size() - - # TODO: compute real varInt sizes instead of assuming 1 byte - # old serialization: [nVersion] [vInt][txins][vInt][txouts] [nLockTime] - old_size = 4 + 1 + isize + 1 + osize + 4 - # marker = 0x00, flag = 0x01 - # new serialization: [nVersion][marker][flag][vInt][txins][vInt][txouts][witness][nLockTime] - new_size = 4 + 1 + 1 + 1 + isize + 1 + osize + wsize + 4 \ - if wsize else old_size - - ret = (old_size * 3 + new_size) // 4 - - dmsg('\nData from estimate_size():') - dmsg(' inputs size: {}, outputs size: {}, witness size: {}'.format(isize,osize,wsize)) - dmsg(' size: {}, vsize: {}, old_size: {}'.format(new_size,ret,old_size)) - - return int(ret * float(opt.vsize_adj)) if hasattr(opt,'vsize_adj') and opt.vsize_adj else ret - - # coin-specific fee routines - @property - def relay_fee(self): - kb_fee = g.proto.coin_amt(g.rpc.cached['networkinfo']['relayfee']) - ret = kb_fee * self.estimate_size() // 1024 - vmsg('Relay fee: {} {c}/kB, for transaction: {} {c}'.format(kb_fee,ret,c=g.coin)) - return ret - - # convert absolute BTC fee to satoshis-per-byte using estimated size - def fee_abs2rel(self,abs_fee,to_unit=None): - unit = getattr(g.proto.coin_amt,to_unit or 'min_coin_unit') - return int(abs_fee // unit // self.estimate_size()) - - async def get_rel_fee_from_network(self): - try: - ret = await g.rpc.call('estimatesmartfee',opt.tx_confs,opt.fee_estimate_mode.upper()) - fee_per_kb = ret['feerate'] if 'feerate' in ret else -2 - fe_type = 'estimatesmartfee' - except: - args = () if g.coin=='BCH' and g.rpc.daemon_version >= 190100 else (opt.tx_confs,) - fee_per_kb = await g.rpc.call('estimatefee',*args) - fe_type = 'estimatefee' - - return fee_per_kb,fe_type - - # given tx size, rel fee and units, return absolute fee - def convert_fee_spec(self,tx_size,units,amt,unit): - self.usr_rel_fee = None # TODO - return g.proto.coin_amt(int(amt)*tx_size*getattr(g.proto.coin_amt,units[unit])) \ - if tx_size else None - - # given network fee estimate in BTC/kB, return absolute fee using estimated tx size - def fee_est2abs(self,fee_per_kb,fe_type=None): - tx_size = self.estimate_size() - f = fee_per_kb * opt.tx_fee_adj * tx_size / 1024 - ret = g.proto.coin_amt(f,from_decimal=True) - if opt.verbose: - msg(fmt(f""" - {fe_type.upper()} fee for {opt.tx_confs} confirmations: {fee_per_kb} {g.coin}/kB - TX size (estimated): {tx_size} bytes - Fee adjustment factor: {opt.tx_fee_adj} - Absolute fee (fee_per_kb * adj_factor * tx_size / 1024): {ret} {g.coin} - """).strip()) - return ret - - def convert_and_check_fee(self,tx_fee,desc='Missing description'): - abs_fee = self.process_fee_spec(tx_fee,self.estimate_size()) - if abs_fee == None: # we shouldn't be calling this method if tx size is unknown - raise ValueError( - f'{tx_fee}: cannot convert {self.rel_fee_desc} to {g.coin} because transaction size is unknown') - elif abs_fee == False: - msg(f'{tx_fee!r}: invalid TX fee (not a {g.coin} amount or {self.rel_fee_desc} specification)') - return False - elif abs_fee > g.proto.max_tx_fee: - msg(f'{abs_fee} {g.coin}: {desc} fee too large (maximum fee: {g.proto.max_tx_fee} {g.coin})') - return False - elif abs_fee < self.relay_fee: - msg(f'{abs_fee} {g.coin}: {desc} fee too small (less than relay fee of {self.relay_fee} {g.coin})') - return False - else: - return abs_fee - - # non-coin-specific fee routines - - # given tx size and absolute fee or fee spec, return absolute fee - # relative fee is N+ - def process_fee_spec(self,tx_fee,tx_size): - if g.proto.coin_amt(tx_fee,on_fail='silent'): - return g.proto.coin_amt(tx_fee) - else: - import re - units = {u[0]:u for u in g.proto.coin_amt.units} - pat = re.compile(r'([1-9][0-9]*)({})'.format('|'.join(units))) - if pat.match(tx_fee): - amt,unit = pat.match(tx_fee).groups() - return self.convert_fee_spec(tx_size,units,amt,unit) - return False - - def get_usr_fee_interactive(self,tx_fee=None,desc='Starting'): - abs_fee = None - while True: - if tx_fee: - abs_fee = self.convert_and_check_fee(tx_fee,desc) - if abs_fee: - prompt = '{} TX fee{}: {}{} {} ({} {})\n'.format( - desc, - (f' (after {opt.tx_fee_adj}X adjustment)' - if opt.tx_fee_adj != 1 and desc.startswith('Network-estimated') - else ''), - ('','≈')[self.fee_is_approximate], - abs_fee.hl(), - g.coin, - pink(str(self.fee_abs2rel(abs_fee))), - self.rel_fee_disp) - if opt.yes or keypress_confirm(prompt+'OK?',default_yes=True): - if opt.yes: - msg(prompt) - return abs_fee - tx_fee = my_raw_input(self.usr_fee_prompt) - desc = 'User-selected' - - async def get_fee_from_user(self,have_estimate_fail=[]): - - if opt.tx_fee: - desc = 'User-selected' - start_fee = opt.tx_fee - else: - desc = f'Network-estimated (mode: {opt.fee_estimate_mode.upper()})' - fee_per_kb,fe_type = await self.get_rel_fee_from_network() - - if fee_per_kb < 0: - if not have_estimate_fail: - msg(self.fee_fail_fs.format(c=opt.tx_confs,t=fe_type)) - have_estimate_fail.append(True) - start_fee = None + def __new__(cls,*args,**kwargs): + """ + determine correct protocol and pass the proto to altcoin_subclass(), which returns the + transaction object + """ + assert args == (), f'MMGenTX.Base_chk1: only keyword args allowed in {cls.__name__} initializer' + if 'proto' in kwargs: + return MMGenObject.__new__(altcoin_subclass(cls,kwargs['proto'],'tx')) + elif 'data' in kwargs: + return MMGenObject.__new__(altcoin_subclass(cls,kwargs['data']['proto'],'tx')) + elif 'filename' in kwargs: + from .txfile import MMGenTxFile + tmp_tx = MMGenObject.__new__(cls) + MMGenTxFile(tmp_tx).parse( + infile = kwargs['filename'], + quiet_open = kwargs.get('quiet_open'), + metadata_only = True ) + me = MMGenObject.__new__(altcoin_subclass(cls,tmp_tx.proto,'tx')) + me.proto = tmp_tx.proto + return me + elif cls.__name__ == 'Base' and args == () and kwargs == {}: # allow instantiation of empty Base() + return cls else: - start_fee = self.fee_est2abs(fee_per_kb,fe_type) + raise ValueError( + f"MMGenTX.Base: {cls.__name__} must be instantiated with 'proto','data' or 'filename' keyword") - return self.get_usr_fee_interactive(start_fee,desc=desc) + def __init__(self): + self.inputs = MMGenTxInputList(self) + self.outputs = MMGenTxOutputList(self) + self.name = type(self).__name__ - def delete_attrs(self,desc,attr): - for e in getattr(self,desc): - if hasattr(e,attr): - delattr(e,attr) + @property + def coin(self): + return self.proto.coin - # inputs methods - def copy_inputs_from_tw(self,tw_unspent_data): - self.inputs = MMGenTxInputList() - for d in tw_unspent_data: - t = MMGenTxInput(**{attr:getattr(d,attr) for attr in d.__dict__ if attr in MMGenTxInput.copy_attrs}) - if d.twmmid.type == 'mmgen': - t.mmid = d.twmmid # twmmid -> mmid - self.inputs.append(t) + @property + def dcoin(self): + return self.proto.dcoin - def get_input_sids(self): - return set(e.mmid.sid for e in self.inputs if e.mmid) + def check_correct_chain(self): + if hasattr(self,'rpc'): + if self.chain != self.rpc.chain: + raise TransactionChainMismatch( + f'Transaction is for {self.chain}, but coin daemon chain is {self.rpc.chain}!') - def get_output_sids(self): - return set(e.mmid.sid for e in self.outputs if e.mmid) + def sum_inputs(self): + return sum(e.amt for e in self.inputs) - def sum_inputs(self): - return sum(e.amt for e in self.inputs) + def sum_outputs(self,exclude=None): + if exclude == None: + olist = self.outputs + else: + olist = self.outputs[:exclude] + self.outputs[exclude+1:] + if not olist: + return self.proto.coin_amt('0') + return self.proto.coin_amt(sum(e.amt for e in olist)) - def add_timestamp(self): - self.timestamp = make_timestamp() + def has_segwit_inputs(self): + return any(i.mmid and i.mmid.mmtype in ('S','B') for i in self.inputs) - def get_hex_locktime(self): - return int(bytes.fromhex(self.hex[-8:])[::-1].hex(),16) + def has_segwit_outputs(self): + return any(o.mmid and o.mmid.mmtype in ('S','B') for o in self.outputs) - def set_hex_locktime(self,val): - assert isinstance(val,int),'locktime value not an integer' - self.hex = self.hex[:-8] + bytes.fromhex('{:08x}'.format(val))[::-1].hex() + # https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending + # 180: uncompressed, 148: compressed + def estimate_size_old(self): + if not self.inputs or not self.outputs: + return None + return len(self.inputs)*180 + len(self.outputs)*34 + 10 - def add_blockcount(self): - self.blockcount = g.rpc.blockcount + # https://bitcoincore.org/en/segwit_wallet_dev/ + # vsize: 3 times of the size with original serialization, plus the size with new + # serialization, divide the result by 4 and round up to the next integer. - def get_non_mmaddrs(self,desc): - return {i.addr for i in getattr(self,desc) if not i.mmid} + # TODO: results differ slightly from actual transaction size + def estimate_size(self): + if not self.inputs or not self.outputs: + return None - def mark_raw(self): - self.desc = 'transaction' - self.ext = self.raw_ext + sig_size = 72 # sig in DER format + pubkey_size_uncompressed = 65 + pubkey_size_compressed = 33 - def mark_signed(self): # called ONLY by check_sigs() - self.desc = 'signed transaction' - self.ext = self.sig_ext + def get_inputs_size(): + # txid vout [scriptSig size (vInt)] scriptSig ( ) nSeq + isize_common = 32 + 4 + 1 + 4 # txid vout [scriptSig size] nSeq = 41 + input_size = { + 'L': isize_common + sig_size + pubkey_size_uncompressed, # = 180 + 'C': isize_common + sig_size + pubkey_size_compressed, # = 148 + 'S': isize_common + 23, # = 64 + 'B': isize_common + 0 # = 41 + } + ret = sum(input_size[i.mmid.mmtype] for i in self.inputs if i.mmid) - def marked_signed(self,color=False): - ret = self.desc == 'signed transaction' - return (red,green)[ret](str(ret)) if color else ret + # We have no way of knowing whether a non-MMGen addr is compressed or uncompressed until + # we see the key, so assume compressed for fee-estimation purposes. If fee estimate is + # off by more than 5%, sign() aborts and user is instructed to use --vsize-adj option + return ret + sum(input_size['C'] for i in self.inputs if not i.mmid) - # check that a malicious, compromised or malfunctioning coin daemon hasn't altered hex tx data: - # does not check witness or signature data - def check_hex_tx_matches_mmgen_tx(self,deserial_tx): - m = 'A malicious or malfunctioning coin daemon or other program may have altered your data!' + def get_outputs_size(): + # output bytes = amt: 8, byte_count: 1+, pk_script + # pk_script bytes: p2pkh: 25, p2sh: 23, bech32: 22 + return sum({'p2pkh':34,'p2sh':32,'bech32':31}[o.addr.addr_fmt] for o in self.outputs) - lt = deserial_tx['lock_time'] - if lt != int(self.locktime or 0): - m2 = 'Transaction hex locktime ({}) does not match MMGen transaction locktime ({})\n{}' - raise TxHexMismatch(m2.format(lt,self.locktime,m)) + # https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki + # The witness is a serialization of all witness data of the transaction. Each txin is + # associated with a witness field. A witness field starts with a var_int to indicate the + # number of stack items for the txin. It is followed by stack items, with each item starts + # with a var_int to indicate the length. Witness data is NOT script. - def check_equal(desc,hexio,mmio): - if mmio != hexio: - msg('\nMMGen {}:\n{}'.format(desc,pp_fmt(mmio))) - msg('Hex {}:\n{}'.format(desc,pp_fmt(hexio))) - m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n' - raise TxHexMismatch((m2+m).format(desc.capitalize())) + # A non-witness program txin MUST be associated with an empty witness field, represented + # by a 0x00. If all txins are not witness program, a transaction's wtxid is equal to its txid. + def get_witness_size(): + if not self.has_segwit_inputs(): + return 0 + wf_size = 1 + 1 + sig_size + 1 + pubkey_size_compressed # vInt vInt sig vInt pubkey = 108 + return sum((1,wf_size)[bool(i.mmid) and i.mmid.mmtype in ('S','B')] for i in self.inputs) - seq_hex = [int(i['nSeq'],16) for i in deserial_tx['txins']] - seq_mmgen = [i.sequence or g.max_int for i in self.inputs] - check_equal('sequence numbers',seq_hex,seq_mmgen) + isize = get_inputs_size() + osize = get_outputs_size() + wsize = get_witness_size() - d_hex = sorted((i['txid'],i['vout']) for i in deserial_tx['txins']) - d_mmgen = sorted((i.txid,i.vout) for i in self.inputs) - check_equal('inputs',d_hex,d_mmgen) + # TODO: compute real varInt sizes instead of assuming 1 byte + # old serialization: [nVersion] [vInt][txins][vInt][txouts] [nLockTime] + old_size = 4 + 1 + isize + 1 + osize + 4 + # marker = 0x00, flag = 0x01 + # new serialization: [nVersion][marker][flag][vInt][txins][vInt][txouts][witness][nLockTime] + new_size = 4 + 1 + 1 + 1 + isize + 1 + osize + wsize + 4 \ + if wsize else old_size - d_hex = sorted((o['address'],g.proto.coin_amt(o['amount'])) for o in deserial_tx['txouts']) - d_mmgen = sorted((o.addr,o.amt) for o in self.outputs) - check_equal('outputs',d_hex,d_mmgen) + ret = (old_size * 3 + new_size) // 4 - uh = deserial_tx['unsigned_hex'] - if str(self.txid) != make_chksum_6(bytes.fromhex(uh)).upper(): - raise TxHexMismatch('MMGen TxID ({}) does not match hex transaction data!\n{}'.format(self.txid,m)) + dmsg('\nData from estimate_size():') + dmsg(f' inputs size: {isize}, outputs size: {osize}, witness size: {wsize}') + dmsg(f' size: {new_size}, vsize: {ret}, old_size: {old_size}') - def check_pubkey_scripts(self): - for n,i in enumerate(self.inputs,1): - addr,fmt = scriptPubKey2addr(i.scriptPubKey) - if i.addr != addr: - if fmt != i.addr.addr_fmt: - m = 'Address format of scriptPubKey ({}) does not match that of address ({}) in input #{}' - msg(m.format(fmt,i.addr.addr_fmt,n)) - m = 'ERROR: Address and scriptPubKey of transaction input #{} do not match!' - die(3,(m+'\n {:23}{}'*3).format(n, 'address:',i.addr, - 'scriptPubKey:',i.scriptPubKey, - 'scriptPubKey->address:',addr )) + return int(ret * float(opt.vsize_adj)) if hasattr(opt,'vsize_adj') and opt.vsize_adj else ret - # check signature and witness data - def check_sigs(self,deserial_tx=None): # return False if no sigs, raise exception on error - txins = (deserial_tx or DeserializedTX(self.hex))['txins'] - has_ss = any(ti['scriptSig'] for ti in txins) - has_witness = any('witness' in ti and ti['witness'] for ti in txins) - if not (has_ss or has_witness): + # convert absolute BTC fee to satoshis-per-byte using estimated size + def fee_abs2rel(self,abs_fee,to_unit=None): + unit = getattr(self.proto.coin_amt,to_unit or 'min_coin_unit') + return int(abs_fee // unit // self.estimate_size()) + + def get_fee(self): + return self.sum_inputs() - self.sum_outputs() + + def get_hex_locktime(self): + return int(bytes.fromhex(self.hex[-8:])[::-1].hex(),16) + + def set_hex_locktime(self,val): + assert isinstance(val,int),'locktime value not an integer' + self.hex = self.hex[:-8] + bytes.fromhex('{:08x}'.format(val))[::-1].hex() + + def add_timestamp(self): + self.timestamp = make_timestamp() + + def add_blockcount(self): + self.blockcount = self.rpc.blockcount + + # returns true if comment added or changed + def add_comment(self,infile=None): + if infile: + self.label = MMGenTxLabel(get_data_from_file(infile,'transaction comment')) + else: # get comment from user, or edit existing comment + m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.label)] + if keypress_confirm(m,default_yes=False): + while True: + s = MMGenTxLabel(my_raw_input('Comment: ',insert_txt=self.label)) + if s: + lbl_save = self.label + self.label = s + return (True,False)[lbl_save == self.label] + else: + msg('Invalid comment') + return False + + def get_non_mmaddrs(self,desc): + return {i.addr for i in getattr(self,desc) if not i.mmid} + + def check_non_mmgen_inputs(self,caller,non_mmaddrs=None): + non_mmaddrs = non_mmaddrs or self.get_non_mmaddrs('inputs') + if non_mmaddrs: + fs = fmt(self.non_mmgen_inputs_msg,strip_char='\t') + m = fs.format('\n '.join(non_mmaddrs)) + if caller in ('txdo','txsign'): + if not opt.keys_from_file: + raise UserOptError('ERROR: ' + m) + else: + msg('WARNING: ' + m) + if not (opt.yes or keypress_confirm('Continue?',default_yes=True)): + die(1,'Exiting at user request') + + class New(Base): + usr_fee_prompt = 'Enter transaction fee: ' + fee_is_approximate = False + fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})' + no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change' + msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)' + msg_low_coin = 'Selected outputs insufficient to fund this transaction ({} {} needed)' + msg_no_change_output = fmt(""" + ERROR: No change address specified. If you wish to create a transaction with + only one output, specify a single output address with no {} amount + """).strip() + + def __init__(self,proto,tw=None): # tw required for resolving ERC20 token data + MMGenTX.Base.__init__(self) + self.proto = proto + self.tw = tw + + def get_chg_output_idx(self): + ch_ops = [x.is_chg for x in self.outputs] + try: + return ch_ops.index(True) + except ValueError: + return None + + def del_output(self,idx): + self.outputs.pop(idx) + + def update_output_amt(self,idx,amt): + o = self.outputs[idx]._asdict() + o['amt'] = amt + self.outputs[idx] = MMGenTxOutput(self.proto,**o) + + def add_mmaddrs_to_outputs(self,ad_w,ad_f): + a = [e.addr for e in self.outputs] + d = ad_w.make_reverse_dict(a) + if ad_f: + d.update(ad_f.make_reverse_dict(a)) + for e in self.outputs: + if e.addr and e.addr in d: + e.mmid,f = d[e.addr] + if f: + e.label = f + + def check_dup_addrs(self,io_str): + assert io_str in ('inputs','outputs') + addrs = [e.addr for e in getattr(self,io_str)] + if len(addrs) != len(set(addrs)): + die(2,f'{addrs}: duplicate address in transaction {io_str}') + + # coin-specific fee routines + @property + def relay_fee(self): + kb_fee = self.proto.coin_amt(self.rpc.cached['networkinfo']['relayfee']) + ret = kb_fee * self.estimate_size() // 1024 + vmsg('Relay fee: {} {c}/kB, for transaction: {} {c}'.format(kb_fee,ret,c=self.coin)) + return ret + + async def get_rel_fee_from_network(self): + try: + ret = await self.rpc.call('estimatesmartfee',opt.tx_confs,opt.fee_estimate_mode.upper()) + fee_per_kb = ret['feerate'] if 'feerate' in ret else -2 + fe_type = 'estimatesmartfee' + except: + args = () if self.coin=='BCH' and self.rpc.daemon_version >= 190100 else (opt.tx_confs,) + fee_per_kb = await self.rpc.call('estimatefee',*args) + fe_type = 'estimatefee' + + return fee_per_kb,fe_type + + # given tx size, rel fee and units, return absolute fee + def convert_fee_spec(self,tx_size,units,amt,unit): + self.usr_rel_fee = None # TODO + return self.proto.coin_amt(int(amt)*tx_size*getattr(self.proto.coin_amt,units[unit])) \ + if tx_size else None + + # given network fee estimate in BTC/kB, return absolute fee using estimated tx size + def fee_est2abs(self,fee_per_kb,fe_type=None): + tx_size = self.estimate_size() + f = fee_per_kb * opt.tx_fee_adj * tx_size / 1024 + ret = self.proto.coin_amt(f,from_decimal=True) + if opt.verbose: + msg(fmt(f""" + {fe_type.upper()} fee for {opt.tx_confs} confirmations: {fee_per_kb} {self.coin}/kB + TX size (estimated): {tx_size} bytes + Fee adjustment factor: {opt.tx_fee_adj} + Absolute fee (fee_per_kb * adj_factor * tx_size / 1024): {ret} {self.coin} + """).strip()) + return ret + + def convert_and_check_fee(self,tx_fee,desc='Missing description'): + abs_fee = self.process_fee_spec(tx_fee,self.estimate_size()) + if abs_fee == None: + raise ValueError(f'{tx_fee}: cannot convert {self.rel_fee_desc} to {self.coin}' + + ' because transaction size is unknown') + if abs_fee == False: + err = f'{tx_fee!r}: invalid TX fee (not a {self.coin} amount or {self.rel_fee_desc} specification)' + elif abs_fee > self.proto.max_tx_fee: + err = f'{abs_fee} {self.coin}: {desc} fee too large (maximum fee: {self.proto.max_tx_fee} {self.coin})' + elif abs_fee < self.relay_fee: + err = f'{abs_fee} {self.coin}: {desc} fee too small (less than relay fee of {self.relay_fee} {self.coin})' + else: + return abs_fee + msg(err) return False - fs = "Hex TX has {} scriptSig but input is of type '{}'!" - for n in range(len(txins)): - ti,mmti = txins[n],self.inputs[n] - if ti['scriptSig'] == '' or ( len(ti['scriptSig']) == 46 and # native P2WPKH or P2SH-P2WPKH - ti['scriptSig'][:6] == '16' + g.proto.witness_vernum_hex + '14' ): - assert 'witness' in ti, 'missing witness' - assert type(ti['witness']) == list and len(ti['witness']) == 2, 'malformed witness' - assert len(ti['witness'][1]) == 66, 'incorrect witness pubkey length' - assert mmti.mmid, fs.format('witness-type','non-MMGen') - assert mmti.mmid.mmtype == ('S','B')[ti['scriptSig']==''],( - fs.format('witness-type',mmti.mmid.mmtype)) - else: # non-witness - if mmti.mmid: - assert mmti.mmid.mmtype not in ('S','B'), fs.format('signature in',mmti.mmid.mmtype) - assert not 'witness' in ti, 'non-witness input has witness' - # sig_size 72 (DER format), pubkey_size 'compressed':33, 'uncompressed':65 - assert (200 < len(ti['scriptSig']) < 300), 'malformed scriptSig' # VERY rough check - self.mark_signed() - return True - def has_segwit_outputs(self): - return any(o.mmid and o.mmid.mmtype in ('S','B') for o in self.outputs) + # non-coin-specific fee routines - async def get_status(self,status=False): + # given tx size and absolute fee or fee spec, return absolute fee + # relative fee is N+ + def process_fee_spec(self,tx_fee,tx_size): + if self.proto.coin_amt(tx_fee,on_fail='silent'): + return self.proto.coin_amt(tx_fee) + else: + import re + units = {u[0]:u for u in self.proto.coin_amt.units} + pat = re.compile(r'([1-9][0-9]*)({})'.format('|'.join(units))) + if pat.match(tx_fee): + amt,unit = pat.match(tx_fee).groups() + return self.convert_fee_spec(tx_size,units,amt,unit) + return False - class r(object): + def get_usr_fee_interactive(self,tx_fee=None,desc='Starting'): + abs_fee = None + while True: + if tx_fee: + abs_fee = self.convert_and_check_fee(tx_fee,desc) + if abs_fee: + prompt = '{} TX fee{}: {}{} {} ({} {})\n'.format( + desc, + (f' (after {opt.tx_fee_adj}X adjustment)' + if opt.tx_fee_adj != 1 and desc.startswith('Network-estimated') + else ''), + ('','≈')[self.fee_is_approximate], + abs_fee.hl(), + self.coin, + pink(str(self.fee_abs2rel(abs_fee))), + self.rel_fee_disp) + if opt.yes or keypress_confirm(prompt+'OK?',default_yes=True): + if opt.yes: + msg(prompt) + return abs_fee + tx_fee = my_raw_input(self.usr_fee_prompt) + desc = 'User-selected' + + async def get_fee_from_user(self,have_estimate_fail=[]): + + if opt.tx_fee: + desc = 'User-selected' + start_fee = opt.tx_fee + else: + desc = f'Network-estimated (mode: {opt.fee_estimate_mode.upper()})' + fee_per_kb,fe_type = await self.get_rel_fee_from_network() + + if fee_per_kb < 0: + if not have_estimate_fail: + msg(self.fee_fail_fs.format(c=opt.tx_confs,t=fe_type)) + have_estimate_fail.append(True) + start_fee = None + else: + start_fee = self.fee_est2abs(fee_per_kb,fe_type) + + return self.get_usr_fee_interactive(start_fee,desc=desc) + + def add_output(self,coinaddr,amt,is_chg=None): + self.outputs.append(MMGenTxOutput(self.proto,addr=coinaddr,amt=amt,is_chg=is_chg)) + + def process_cmd_arg(self,arg,ad_f,ad_w): + + def add_output_chk(addr,amt,err_desc): + if not amt and self.get_chg_output_idx() != None: + die(2,'ERROR: More than one change address listed on command line') + if is_mmgen_id(self.proto,addr) or is_coin_addr(self.proto,addr): + coin_addr = ( mmaddr2coinaddr(addr,ad_w,ad_f,self.proto) if is_mmgen_id(self.proto,addr) + else CoinAddr(self.proto,addr) ) + self.add_output(coin_addr,self.proto.coin_amt(amt or '0'),is_chg=not amt) + else: + die(2,f'{addr}: invalid {err_desc} {{!r}}'.format(f'{addr},{amt}' if amt else addr)) + + if ',' in arg: + addr,amt = arg.split(',',1) + add_output_chk(addr,amt,'coin argument in command-line argument') + else: + add_output_chk(arg,None,'command-line argument') + + async def get_cmdline_input_addrs(self): + # Bitcoin full node, call doesn't go to the network, so just call listunspent with addrs=[] + return [] + + def process_cmd_args(self,cmd_args,ad_f,ad_w): + + for a in cmd_args: + self.process_cmd_arg(a,ad_f,ad_w) + + if self.get_chg_output_idx() == None: + die(2,( 'ERROR: No change output specified', + self.msg_no_change_output.format(self.dcoin))[len(self.outputs) == 1]) + + if not self.rpc.info('segwit_is_active') and self.has_segwit_outputs(): + rdie(2,f'{g.proj_name} Segwit address requested on the command line, ' + + 'but Segwit is not active on this chain') + + if not self.outputs: + die(2,'At least one output must be specified on the command line') + + async def get_outputs_from_cmdline(self,cmd_args): + from .addr import AddrList,AddrData,TwAddrData + addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext] + cmd_args = set(cmd_args) - set(addrfiles) + + ad_f = AddrData(self.proto) + for a in addrfiles: + check_infile(a) + ad_f.add(AddrList(self.proto,a)) + + ad_w = await TwAddrData(self.proto,wallet=self.tw) + + self.process_cmd_args(cmd_args,ad_f,ad_w) + + self.add_mmaddrs_to_outputs(ad_w,ad_f) + self.check_dup_addrs('outputs') + + # inputs methods + def select_unspent(self,unspent): + prompt = 'Enter a range or space-separated list of outputs to spend: ' + while True: + reply = my_raw_input(prompt).strip() + if reply: + selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return') + if selected: + if selected[-1] <= len(unspent): + return selected + msg('Unspent output number must be <= {}'.format(len(unspent))) + + def select_unspent_cmdline(self,unspent): + + def idx2num(idx): + uo = unspent[idx] + mmid_disp = f' ({uo.twmmid})' if uo.twmmid.type == 'mmgen' else '' + msg(f'Adding input: {idx + 1} {uo.addr}{mmid_disp}') + return idx + 1 + + def get_uo_nums(): + for addr in opt.inputs.split(','): + if is_mmgen_id(self.proto,addr): + attr = 'twmmid' + elif is_coin_addr(self.proto,addr): + attr = 'addr' + else: + die(1,f'{addr!r}: not an MMGen ID or {self.coin} address') + + found = False + for idx in range(len(unspent)): + if getattr(unspent[idx],attr) == addr: + yield idx2num(idx) + found = True + + if not found: + die(1,f'{addr!r}: address not found in tracking wallet') + + return set(get_uo_nums()) # silently discard duplicates + + # we don't know fee yet, so perform preliminary check with fee == 0 + async def precheck_sufficient_funds(self,inputs_sum,sel_unspent): + if self.twuo.total < self.send_amt: + msg(self.msg_wallet_low_coin.format(self.send_amt-inputs_sum,self.dcoin)) + return False + if inputs_sum < self.send_amt: + msg(self.msg_low_coin.format(self.send_amt-inputs_sum,self.dcoin)) + return False + return True + + def copy_inputs_from_tw(self,tw_unspent_data): + def gen_inputs(): + for d in tw_unspent_data: + i = MMGenTxInput( + self.proto, + **{attr:getattr(d,attr) for attr in d.__dict__ if attr in MMGenTxInput.tw_copy_attrs} ) + if d.twmmid.type == 'mmgen': + i.mmid = d.twmmid # twmmid -> mmid + yield i + self.inputs = MMGenTxInputList(self,list(gen_inputs())) + + async def get_change_amt(self): + return self.sum_inputs() - self.send_amt - self.fee + + def final_inputs_ok_msg(self,change_amt): + return f'Transaction produces {self.proto.coin_amt(change_amt).hl()} {self.coin} in change' + + def warn_insufficient_chg(self,change_amt): + msg(self.msg_low_coin.format(self.proto.coin_amt(-change_amt).hl(),self.coin)) + + async def get_inputs_from_user(self): + + while True: + us_f = self.select_unspent_cmdline if opt.inputs else self.select_unspent + sel_nums = us_f(self.twuo.unspent) + + msg(f'Selected output{suf(sel_nums)}: {{}}'.format(' '.join(str(n) for n in sel_nums))) + sel_unspent = self.twuo.MMGenTwOutputList([self.twuo.unspent[i-1] for i in sel_nums]) + + inputs_sum = sum(s.amt for s in sel_unspent) + if not await self.precheck_sufficient_funds(inputs_sum,sel_unspent): + continue + + self.copy_inputs_from_tw(sel_unspent) # makes self.inputs + + self.fee = await self.get_fee_from_user() + + change_amt = await self.get_change_amt() + + if change_amt >= 0: + p = self.final_inputs_ok_msg(change_amt) + if opt.yes or keypress_confirm(p+'. OK?',default_yes=True): + if opt.yes: + msg(p) + return change_amt + else: + self.warn_insufficient_chg(change_amt) + + def update_change_output(self,change_amt): + chg_idx = self.get_chg_output_idx() + if change_amt == 0: + msg(self.no_chg_msg) + self.del_output(chg_idx) + else: + self.update_output_amt(chg_idx,self.proto.coin_amt(change_amt)) + + def update_send_amt(self,change_amt): + if not self.send_amt: + self.send_amt = change_amt + + def check_fee(self): + fee = self.sum_inputs() - self.sum_outputs() + if fee > self.proto.max_tx_fee: + c = self.proto.coin + raise MaxFeeExceeded(f'Transaction fee of {fee} {c} too high! (> {self.proto.max_tx_fee} {c})') + + def update_txid(self): + self.txid = MMGenTxID(make_chksum_6(bytes.fromhex(self.hex)).upper()) + + async def create_raw(self): + i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs] + if self.inputs[0].sequence: + i[0]['sequence'] = self.inputs[0].sequence + o = {e.addr:e.amt for e in self.outputs} + self.hex = HexStr(await self.rpc.call('createrawtransaction',i,o)) + self.update_txid() + + async def create(self,cmd_args,locktime,do_info=False,caller='txcreate'): + + assert isinstance(locktime,int),'locktime must be of type int' + + from .tw import TwUnspentOutputs + + if opt.comment_file: + self.add_comment(opt.comment_file) + + twuo_addrs = await self.get_cmdline_input_addrs() + + self.twuo = await TwUnspentOutputs(self.proto,minconf=opt.minconf,addrs=twuo_addrs) + await self.twuo.get_unspent_data() + + if not do_info: + await self.get_outputs_from_cmdline(cmd_args) + + do_license_msg() + + if not opt.inputs: + await self.twuo.view_and_sort(self) + + self.twuo.display_total() + + if do_info: + del self.twuo.wallet + sys.exit(0) + + self.send_amt = self.sum_outputs() + + msg_r('Total amount to spend: ') + msg(f'{self.send_amt.hl()} {self.dcoin}' if self.send_amt else 'Unknown') + + change_amt = await self.get_inputs_from_user() + + self.check_non_mmgen_inputs(caller) + + self.update_change_output(change_amt) + self.update_send_amt(change_amt) + + if self.proto.base_proto == 'Bitcoin': + self.inputs.sort_bip69() + self.outputs.sort_bip69() + # do this only after inputs are sorted + if opt.rbf: + self.inputs[0].sequence = g.max_int - 2 # handles the nLockTime case too + elif locktime: + self.inputs[0].sequence = g.max_int - 1 + + if not opt.yes: + self.add_comment() # edits an existing comment + + await self.create_raw() # creates self.hex, self.txid + + if self.proto.base_proto == 'Bitcoin' and locktime: + msg(f'Setting nLockTime to {strfmt_locktime(locktime)}!') + self.set_hex_locktime(locktime) + self.update_txid() + self.locktime = locktime + + self.add_timestamp() + self.add_blockcount() + self.chain = self.proto.chain_name + self.check_fee() + + qmsg('Transaction successfully created') + + new = MMGenTX.Unsigned(data=self.__dict__) + + if not opt.yes: + new.view_with_prompt('View transaction details?') + + del new.twuo.wallet + return new + + class Completed(Base): + """ + signed or unsigned transaction with associated file + """ + fn_fee_unit = 'satoshi' + view_sort_orders = ('addr','raw') + dfl_view_sort_order = 'addr' + txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n' + txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n' + txview_ftr_fs = 'Total input: {i} {d}\nTotal output: {o} {d}\nTX fee: {a} {c}{r}\n' + txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n' + parsed_hex = None + + def __init__(self,filename=None,quiet_open=False,data=None): + MMGenTX.Base.__init__(self) + if data: + assert filename is None, 'MMGenTX.Completed_chk1' + assert type(data) is dict, 'MMGenTX.Completed_chk2' + self.__dict__ = data + return + elif filename: + assert data is None, 'MMGenTX.Completed_chk3' + from .txfile import MMGenTxFile + MMGenTxFile(self).parse(filename,quiet_open=quiet_open) + self.check_pubkey_scripts() + + # repeat with sign and send, because coin daemon could be restarted + self.check_correct_chain() + + # check signature and witness data + def check_sigs(self): # return False if no sigs, raise exception on error + txins = (self.parsed_hex or DeserializedTX(self.proto,self.hex))['txins'] + has_ss = any(ti['scriptSig'] for ti in txins) + has_witness = any('witness' in ti and ti['witness'] for ti in txins) + if not (has_ss or has_witness): + return False + fs = "Hex TX has {} scriptSig but input is of type '{}'!" + for n in range(len(txins)): + ti,mmti = txins[n],self.inputs[n] + if ti['scriptSig'] == '' or ( len(ti['scriptSig']) == 46 and # native P2WPKH or P2SH-P2WPKH + ti['scriptSig'][:6] == '16' + self.proto.witness_vernum_hex + '14' ): + assert 'witness' in ti, 'missing witness' + assert type(ti['witness']) == list and len(ti['witness']) == 2, 'malformed witness' + assert len(ti['witness'][1]) == 66, 'incorrect witness pubkey length' + assert mmti.mmid, fs.format('witness-type','non-MMGen') + assert mmti.mmid.mmtype == ('S','B')[ti['scriptSig']==''],( + fs.format('witness-type',mmti.mmid.mmtype)) + else: # non-witness + if mmti.mmid: + assert mmti.mmid.mmtype not in ('S','B'), fs.format('signature in',mmti.mmid.mmtype) + assert not 'witness' in ti, 'non-witness input has witness' + # sig_size 72 (DER format), pubkey_size 'compressed':33, 'uncompressed':65 + assert (200 < len(ti['scriptSig']) < 300), 'malformed scriptSig' # VERY rough check + return True + + def check_pubkey_scripts(self): + for n,i in enumerate(self.inputs,1): + addr,fmt = scriptPubKey2addr(self.proto,i.scriptPubKey) + if i.addr != addr: + if fmt != i.addr.addr_fmt: + m = 'Address format of scriptPubKey ({}) does not match that of address ({}) in input #{}' + msg(m.format(fmt,i.addr.addr_fmt,n)) + m = 'ERROR: Address and scriptPubKey of transaction input #{} do not match!' + die(3,(m+'\n {:23}{}'*3).format(n, 'address:',i.addr, + 'scriptPubKey:',i.scriptPubKey, + 'scriptPubKey->address:',addr )) + +# def is_replaceable_from_rpc(self): +# dec_tx = await self.rpc.call('decoderawtransaction',self.hex) +# return None < dec_tx['vin'][0]['sequence'] <= g.max_int - 2 + + def is_replaceable(self): + return self.inputs[0].sequence == g.max_int - 2 + + def check_txfile_hex_data(self): + self.hex = HexStr(self.hex,on_fail='raise') + + def parse_txfile_hex_data(self): pass - async def is_in_wallet(): - try: ret = await g.rpc.call('gettransaction',self.coin_txid) - except: return False - if 'confirmations' in ret and ret['confirmations'] > 0: - r.confs = ret['confirmations'] - return True + def write_to_file(self,*args,**kwargs): + from .txfile import MMGenTxFile + MMGenTxFile(self).write(*args,**kwargs) + + def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse,sort): + + if sort not in self.view_sort_orders: + die(1,f'{sort!r}: invalid transaction view sort order. Valid options: {{}}'.format( + ','.join(self.view_sort_orders) )) + + def format_io(desc): + io = getattr(self,desc) + is_input = desc == 'inputs' + yield desc.capitalize() + ':\n' + enl + addr_w = max(len(e.addr) for e in io) + confs_per_day = 60*60*24 // self.proto.avg_bdi + io_sorted = { + # prepend '/' (sorts before '0') to ensure non-MMGen addrs are displayed first + 'addr': lambda: sorted(io,key=lambda o: o.mmid.sort_key if o.mmid else '/'+o.addr), + 'raw': lambda: io + }[sort] + for n,e in enumerate(io_sorted()): + if is_input and blockcount: + confs = e.confs + blockcount - self.blockcount + days = int(confs // confs_per_day) + if e.mmid: + mmid_fmt = e.mmid.fmt( + width=max_mmwid, + encl='()', + color=True, + append_chars=('',' (chg)')[bool(not is_input and e.is_chg and terse)], + append_color='green') + else: + mmid_fmt = MMGenID.fmtc(nonmm_str,width=max_mmwid,color=True) + if terse: + yield '{:3} {} {} {} {}\n'.format( + n+1, + e.addr.fmt(color=True,width=addr_w), + mmid_fmt, + e.amt.hl(), + self.dcoin ) + else: + def gen(): + if is_input: + yield (n+1, 'tx,vout:', e.txid + ',' + str(e.vout)) + yield ('', 'address:', e.addr.fmt(color=True,width=addr_w) + ' ' + mmid_fmt) + else: + yield (n+1, 'address:', e.addr.fmt(color=True,width=addr_w) + ' ' + mmid_fmt) + if e.label: + yield ('', 'comment:', e.label.hl()) + yield ('', 'amount:', e.amt.hl() + ' ' + self.dcoin) + if is_input and blockcount: + yield ('', 'confirmations:', f'{confs} (around {days} days)') + if not is_input and e.is_chg: + yield ('', 'change:', green('True')) + yield '\n'.join('{:>3} {:<8} {}'.format(*d) for d in gen()) + '\n\n' + + return ( + 'Displaying inputs and outputs in {} sort order'.format({'raw':'raw','addr':'address'}[sort]) + + ('\n\n','\n')[terse] + + ''.join(format_io('inputs')) + + ''.join(format_io('outputs')) ) + + def format_view_rel_fee(self,terse): + return ' ({} {})\n'.format( + pink(str(self.fee_abs2rel(self.get_fee()))), + self.rel_fee_disp) + + def format_view_abs_fee(self): + return self.proto.coin_amt(self.get_fee()).hl() + + def format_view_verbose_footer(self): + tsize = len(self.hex)//2 if self.hex else 'unknown' + out = f'Transaction size: Vsize {self.estimate_size()} (estimated), Total {tsize}' + if self.name == 'Signed': + wsize = DeserializedTX(self.proto,self.hex)['witness_size'] + out += f', Base {tsize-wsize}, Witness {wsize}' + return out + '\n' + + def format_view(self,terse=False,sort=dfl_view_sort_order): + blockcount = None + if self.proto.base_coin != 'ETH': + try: + blockcount = self.rpc.blockcount + except: + pass + + def get_max_mmwid(io): + if io == self.inputs: + sel_f = lambda o: len(o.mmid) + 2 # len('()') + else: + sel_f = lambda o: len(o.mmid) + (2,8)[bool(o.is_chg)] # + len(' (chg)') + return max(max([sel_f(o) for o in io if o.mmid] or [0]),len(nonmm_str)) + + nonmm_str = f'(non-{g.proj_name} address)' + max_mmwid = max(get_max_mmwid(self.inputs),get_max_mmwid(self.outputs)) + + def gen_view(): + yield (self.txview_hdr_fs_short if terse else self.txview_hdr_fs).format( + i = self.txid.hl(), + a = self.send_amt.hl(), + c = self.dcoin, + t = self.timestamp, + r = (red('False'),green('True'))[self.is_replaceable()], + s = (red('False'),green('True'))[self.name == 'Signed'], + l = (green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)] ) + + if self.chain != 'mainnet': # if mainnet has a coin-specific name, display it + yield green(f'Chain: {self.chain.upper()}') + '\n' + + if self.coin_txid: + yield f'{self.coin} TxID: {self.coin_txid.hl()}\n' + + enl = ('\n','')[bool(terse)] + yield enl + + if self.label: + yield f'Comment: {self.label.hl()}\n{enl}' + + yield self.format_view_body(blockcount,nonmm_str,max_mmwid,enl,terse=terse,sort=sort) + + yield (self.txview_ftr_fs_short if terse else self.txview_ftr_fs).format( + i = self.sum_inputs().hl(), + o = self.sum_outputs().hl(), + a = self.format_view_abs_fee(), + r = self.format_view_rel_fee(terse), + d = self.dcoin, + c = self.coin ) + + if opt.verbose: + yield self.format_view_verbose_footer() + + return ''.join(gen_view()) # TX label might contain non-ascii chars + + def view_with_prompt(self,prompt='',pause=True): + prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view: ' + from .term import get_char + ok_chars = 'YyNnVvTt' + while True: + reply = get_char(prompt,immed_chars=ok_chars).strip('\n\r') + msg('') + if reply == '' or reply in 'Nn': + break + elif reply in 'YyVvTt': + self.view(pager=reply in 'Vv',terse=reply in 'Tt',pause=pause) + break + else: + msg('Invalid reply') + + def view(self,pager=False,pause=True,terse=False): + o = self.format_view(terse=terse) + if pager: + do_pager(o) else: - return False + msg_r(o) + from .term import get_char + if pause: + get_char('Press any key to continue: ') + msg('') - async def is_in_utxos(): - try: return 'txid' in await g.rpc.call('getrawtransaction',self.coin_txid,True) - except: return False + class Unsigned(Completed): + desc = 'unsigned transaction' + ext = 'rawtx' - async def is_in_mempool(): - try: return 'height' in await g.rpc.call('getmempoolentry',self.coin_txid) - except: return False + def __init__(self,*args,**kwargs): + super().__init__(*args,**kwargs) + if self.check_sigs(): + die(1,'Transaction is signed!') + + def delete_attrs(self,desc,attr): + for e in getattr(self,desc): + if hasattr(e,attr): + delattr(e,attr) + + def get_input_sids(self): + return set(e.mmid.sid for e in self.inputs if e.mmid) + + def get_output_sids(self): + return set(e.mmid.sid for e in self.outputs if e.mmid) + + async def sign(self,tx_num_str,keys): # return signed object or False; don't exit or raise exception - async def is_replaced(): - if await is_in_mempool(): - return False try: - ret = await g.rpc.call('gettransaction',self.coin_txid) - except: + self.check_correct_chain() + except TransactionChainMismatch: return False - else: - if 'bip125-replaceable' in ret and 'confirmations' in ret and ret['confirmations'] <= 0: - r.replacing_confs = -ret['confirmations'] - r.replacing_txs = ret['walletconflicts'] + + if (self.has_segwit_inputs() or self.has_segwit_outputs()) and not self.proto.cap('segwit'): + ymsg(f"TX has Segwit inputs or outputs, but {self.coin} doesn't support Segwit!") + return False + + self.check_pubkey_scripts() + + qmsg(f'Passing {len(keys)} key{suf(keys)} to {self.proto.daemon_name}') + + if self.has_segwit_inputs(): + from .addr import KeyGenerator,AddrGenerator + kg = KeyGenerator(self.proto,'std') + ag = AddrGenerator(self.proto,'segwit') + keydict = MMGenDict([(d.addr,d.sec) for d in keys]) + + sig_data = [] + for d in self.inputs: + e = {k:getattr(d,k) for k in ('txid','vout','scriptPubKey','amt')} + e['amount'] = e['amt'] + del e['amt'] + if d.mmid and d.mmid.mmtype == 'S': + e['redeemScript'] = ag.to_segwit_redeem_script(kg.to_pubhex(keydict[d.addr])) + sig_data.append(e) + + msg_r(f'Signing transaction{tx_num_str}...') + wifs = [d.sec.wif for d in keys] + + try: + args = ( + ('signrawtransaction', self.hex,sig_data,wifs,self.proto.sighash_type), + ('signrawtransactionwithkey',self.hex,wifs,sig_data,self.proto.sighash_type) + )['sign_with_key' in self.rpc.caps] + ret = await self.rpc.call(*args) + except Exception as e: + msg(yellow(( + e.args[0], + 'This is not the BCH chain.\nRe-run the script without the --coin=bch option.' + )['Invalid sighash param' in e.args[0]])) + return False + + try: + self.hex = HexStr(ret['hex']) + self.parsed_hex = dtx = DeserializedTX(self.proto,self.hex) + new = MMGenTX.Signed(data=self.__dict__) + tx_decoded = await self.rpc.call('decoderawtransaction',ret['hex']) + new.compare_size_and_estimated_size(tx_decoded) + new.check_hex_tx_matches_mmgen_tx(dtx) + new.coin_txid = CoinTxID(dtx['txid'],on_fail='raise') + if not new.coin_txid == tx_decoded['txid']: + raise BadMMGenTxID('txid mismatch (after signing)') + msg('OK') + return new + except Exception as e: + try: m = '{}'.format(e.args[0]) + except: m = repr(e.args[0]) + msg('\n'+yellow(m)) + if g.traceback: + import traceback + ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info()))) + return False + + class Signed(Completed): + desc = 'signed transaction' + ext = 'sigtx' + + def __init__(self,*args,**kwargs): + if 'tw' in kwargs: + self.tw = kwargs['tw'] + del kwargs['tw'] + super().__init__(*args,**kwargs) + if not self.check_sigs(): + die(1,'Transaction is not signed!') + + # check that a malicious, compromised or malfunctioning coin daemon hasn't altered hex tx data: + # does not check witness or signature data + def check_hex_tx_matches_mmgen_tx(self,dtx): + m = 'A malicious or malfunctioning coin daemon or other program may have altered your data!' + + lt = dtx['lock_time'] + if lt != int(self.locktime or 0): + m2 = 'Transaction hex nLockTime ({}) does not match MMGen transaction nLockTime ({})\n{}' + raise TxHexMismatch(m2.format(lt,self.locktime,m)) + + def check_equal(desc,hexio,mmio): + if mmio != hexio: + msg('\nMMGen {}:\n{}'.format(desc,pp_fmt(mmio))) + msg('Hex {}:\n{}'.format(desc,pp_fmt(hexio))) + m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n' + raise TxHexMismatch((m2+m).format(desc.capitalize())) + + seq_hex = [int(i['nSeq'],16) for i in dtx['txins']] + seq_mmgen = [i.sequence or g.max_int for i in self.inputs] + check_equal('sequence numbers',seq_hex,seq_mmgen) + + d_hex = sorted((i['txid'],i['vout']) for i in dtx['txins']) + d_mmgen = sorted((i.txid,i.vout) for i in self.inputs) + check_equal('inputs',d_hex,d_mmgen) + + d_hex = sorted((o['address'],self.proto.coin_amt(o['amount'])) for o in dtx['txouts']) + d_mmgen = sorted((o.addr,o.amt) for o in self.outputs) + check_equal('outputs',d_hex,d_mmgen) + + uh = dtx['unsigned_hex'] + if str(self.txid) != make_chksum_6(bytes.fromhex(uh)).upper(): + raise TxHexMismatch('MMGen TxID ({}) does not match hex transaction data!\n{}'.format(self.txid,m)) + + def compare_size_and_estimated_size(self,tx_decoded): + est_vsize = self.estimate_size() + d = tx_decoded + vsize = d['vsize'] if 'vsize' in d else d['size'] + vmsg(f'\nVsize: {vsize} (true) {est_vsize} (estimated)') + ratio = float(est_vsize) / vsize + if not (0.95 < ratio < 1.05): # allow for 5% error + raise BadTxSizeEstimate(fmt(f""" + Estimated transaction vsize is {ratio:1.2f} times the true vsize + Your transaction fee estimates will be inaccurate + Please re-create and re-sign the transaction using the option --vsize-adj={1/ratio:1.2f} + """).strip()) + + async def get_status(self,status=False): + + class r(object): + pass + + async def is_in_wallet(): + try: ret = await self.rpc.call('gettransaction',self.coin_txid) + except: return False + if ret.get('confirmations',0) > 0: + r.confs = ret['confirmations'] return True else: return False - if await is_in_mempool(): - if status: - d = await g.rpc.call('gettransaction',self.coin_txid) - brs = 'bip125-replaceable' - rep = '{}replaceable'.format(('NOT ','')[brs in d and d[brs]=='yes']) - t = d['timereceived'] - m = 'Sent {} ({} h/m/s ago)' - b = m.format(time.strftime('%c',time.gmtime(t)),secs_to_dhms(int(time.time()-t))) - if opt.quiet: - msg('Transaction is in mempool') + async def is_in_utxos(): + try: return 'txid' in await self.rpc.call('getrawtransaction',self.coin_txid,True) + except: return False + + async def is_in_mempool(): + try: return 'height' in await self.rpc.call('getmempoolentry',self.coin_txid) + except: return False + + async def is_replaced(): + if await is_in_mempool(): + return False + try: + ret = await self.rpc.call('gettransaction',self.coin_txid) + except: + return False else: - msg('TX status: in mempool, {}\n{}'.format(rep,b)) - else: - msg('Warning: transaction is in mempool!') - elif await is_in_wallet(): - die(0,'Transaction has {} confirmation{}'.format(r.confs,suf(r.confs))) - elif await is_in_utxos(): - die(2,red('ERROR: transaction is in the blockchain (but not in the tracking wallet)!')) - elif await is_replaced(): - msg('Transaction has been replaced\nReplacement transaction ' + ( - f'has {r.replacing_confs} confirmation{suf(r.replacing_confs)}' - if r.replacing_confs else - 'is in mempool' )) - if not opt.quiet: - msg('Replacing transactions:') - d = [] - for txid in r.replacing_txs: - try: d.append(await g.rpc.call('getmempoolentry',txid)) - except: d.append({}) - for txid,mp_entry in zip(r.replacing_txs,d): - msg(f' {txid}' + ('',' in mempool')['height' in mp_entry]) - die(0,'') + if 'bip125-replaceable' in ret and ret.get('confirmations',1) <= 0: + r.replacing_confs = -ret['confirmations'] + r.replacing_txs = ret['walletconflicts'] + return True + else: + return False - def confirm_send(self): - m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)] - m2 = 'broadcast this transaction to the {} network'.format(g.chain.upper()) - m3 = ('YES, I REALLY WANT TO DO THIS','YES')[bool(opt.quiet or opt.yes)] - confirm_or_raise(m1,m2,m3) - msg('Sending transaction') - - async def send(self,prompt_user=True,exit_on_fail=False): - if not self.marked_signed(): - die(1,'Transaction is not signed!') - - self.check_correct_chain() - - self.check_pubkey_scripts() - - self.check_hex_tx_matches_mmgen_tx(DeserializedTX(self.hex)) - - if self.has_segwit_outputs() and not segwit_is_active() and not g.bogus_send: - m = 'Transaction has MMGen Segwit outputs, but this blockchain does not support Segwit' - die(2,m+' at the current height') - - if self.get_fee() > g.proto.max_tx_fee: - die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format( - self.get_fee(), - g.proto.name, - g.proto.max_tx_fee, - g.proto.coin )) - - await self.get_status() - - if prompt_user: - self.confirm_send() - - if g.bogus_send: - ret = None - else: - try: - ret = await g.rpc.call('sendrawtransaction',self.hex) - except Exception as e: - ret = False - - if ret == False: - errmsg = e - if 'Signature must use SIGHASH_FORKID' in errmsg: - m = 'The Aug. 1 2017 UAHF has activated on this chain.' - m += "\nRe-run the script with the --coin=bch option." - elif 'Illegal use of SIGHASH_FORKID' in errmsg: - m = 'The Aug. 1 2017 UAHF is not yet active on this chain.' - m += "\nRe-run the script without the --coin=bch option." - elif '64: non-final' in errmsg: - m2 = "Transaction with locktime '{}' can't be included in this block!" - m = m2.format(strfmt_locktime(self.get_hex_locktime())) - else: - m = errmsg - msg(yellow(m)) - msg(red('Send of MMGen transaction {} failed'.format(self.txid))) - if exit_on_fail: - sys.exit(1) - return False - else: - if g.bogus_send: - m = 'BOGUS transaction NOT sent: {}' - else: - assert ret == self.coin_txid, 'txid mismatch (after sending)' - m = 'Transaction sent: {}' - self.desc = 'sent transaction' - msg(m.format(self.coin_txid.hl())) - self.add_timestamp() - self.add_blockcount() - return True - - def view_with_prompt(self,prompt='',pause=True): - prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view: ' - from .term import get_char - ok_chars = 'YyNnVvTt' - while True: - reply = get_char(prompt,immed_chars=ok_chars).strip('\n\r') - msg('') - if reply == '' or reply in 'Nn': - break - elif reply in 'YyVvTt': - self.view(pager=reply in 'Vv',terse=reply in 'Tt',pause=pause) - break - else: - msg('Invalid reply') - - def view(self,pager=False,pause=True,terse=False): - o = self.format_view(terse=terse) - if pager: - do_pager(o) - else: - msg_r(o) - from .term import get_char - if pause: - get_char('Press any key to continue: ') - msg('') - -# def is_replaceable_from_rpc(self): -# dec_tx = await g.rpc.call('decoderawtransaction',self.hex) -# return None < dec_tx['vin'][0]['sequence'] <= g.max_int - 2 - - def is_replaceable(self): - return self.inputs[0].sequence == g.max_int - 2 - - def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse,sort): - - if sort not in self.view_sort_orders: - die(1,f'{sort!r}: invalid transaction view sort order. Valid options: {{}}'.format( - ','.join(self.view_sort_orders) )) - - def format_io(desc): - io = getattr(self,desc) - is_input = desc == 'inputs' - yield desc.capitalize() + ':\n' + enl - addr_w = max(len(e.addr) for e in io) - confs_per_day = 60*60*24 // g.proto.avg_bdi - io_sorted = { - # prepend '/' (sorts before '0') to ensure non-MMGen addrs are displayed first - 'addr': lambda: sorted(io,key=lambda o: o.mmid.sort_key if o.mmid else '/'+o.addr), - 'raw': lambda: io - }[sort] - for n,e in enumerate(io_sorted()): - if is_input and blockcount: - confs = e.confs + blockcount - self.blockcount - days = int(confs // confs_per_day) - if e.mmid: - mmid_fmt = e.mmid.fmt( - width=max_mmwid, - encl='()', - color=True, - append_chars=('',' (chg)')[bool(not is_input and e.is_chg and terse)], - append_color='green') + if await is_in_mempool(): + if status: + d = await self.rpc.call('gettransaction',self.coin_txid) + rep = ('' if d.get('bip125-replaceable') == 'yes' else 'NOT ') + 'replaceable' + t = d['timereceived'] + if opt.quiet: + msg('Transaction is in mempool') + else: + msg(f'TX status: in mempool, {rep}') + msg('Sent {} ({} h/m/s ago)'.format( + time.strftime('%c',time.gmtime(t)), + secs_to_dhms(int(time.time()-t))) ) else: - mmid_fmt = MMGenID.fmtc(nonmm_str,width=max_mmwid,color=True) - if terse: - yield '{:3} {} {} {} {}\n'.format( - n+1, - e.addr.fmt(color=True,width=addr_w), - mmid_fmt, - e.amt.hl(), - g.dcoin ) - else: - def gen(): - if is_input: - yield (n+1, 'tx,vout:', e.txid + ',' + str(e.vout)) - yield ('', 'address:', e.addr.fmt(color=True,width=addr_w) + ' ' + mmid_fmt) - else: - yield (n+1, 'address:', e.addr.fmt(color=True,width=addr_w) + ' ' + mmid_fmt) - if e.label: - yield ('', 'comment:', e.label.hl()) - yield ('', 'amount:', e.amt.hl() + ' ' + g.dcoin) - if is_input and blockcount: - yield ('', 'confirmations:', f'{confs} (around {days} days)') - if not is_input and e.is_chg: - yield ('', 'change:', green('True')) - yield '\n'.join('{:>3} {:<8} {}'.format(*d) for d in gen()) + '\n\n' + msg('Warning: transaction is in mempool!') + elif await is_in_wallet(): + die(0,f'Transaction has {r.confs} confirmation{suf(r.confs)}') + elif await is_in_utxos(): + die(2,red('ERROR: transaction is in the blockchain (but not in the tracking wallet)!')) + elif await is_replaced(): + msg('Transaction has been replaced') + msg('Replacement transaction ' + ( + f'has {r.replacing_confs} confirmation{suf(r.replacing_confs)}' + if r.replacing_confs else + 'is in mempool' ) ) + if not opt.quiet: + msg('Replacing transactions:') + d = [] + for txid in r.replacing_txs: + try: d.append(await self.rpc.call('getmempoolentry',txid)) + except: d.append({}) + for txid,mp_entry in zip(r.replacing_txs,d): + msg(f' {txid}' + (' in mempool' if 'height' in mp_entry else '') ) + die(0,'') + + def confirm_send(self): + confirm_or_raise( + ('' if opt.quiet else "Once this transaction is sent, there's no taking it back!"), + f'broadcast this transaction to the {self.proto.coin} {self.proto.network.upper()} network', + ('YES' if opt.quiet or opt.yes else 'YES, I REALLY WANT TO DO THIS') ) + msg('Sending transaction') + + async def send(self,prompt_user=True,exit_on_fail=False): - return ( - 'Displaying inputs and outputs in {} sort order'.format({'raw':'raw','addr':'address'}[sort]) - + ('\n\n','\n')[terse] - + ''.join(format_io('inputs')) - + ''.join(format_io('outputs')) ) - - def format_view_rel_fee(self,terse): - return ' ({} {})\n'.format( - pink(str(self.fee_abs2rel(self.get_fee()))), - self.rel_fee_disp) - - def format_view_abs_fee(self): - return g.proto.coin_amt(self.get_fee()).hl() - - def format_view_verbose_footer(self): - tsize = len(self.hex)//2 if self.hex else 'unknown' - out = f'Transaction size: Vsize {self.estimate_size()} (estimated), Total {tsize}' - if self.marked_signed(): - wsize = DeserializedTX(self.hex)['witness_size'] - out += f', Base {tsize-wsize}, Witness {wsize}' - return out + '\n' - - def format_view(self,terse=False,sort=dfl_view_sort_order): - blockcount = None - if g.proto.base_coin != 'ETH': - try: - blockcount = g.rpc.blockcount - except: - pass - - def get_max_mmwid(io): - if io == self.inputs: - sel_f = lambda o: len(o.mmid) + 2 # len('()') - else: - sel_f = lambda o: len(o.mmid) + (2,8)[bool(o.is_chg)] # + len(' (chg)') - return max(max([sel_f(o) for o in io if o.mmid] or [0]),len(nonmm_str)) - - nonmm_str = f'(non-{g.proj_name} address)' - max_mmwid = max(get_max_mmwid(self.inputs),get_max_mmwid(self.outputs)) - - def gen_view(): - yield (self.txview_hdr_fs_short if terse else self.txview_hdr_fs).format( - i = self.txid.hl(), - a = self.send_amt.hl(), - c = g.dcoin, - t = self.timestamp, - r = (red('False'),green('True'))[self.is_replaceable()], - s = self.marked_signed(color=True), - l = (green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)] ) - - if self.chain != 'mainnet': - yield green(f'Chain: {self.chain.upper()}') + '\n' - - if self.coin_txid: - yield f'{g.coin} TxID: {self.coin_txid.hl()}\n' - - enl = ('\n','')[bool(terse)] - yield enl - - if self.label: - yield f'Comment: {self.label.hl()}\n{enl}' - - yield self.format_view_body(blockcount,nonmm_str,max_mmwid,enl,terse=terse,sort=sort) - - yield (self.txview_ftr_fs_short if terse else self.txview_ftr_fs).format( - i = self.sum_inputs().hl(), - o = self.sum_outputs().hl(), - a = self.format_view_abs_fee(), - r = self.format_view_rel_fee(terse), - d = g.dcoin, - c = g.coin ) - - if opt.verbose: - yield self.format_view_verbose_footer() - - return ''.join(gen_view()) # TX label might contain non-ascii chars - - def check_txfile_hex_data(self): - self.hex = HexStr(self.hex,on_fail='raise') - - def parse_txfile_hex_data(self): - pass - - def process_cmd_arg(self,arg,ad_f,ad_w): - - def add_output_chk(addr,amt,err_desc): - if not amt and self.get_chg_output_idx() != None: - die(2,'ERROR: More than one change address listed on command line') - if is_mmgen_id(addr) or is_coin_addr(addr): - coin_addr = mmaddr2coinaddr(addr,ad_w,ad_f) if is_mmgen_id(addr) else CoinAddr(addr) - self.add_output(coin_addr,g.proto.coin_amt(amt or '0'),is_chg=not amt) - else: - die(2,f'{addr}: invalid {err_desc} {{!r}}'.format(f'{addr},{amt}' if amt else addr)) - - if ',' in arg: - addr,amt = arg.split(',',1) - add_output_chk(addr,amt,'coin argument in command-line argument') - else: - add_output_chk(arg,None,'command-line argument') - - def process_cmd_args(self,cmd_args,ad_f,ad_w): - - for a in cmd_args: - self.process_cmd_arg(a,ad_f,ad_w) - - if self.get_chg_output_idx() == None: - die(2,( 'ERROR: No change output specified', - self.msg_no_change_output.format(g.dcoin))[len(self.outputs) == 1]) - - if not segwit_is_active() and self.has_segwit_outputs(): - rdie(2,f'{g.proj_name} Segwit address requested on the command line, ' - + 'but Segwit is not active on this chain') - - if not self.outputs: - die(2,'At least one output must be specified on the command line') - - async def get_outputs_from_cmdline(self,cmd_args): - from .addr import AddrList,AddrData,TwAddrData - addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext] - cmd_args = set(cmd_args) - set(addrfiles) - - ad_f = AddrData() - for a in addrfiles: - check_infile(a) - ad_f.add(AddrList(a)) - - ad_w = await TwAddrData(wallet=self.tw) - - self.process_cmd_args(cmd_args,ad_f,ad_w) - - self.add_mmaddrs_to_outputs(ad_w,ad_f) - self.check_dup_addrs('outputs') - - def select_unspent(self,unspent): - prompt = 'Enter a range or space-separated list of outputs to spend: ' - while True: - reply = my_raw_input(prompt).strip() - if reply: - selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return') - if selected: - if selected[-1] <= len(unspent): - return selected - msg('Unspent output number must be <= {}'.format(len(unspent))) - - # we don't know fee yet, so perform preliminary check with fee == 0 - async def precheck_sufficient_funds(self,inputs_sum,sel_unspent): - if self.twuo.total < self.send_amt: - msg(self.msg_wallet_low_coin.format(self.send_amt-inputs_sum,g.dcoin)) - return False - if inputs_sum < self.send_amt: - msg(self.msg_low_coin.format(self.send_amt-inputs_sum,g.dcoin)) - return False - return True - - async def get_change_amt(self): - return self.sum_inputs() - self.send_amt - self.fee - - def warn_insufficient_chg(self,change_amt): - msg(self.msg_low_coin.format(g.proto.coin_amt(-change_amt).hl(),g.coin)) - - def final_inputs_ok_msg(self,change_amt): - return f'Transaction produces {g.proto.coin_amt(change_amt).hl()} {g.coin} in change' - - def select_unspent_cmdline(self,unspent): - - def idx2num(idx): - uo = unspent[idx] - mmid_disp = f' ({uo.twmmid})' if uo.twmmid.type == 'mmgen' else '' - msg(f'Adding input: {idx + 1} {uo.addr}{mmid_disp}') - return idx + 1 - - def get_uo_nums(): - for addr in opt.inputs.split(','): - if is_mmgen_id(addr): - attr = 'twmmid' - elif is_coin_addr(addr): - attr = 'addr' - else: - die(1,f'{addr!r}: not an MMGen ID or {g.coin} address') - - found = False - for idx in range(len(unspent)): - if getattr(unspent[idx],attr) == addr: - yield idx2num(idx) - found = True - - if not found: - die(1,f'{addr!r}: address not found in tracking wallet') - - return set(get_uo_nums()) # silently discard duplicates - - async def get_cmdline_input_addrs(self): - # Bitcoin full node, call doesn't go to the network, so just call listunspent with addrs=[] - return [] - - async def get_inputs_from_user(self): - - while True: - us_f = self.select_unspent_cmdline if opt.inputs else self.select_unspent - sel_nums = us_f(self.twuo.unspent) - - msg(f'Selected output{suf(sel_nums)}: {{}}'.format(' '.join(str(n) for n in sel_nums))) - sel_unspent = self.twuo.MMGenTwOutputList([self.twuo.unspent[i-1] for i in sel_nums]) - - inputs_sum = sum(s.amt for s in sel_unspent) - if not await self.precheck_sufficient_funds(inputs_sum,sel_unspent): - continue - - non_mmaddrs = [i for i in sel_unspent if i.twmmid.type == 'non-mmgen'] - if non_mmaddrs and self.caller != 'txdo': - msg(self.msg_non_mmgen_inputs.format( - ', '.join(sorted({a.addr.hl() for a in non_mmaddrs})))) - if not (opt.yes or keypress_confirm('Accept?')): - continue - - self.copy_inputs_from_tw(sel_unspent) # makes self.inputs - - self.fee = await self.get_fee_from_user() - - change_amt = await self.get_change_amt() - - if change_amt >= 0: - p = self.final_inputs_ok_msg(change_amt) - if opt.yes or keypress_confirm(p+'. OK?',default_yes=True): - if opt.yes: - msg(p) - return change_amt - else: - self.warn_insufficient_chg(change_amt) - - def check_fee(self): - assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee - - def update_send_amt(self,change_amt): - if not self.send_amt: - self.send_amt = change_amt - - async def set_token_params(self): - pass - - async def create(self,cmd_args,locktime,do_info=False): - assert isinstance(locktime,int),'locktime must be of type int' - - from .tw import TwUnspentOutputs - - if opt.comment_file: - self.add_comment(opt.comment_file) - - twuo_addrs = await self.get_cmdline_input_addrs() - - self.twuo = await TwUnspentOutputs(minconf=opt.minconf,addrs=twuo_addrs) - await self.twuo.get_unspent_data() - - if not do_info: - await self.get_outputs_from_cmdline(cmd_args) - - do_license_msg() - - if not opt.inputs: - await self.twuo.view_and_sort(self) - - self.twuo.display_total() - - if do_info: - del self.twuo.wallet - sys.exit(0) - - self.send_amt = self.sum_outputs() - - msg_r('Total amount to spend: ') - msg(f'{self.send_amt.hl()} {g.dcoin}' if self.send_amt else 'Unknown') - - change_amt = await self.get_inputs_from_user() - - self.update_change_output(change_amt) - self.update_send_amt(change_amt) - - if g.proto.base_proto == 'Bitcoin': - self.inputs.sort_bip69() - self.outputs.sort_bip69() - # do this only after inputs are sorted - if opt.rbf: - self.inputs[0].sequence = g.max_int - 2 # handles the locktime case too - elif locktime: - self.inputs[0].sequence = g.max_int - 1 - - if not opt.yes: - self.add_comment() # edits an existing comment - - await self.create_raw() # creates self.hex, self.txid - - if g.proto.base_proto == 'Bitcoin' and locktime: - msg(f'Setting nlocktime to {strfmt_locktime(locktime)}!') - self.set_hex_locktime(locktime) - self.update_txid() - self.locktime = locktime - - self.add_timestamp() - self.add_blockcount() - self.chain = g.chain - - self.check_fee() - - qmsg('Transaction successfully created') - - if not opt.yes: - self.view_with_prompt('View decoded transaction?') - - del self.twuo.wallet - -class MMGenTxForSigning(MMGenTX): - - hexdata_type = 'json' - - def __new__(cls,*args,**kwargs): - return MMGenObject.__new__(altcoin_subclass(cls,'tx','MMGenTxForSigning')) - - async def sign(self,tx_num_str,keys): # return True or False; don't exit or raise exception - - if self.marked_signed(): - msg('Transaction is already signed!') - return False - - try: self.check_correct_chain() - except TransactionChainMismatch: - return False + self.check_pubkey_scripts() + self.check_hex_tx_matches_mmgen_tx(DeserializedTX(self.proto,self.hex)) - if (self.has_segwit_inputs() or self.has_segwit_outputs()) and not g.proto.cap('segwit'): - ymsg(f"TX has Segwit inputs or outputs, but {g.coin} doesn't support Segwit!") - return False + if self.has_segwit_outputs() and not self.rpc.info('segwit_is_active') and not g.bogus_send: + die(2,'Transaction has Segwit outputs, but this blockchain does not support Segwit' + + ' at the current height') - self.check_pubkey_scripts() + if self.get_fee() > self.proto.max_tx_fee: + die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format( + self.get_fee(), + self.proto.name, + self.proto.max_tx_fee, + self.proto.coin )) - qmsg(f'Passing {len(keys)} key{suf(keys)} to {g.proto.daemon_name}') + await self.get_status() - if self.has_segwit_inputs(): - from .addr import KeyGenerator,AddrGenerator - kg = KeyGenerator('std') - ag = AddrGenerator('segwit') - keydict = MMGenDict([(d.addr,d.sec) for d in keys]) + if prompt_user: + self.confirm_send() - sig_data = [] - for d in self.inputs: - e = {k:getattr(d,k) for k in ('txid','vout','scriptPubKey','amt')} - e['amount'] = e['amt'] - del e['amt'] - if d.mmid and d.mmid.mmtype == 'S': - e['redeemScript'] = ag.to_segwit_redeem_script(kg.to_pubhex(keydict[d.addr])) - sig_data.append(e) + if g.bogus_send: + ret = None + else: + try: + ret = await self.rpc.call('sendrawtransaction',self.hex) + except Exception as e: + ret = False - msg_r(f'Signing transaction{tx_num_str}...') - wifs = [d.sec.wif for d in keys] + if ret == False: + errmsg = e + if 'Signature must use SIGHASH_FORKID' in errmsg: + m = ('The Aug. 1 2017 UAHF has activated on this chain.\n' + + 'Re-run the script with the --coin=bch option.' ) + elif 'Illegal use of SIGHASH_FORKID' in errmsg: + m = ('The Aug. 1 2017 UAHF is not yet active on this chain.\n' + + 'Re-run the script without the --coin=bch option.' ) + elif '64: non-final' in errmsg: + m = "Transaction with nLockTime {!r} can't be included in this block!".format( + strfmt_locktime(self.get_hex_locktime()) ) + else: + m = errmsg + ymsg(m) + rmsg(f'Send of MMGen transaction {self.txid} failed') + if exit_on_fail: + sys.exit(1) + return False + else: + if g.bogus_send: + m = 'BOGUS transaction NOT sent: {}' + else: + m = 'Transaction sent: {}' + assert ret == self.coin_txid, 'txid mismatch (after sending)' + msg(m.format(self.coin_txid.hl())) + self.add_timestamp() + self.add_blockcount() + self.desc = 'sent transaction' + return True - try: - args = ( - ('signrawtransaction', self.hex,sig_data,wifs,g.proto.sighash_type), - ('signrawtransactionwithkey',self.hex,wifs,sig_data,g.proto.sighash_type) - )['sign_with_key' in g.rpc.caps] - ret = await g.rpc.call(*args) - except Exception as e: - msg(yellow(( - e.args[0], - 'This is not the BCH chain.\nRe-run the script without the --coin=bch option.' - )['Invalid sighash param' in e.args[0]])) - return False + def print_contract_addr(self): + pass - try: - self.hex = HexStr(ret['hex']) - tx_decoded = await g.rpc.call('decoderawtransaction',ret['hex']) - self.compare_size_and_estimated_size(tx_decoded) - dt = DeserializedTX(self.hex) - self.check_hex_tx_matches_mmgen_tx(dt) - self.coin_txid = CoinTxID(dt['txid'],on_fail='raise') - self.check_sigs(dt) - if not self.coin_txid == tx_decoded['txid']: - raise BadMMGenTxID('txid mismatch (after signing)') - msg('OK') - return True - except Exception as e: - try: m = '{}'.format(e.args[0]) - except: m = repr(e.args[0]) - msg('\n'+yellow(m)) - if g.traceback: - import traceback - ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info()))) - return False + @staticmethod + async def get_tracking_wallet(filename): + from .txfile import MMGenTxFile + tmp_tx = MMGenTX.Base() + MMGenTxFile(tmp_tx).parse(filename,metadata_only=True) + if tmp_tx.proto.tokensym: + from .tw import TrackingWallet + return await TrackingWallet(tmp_tx.proto) + else: + return None -class MMGenBumpTX(MMGenTxForSigning): + class Bump(Completed,New): + desc = 'fee-bumped transaction' + ext = 'rawtx' - def __new__(cls,*args,**kwargs): - return MMGenTX.__new__(altcoin_subclass(cls,'tx','MMGenBumpTX'),*args,**kwargs) + min_fee = None + bump_output_idx = None - min_fee = None - bump_output_idx = None + def __init__(self,data,send,tw=None): + MMGenTX.Completed.__init__(self,data=data) + self.tw = tw - def __init__(self,filename,send=False,tw=None): - super().__init__(filename,tw=tw) + if not self.is_replaceable(): + die(1,f'Transaction {self.txid} is not replaceable') - if not self.is_replaceable(): - die(1,f'Transaction {self.txid} is not replaceable') - - # If sending, require tx to be signed - if send: - if not self.marked_signed(): - die(1,'File {filename!r} is not a signed {g.proj_name} transaction file') - if not self.coin_txid: + # If sending, require original tx to be sent + if send and not self.coin_txid: die(1,'Transaction {self.txid!r} was not broadcast to the network') - self.coin_txid = '' - self.mark_raw() + self.coin_txid = '' - def check_bumpable(self): - if not [o.amt for o in self.outputs if o.amt >= self.min_fee]: - die(1, - 'Transaction cannot be bumped.\n' + - f'All outputs contain less than the minimum fee ({self.min_fee} {g.coin})') + def check_bumpable(self): + if not [o.amt for o in self.outputs if o.amt >= self.min_fee]: + die(1, + 'Transaction cannot be bumped.\n' + + f'All outputs contain less than the minimum fee ({self.min_fee} {self.coin})') - def choose_output(self): - chg_idx = self.get_chg_output_idx() - init_reply = opt.output_to_reduce + def choose_output(self): + chg_idx = self.get_chg_output_idx() + init_reply = opt.output_to_reduce - def check_sufficient_funds(o_amt): - if o_amt < self.min_fee: - msg(f'Minimum fee ({self.min_fee} {g.coin}) is greater than output amount ({o_amt} {g.coin})') - return False - return True + def check_sufficient_funds(o_amt): + if o_amt < self.min_fee: + msg(f'Minimum fee ({self.min_fee} {self.coin}) is greater than output amount ({o_amt} {self.coin})') + return False + return True - if len(self.outputs) == 1: - if check_sufficient_funds(self.outputs[0].amt): - self.bump_output_idx = 0 - return 0 - else: - die(1,'Insufficient funds to bump transaction') - - while True: - if init_reply == None: - m = 'Choose an output to deduct the fee from (Hit ENTER for the change output): ' - reply = my_raw_input(m) or 'c' - else: - reply,init_reply = init_reply,None - if chg_idx == None and not is_int(reply): - msg('Output must be an integer') - elif chg_idx != None and not is_int(reply) and reply != 'c': - msg("Output must be an integer, or 'c' for the change output") - else: - idx = chg_idx if reply == 'c' else (int(reply) - 1) - if idx < 0 or idx >= len(self.outputs): - msg(f'Output must be in the range 1-{len(self.outputs)}') + if len(self.outputs) == 1: + if check_sufficient_funds(self.outputs[0].amt): + self.bump_output_idx = 0 + return 0 else: - o_amt = self.outputs[idx].amt - cm = ' (change output)' if chg_idx == idx else '' - prompt = f'Fee will be deducted from output {idx+1}{cm} ({o_amt} {g.coin})' - if check_sufficient_funds(o_amt): - if opt.yes or keypress_confirm(prompt+'. OK?',default_yes=True): - if opt.yes: - msg(prompt) - self.bump_output_idx = idx - return idx + die(1,'Insufficient funds to bump transaction') - @property - def min_fee(self): - return self.sum_inputs() - self.sum_outputs() + self.relay_fee + while True: + if init_reply == None: + m = 'Choose an output to deduct the fee from (Hit ENTER for the change output): ' + reply = my_raw_input(m) or 'c' + else: + reply,init_reply = init_reply,None + if chg_idx == None and not is_int(reply): + msg('Output must be an integer') + elif chg_idx != None and not is_int(reply) and reply != 'c': + msg("Output must be an integer, or 'c' for the change output") + else: + idx = chg_idx if reply == 'c' else (int(reply) - 1) + if idx < 0 or idx >= len(self.outputs): + msg(f'Output must be in the range 1-{len(self.outputs)}') + else: + o_amt = self.outputs[idx].amt + cm = ' (change output)' if chg_idx == idx else '' + prompt = f'Fee will be deducted from output {idx+1}{cm} ({o_amt} {self.coin})' + if check_sufficient_funds(o_amt): + if opt.yes or keypress_confirm(prompt+'. OK?',default_yes=True): + if opt.yes: + msg(prompt) + self.bump_output_idx = idx + return idx - def update_fee(self,op_idx,fee): - amt = self.sum_inputs() - self.sum_outputs(exclude=op_idx) - fee - self.update_output_amt(op_idx,amt) + @property + def min_fee(self): + return self.sum_inputs() - self.sum_outputs() + self.relay_fee - def convert_and_check_fee(self,tx_fee,desc): - ret = super().convert_and_check_fee(tx_fee,desc) - if ret < self.min_fee: - msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} {})'.format( - ret.hl(), - desc, - self.min_fee, - self.fee_abs2rel(self.min_fee.hl()), - self.rel_fee_desc, - c = g.coin )) - return False - output_amt = self.outputs[self.bump_output_idx].amt - if ret >= output_amt: - msg('{} {c}: {} fee too large. Maximum fee: <{} {c}'.format( - ret.hl(), - desc, - output_amt.hl(), - c = g.coin )) - return False - return ret + def update_fee(self,op_idx,fee): + amt = self.sum_inputs() - self.sum_outputs(exclude=op_idx) - fee + self.update_output_amt(op_idx,amt) + + def convert_and_check_fee(self,tx_fee,desc): + ret = super().convert_and_check_fee(tx_fee,desc) + if ret < self.min_fee: + msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} {})'.format( + ret.hl(), + desc, + self.min_fee, + self.fee_abs2rel(self.min_fee.hl()), + self.rel_fee_desc, + c = self.coin )) + return False + output_amt = self.outputs[self.bump_output_idx].amt + if ret >= output_amt: + msg('{} {c}: {} fee too large. Maximum fee: <{} {c}'.format( + ret.hl(), + desc, + output_amt.hl(), + c = self.coin )) + return False + return ret # NOT MAINTAINED -class MMGenSplitTX(MMGenTX): - - async def get_outputs_from_cmdline(self,mmid): # TODO: check that addr is empty - - from .addr import TwAddrData - ad_w = await TwAddrData() - - if is_mmgen_id(mmid): - coin_addr = mmaddr2coinaddr(mmid,ad_w,None) if is_mmgen_id(mmid) else CoinAddr(mmid) - self.add_output(coin_addr,g.proto.coin_amt('0'),is_chg=True) - else: - die(2,'{}: invalid command-line argument'.format(mmid)) - - self.add_mmaddrs_to_outputs(ad_w,None) - - if not segwit_is_active() and self.has_segwit_outputs(): - fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain' - rdie(2,fs.format(g.proj_name)) - - def get_split_fee_from_user(self): - if opt.rpc_host2: - g.rpc_host = opt.rpc_host2 - if opt.tx_fees: - opt.tx_fee = opt.tx_fees.split(',')[1] - return super().get_fee_from_user() - - async def create_split(self,mmid): - - self.outputs = self.MMGenTxOutputList() - await self.get_outputs_from_cmdline(mmid) - - while True: - change_amt = self.sum_inputs() - self.get_split_fee_from_user() - if change_amt >= 0: - p = 'Transaction produces {} {} in change'.format(change_amt.hl(),g.coin) - if opt.yes or keypress_confirm(p+'. OK?',default_yes=True): - if opt.yes: - msg(p) - break - else: - self.warn_insufficient_chg(change_amt) - - self.update_output_amt(0,change_amt) - self.send_amt = change_amt - - if not opt.yes: - self.add_comment() # edits an existing comment - - await self.create_raw() # creates self.hex, self.txid - - self.add_timestamp() - self.add_blockcount() # TODO - self.chain = g.chain - - assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee - - qmsg('Transaction successfully created') - - if not opt.yes: - self.view_with_prompt('View decoded transaction?') +# class Split(Base): +# +# async def get_outputs_from_cmdline(self,mmid): # TODO: check that addr is empty +# +# from .addr import TwAddrData +# ad_w = await TwAddrData() +# +# if is_mmgen_id(self.proto,mmid): +# coin_addr = mmaddr2coinaddr(mmid,ad_w,None) if is_mmgen_id(self.proto,mmid) else CoinAddr(mmid) +# self.add_output(coin_addr,self.proto.coin_amt('0'),is_chg=True) +# else: +# die(2,'{}: invalid command-line argument'.format(mmid)) +# +# self.add_mmaddrs_to_outputs(ad_w,None) +# +# if not segwit_is_active() and self.has_segwit_outputs(): +# fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain' +# rdie(2,fs.format(g.proj_name)) +# +# def get_split_fee_from_user(self): +# if opt.rpc_host2: +# g.rpc_host = opt.rpc_host2 +# if opt.tx_fees: +# opt.tx_fee = opt.tx_fees.split(',')[1] +# return super().get_fee_from_user() +# +# async def create_split(self,mmid): +# +# self.outputs = self.MMGenTxOutputList(self) +# await self.get_outputs_from_cmdline(mmid) +# +# while True: +# change_amt = self.sum_inputs() - self.get_split_fee_from_user() +# if change_amt >= 0: +# p = 'Transaction produces {} {} in change'.format(change_amt.hl(),self.coin) +# if opt.yes or keypress_confirm(p+'. OK?',default_yes=True): +# if opt.yes: +# msg(p) +# break +# else: +# self.warn_insufficient_chg(change_amt) +# +# self.update_output_amt(0,change_amt) +# self.send_amt = change_amt +# +# if not opt.yes: +# self.add_comment() # edits an existing comment +# +# await self.create_raw() # creates self.hex, self.txid +# +# self.add_timestamp() +# self.add_blockcount() # TODO +# self.chain = g.chain +# +# assert self.sum_inputs() - self.sum_outputs() <= self.proto.max_tx_fee +# +# qmsg('Transaction successfully created') +# +# if not opt.yes: +# self.view_with_prompt('View transaction details?') diff --git a/mmgen/txfile.py b/mmgen/txfile.py index e6f62921..7cfb61fb 100755 --- a/mmgen/txfile.py +++ b/mmgen/txfile.py @@ -46,15 +46,15 @@ class MMGenTxFile: import re d = literal_eval(re.sub(r"[A-Za-z]+?\(('.+?')\)",r'\1',raw_data)) assert type(d) == list,'{} data not a list!'.format(desc) - if not (desc == 'outputs' and g.proto.base_coin == 'ETH'): # ETH txs can have no outputs + if not (desc == 'outputs' and tx.proto.base_coin == 'ETH'): # ETH txs can have no outputs assert len(d),'no {}!'.format(desc) for e in d: - e['amt'] = g.proto.coin_amt(e['amt']) + e['amt'] = tx.proto.coin_amt(e['amt']) io,io_list = ( (MMGenTxOutput,MMGenTxOutputList), (MMGenTxInput,MMGenTxInputList) )[desc=='inputs'] - return io_list(io(**e) for e in d) + return io_list(tx,[io(tx.proto,**e) for e in d]) tx_data = get_data_from_file(infile,tx.desc+' data',quiet=quiet_open) @@ -94,58 +94,51 @@ class MMGenTxFile: desc = 'locktime' tx.locktime = int(metadata.pop()[3:]) - tx.coin = metadata.pop(0) if len(metadata) == 6 else 'BTC' - if ':' in tx.coin: - tx.coin,tx.dcoin = tx.coin.split(':') + desc = 'coin token in metadata' + coin = metadata.pop(0) if len(metadata) == 6 else 'BTC' + coin,tokensym = coin.split(':') if ':' in coin else (coin,None) - if len(metadata) == 5: - t = metadata.pop(0) - tx.chain = (t.lower(),None)[t=='Unknown'] + desc = 'chain token in metadata' + tx.chain = metadata.pop(0).lower() if len(metadata) == 5 else 'mainnet' - desc = 'metadata (4 items minimum required)' + from .protocol import CoinProtocol,init_proto + network = CoinProtocol.Base.chain_name_to_network(coin,tx.chain) + + desc = 'initialization of protocol' + tx.proto = init_proto(coin,network=network) + if tokensym: + tx.proto.tokensym = tokensym + + desc = 'metadata (4 items)' txid,send_amt,tx.timestamp,blockcount = metadata - desc = 'txid in metadata' + desc = 'TxID in metadata' tx.txid = MMGenTxID(txid,on_fail='raise') desc = 'send amount in metadata' - tx.send_amt = UnknownCoinAmt(send_amt) # temporary, for 'metadata_only' + tx.send_amt = tx.proto.coin_amt(send_amt) desc = 'block count in metadata' tx.blockcount = int(blockcount) if metadata_only: return - desc = 'send amount in metadata' - tx.send_amt = g.proto.coin_amt(send_amt,on_fail='raise') - desc = 'transaction file hex data' tx.check_txfile_hex_data() - desc = f'transaction file {tx.hexdata_type} data' + desc = 'Ethereum transaction file hex or json data' tx.parse_txfile_hex_data() - # the following ops will all fail if g.coin doesn't match tx.coin - desc = 'coin type in metadata' - assert tx.coin == g.coin, tx.coin desc = 'inputs data' tx.inputs = eval_io_data(inputs_data,'inputs') desc = 'outputs data' tx.outputs = eval_io_data(outputs_data,'outputs') except Exception as e: - die(2,f'Invalid {desc} in transaction file: {e.args[0]}') - - # is_for_chain() is no-op for Ethereum: test and mainnet addrs have same format - if not tx.chain and not tx.inputs[0].addr.is_for_chain('testnet'): - tx.chain = 'mainnet' - - if tx.dcoin: - tx.resolve_g_token_from_txfile() - g.proto.dcoin = tx.dcoin + die(2,f'Invalid {desc} in transaction file: {e!s}') def make_filename(self): tx = self.tx def gen_filename(): yield tx.txid - if g.coin != 'BTC': - yield '-' + g.dcoin + if tx.coin != 'BTC': + yield '-' + tx.dcoin yield f'[{tx.send_amt!s}' if tx.is_replaceable(): yield ',{}'.format(tx.fee_abs2rel(tx.get_fee(),to_unit=tx.fn_fee_unit)) @@ -154,24 +147,22 @@ class MMGenTxFile: yield ']' if g.debug_utf8: yield '-α' - if g.proto.testnet: - yield '.testnet' + if tx.proto.testnet: + yield '.' + tx.proto.network yield '.' + tx.ext return ''.join(gen_filename()) def format(self): tx = self.tx - tx.inputs.check_coin_mismatch() - tx.outputs.check_coin_mismatch() def amt_to_str(d): return {k: (str(d[k]) if k == 'amt' else d[k]) for k in d} - coin_id = '' if g.coin == 'BTC' else g.coin + ('' if g.coin == g.dcoin else ':'+g.dcoin) + coin_id = '' if tx.coin == 'BTC' else tx.coin + ('' if tx.coin == tx.dcoin else ':'+tx.dcoin) lines = [ '{}{} {} {} {} {}{}'.format( (coin_id+' ' if coin_id else ''), - tx.chain.upper() if tx.chain else 'Unknown', + tx.chain.upper(), tx.txid, tx.send_amt, tx.timestamp, @@ -179,8 +170,8 @@ class MMGenTxFile: ('',' LT={}'.format(tx.locktime))[bool(tx.locktime)] ), tx.hex, - ascii([amt_to_str(e.__dict__) for e in tx.inputs]), - ascii([amt_to_str(e.__dict__) for e in tx.outputs]) + ascii([amt_to_str(e._asdict()) for e in tx.inputs]), + ascii([amt_to_str(e._asdict()) for e in tx.outputs]) ] if tx.label: @@ -222,3 +213,10 @@ class MMGenTxFile: ask_write = ask_write, ask_tty = ask_tty, ask_write_default_yes = ask_write_default_yes ) + + @classmethod + def get_proto(cls,filename,quiet_open=False): + from .tx import MMGenTX + tmp_tx = MMGenTX.Base() + cls(tmp_tx).parse(filename,metadata_only=True,quiet_open=quiet_open) + return tmp_tx.proto diff --git a/mmgen/txsign.py b/mmgen/txsign.py index fa74ed02..263aefa3 100755 --- a/mmgen/txsign.py +++ b/mmgen/txsign.py @@ -21,22 +21,10 @@ txsign: Sign a transaction generated by 'mmgen-txcreate' """ from .common import * -from .wallet import * -from .tx import * -from .addr import * - -pnm = g.proj_name - -wmsg = { - 'mapping_error': """ -{pnm} -> {c} address mappings differ! -{{:<23}} {{}} -> {{}} -{{:<23}} {{}} -> {{}} -""".strip().format(pnm=pnm,c=g.coin), - 'missing_keys_error': """ -ERROR: a key file must be supplied for the following non-{pnm} address{{}}:\n {{}} -""".format(pnm=pnm).strip() -} +from .addr import AddrIdxList,KeyAddrList +from .obj import MMGenAddrType,MMGenList +from .wallet import Wallet,WalletUnenc,WalletEnc,MMGenWallet +from .tx import MMGenTX saved_seeds = {} @@ -67,21 +55,23 @@ def get_seed_for_seed_id(sid,infiles,saved_seeds): else: die(2,f'ERROR: No seed source found for Seed ID: {sid}') -def generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds): +def generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds,proto): mmids = [e.mmid for e in need_keys] sids = {i.sid for i in mmids} - vmsg('Need seed{}: {}'.format(suf(sids),' '.join(sids))) - d = MMGenList() - from .addr import KeyAddrList - for sid in sids: - # Returns only if seed is found - seed = get_seed_for_seed_id(sid,infiles,saved_seeds) - for t in MMGenAddrType.mmtypes: - idx_list = [i.idx for i in mmids if i.sid == sid and i.mmtype == t] - if idx_list: - addr_idxs = AddrIdxList(idx_list=idx_list) - d.append(KeyAddrList(seed=seed,addr_idxs=addr_idxs,mmtype=MMGenAddrType(t))) - return d + vmsg(f"Need seed{suf(sids)}: {' '.join(sids)}") + def gen_kals(): + for sid in sids: + # Returns only if seed is found + seed = get_seed_for_seed_id(sid,infiles,saved_seeds) + for id_str in MMGenAddrType.mmtypes: + idx_list = [i.idx for i in mmids if i.sid == sid and i.mmtype == id_str] + if idx_list: + yield KeyAddrList( + proto = proto, + seed = seed, + addr_idxs = AddrIdxList(idx_list=idx_list), + mmtype = MMGenAddrType(proto,id_str) ) + return MMGenList(gen_kals()) def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None): need_keys = [e for e in getattr(tx,src) if e.mmid and not e.have_wif] @@ -90,10 +80,10 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None): desc,src_desc = ( ('key-address file','From key-address file:') if keyaddr_list else ('seed(s)','Generated from seed:') ) - qmsg(f'Checking {g.proj_name} -> {g.coin} address mappings for {src} (from {desc})') + qmsg(f'Checking {g.proj_name} -> {tx.proto.coin} address mappings for {src} (from {desc})') d = ( MMGenList([keyaddr_list]) if keyaddr_list else - generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds) ) + generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds,tx.proto) ) new_keys = [] for e in need_keys: for kal in d: @@ -105,7 +95,11 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None): if src == 'inputs': new_keys.append(f) else: - die(3,wmsg['mapping_error'].format(src_desc,mmid,f.addr,'tx file:',e.mmid,e.addr)) + die(3,fmt(f""" + {g.proj_name} -> {tx.proto.coin} address mappings differ! + {{src_desc:<23}} {{mmid}} -> {{f.addr}} + {{'tx file:':<23}} {{e.mmid}} -> {{e.addr}} + """).strip()) if new_keys: vmsg(f'Added {len(new_keys)} wif key{suf(new_keys)} from {desc}') return new_keys @@ -114,7 +108,7 @@ def _pop_and_return(args,cmplist): # strips found args return list(reversed([args.pop(args.index(a)) for a in reversed(args) if get_extension(a) in cmplist])) def get_tx_files(opt,args): - ret = _pop_and_return(args,[MMGenTX.raw_ext]) + ret = _pop_and_return(args,[MMGenTX.Unsigned.ext]) if not ret: die(1,'You must specify a raw transaction file!') return ret @@ -131,15 +125,15 @@ def get_seed_files(opt,args): die(1,'You must specify a seed or key source!') return ret -def get_keyaddrlist(opt): +def get_keyaddrlist(proto,opt): if opt.mmgen_keys_from_file: - return KeyAddrList(opt.mmgen_keys_from_file) + return KeyAddrList(proto,opt.mmgen_keys_from_file) return None -def get_keylist(opt): +def get_keylist(proto,opt): if opt.keys_from_file: l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True) - kal = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept coin daemon wallet dumps + kal = KeyAddrList(proto=proto,keylist=[m.split()[0] for m in l]) # accept coin daemon wallet dumps kal.generate_addrs_from_keys() return kal return None @@ -150,14 +144,17 @@ async def txsign(tx,seed_files,kl,kal,tx_num_str=''): non_mmaddrs = tx.get_non_mmaddrs('inputs') if non_mmaddrs: - if not kl: - die(2,'Transaction has non-{} inputs, but no flat key list is present'.format(g.proj_name)) + tx.check_non_mmgen_inputs(caller='txsign',non_mmaddrs=non_mmaddrs) tmp = KeyAddrList( + proto = tx.proto, addrlist = non_mmaddrs ) tmp.add_wifs(kl) m = tmp.list_missing('sec') if m: - die(2,wmsg['missing_keys_error'].format(suf(m,'es'),'\n '.join(m))) + die(2, fmt(f""" + ERROR: a key file must be supplied for the following non-{g.proj_name} address{suf(m,'es')}: + {{}} + """.format('\n '.join(m)),strip_char='\t').strip()) keys += tmp.data if opt.mmgen_keys_from_file: @@ -175,4 +172,4 @@ async def txsign(tx,seed_files,kl,kal,tx_num_str=''): if extra_sids: msg(f"Unused Seed ID{suf(extra_sids)}: {' '.join(extra_sids)}") - return await tx.sign(tx_num_str,keys) # returns True or False + return await tx.sign(tx_num_str,keys) # returns signed TX object or False diff --git a/mmgen/util.py b/mmgen/util.py index 160ba9ed..3c0164b6 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -116,6 +116,11 @@ def fmt_list(l,fmt='dfl',indent=''): CUR_HIDE = '\033[?25l' CUR_SHOW = '\033[?25h' +def exit_if_mswin(feature): + if g.platform == 'win': + m = capfirst(feature) + ' not supported on the MSWin / MSYS2 platform' + ydie(1,m) + def warn_altcoins(coinsym,trust_level): if trust_level > 3: return @@ -817,35 +822,6 @@ def do_license_msg(immed=False): msg_r('\r') msg('') -# TODO: these belong in protocol.py -def get_coin_daemon_cfg_fn(): - # Use dirname() to remove 'bob' or 'alice' component - cfg_dir = os.path.dirname(g.data_dir) if g.proto.regtest else g.proto.daemon_data_dir - return os.path.join( - cfg_dir, - (g.proto.is_fork_of or g.proto.name).lower() + '.conf' ) - -def get_coin_daemon_cfg_options(req_keys): - - fn = get_coin_daemon_cfg_fn() - try: - lines = get_lines_from_file(fn,'',silent=not opt.verbose) - except: - vmsg(f'Warning: {fn!r} does not exist or is unreadable') - return dict((k,None) for k in req_keys) - - def gen(): - for key in req_keys: - val = None - for l in lines: - if l.startswith(key): - res = l.split('=',1) - if len(res) == 2 and not ' ' in res[1].strip(): - val = res[1].strip() - yield (key,val) - - return dict(gen()) - def format_par(s,indent=0,width=80,as_list=False): words,lines = s.split(),[] assert width >= indent + 4,'width must be >= indent + 4' @@ -857,19 +833,29 @@ def format_par(s,indent=0,width=80,as_list=False): lines.append(' '*indent + line) return lines if as_list else '\n'.join(lines) + '\n' -# module loading magic for tx.py and tw.py -def altcoin_subclass(cls,mod_id,cls_name): - if cls.__name__ != cls_name: +def altcoin_subclass(cls,proto,mod_dir): + """ + magic module loading and class retrieval + """ + from .protocol import CoinProtocol + if isinstance(proto,CoinProtocol.Bitcoin): return cls - mod_dir = g.proto.base_coin.lower() - tname = 'Token' if g.token else '' + + modname = f'mmgen.altcoins.{proto.base_coin.lower()}.{mod_dir}' + import importlib - modname = f'mmgen.altcoins.{mod_dir}.{mod_id}' - clsname = g.proto.mod_clsname + tname + cls_name - try: + if mod_dir == 'tx': # nested classes + outer_clsname,inner_clsname = ( + proto.mod_clsname + + ('Token' if proto.tokensym else '') + + cls.__qualname__ ).split('.') + return getattr(getattr(importlib.import_module(modname),outer_clsname),inner_clsname) + else: + clsname = ( + proto.mod_clsname + + ('Token' if proto.tokensym else '') + + cls.__name__ ) return getattr(importlib.import_module(modname),clsname) - except ImportError: - return cls # decorator for TrackingWallet def write_mode(orig_func): @@ -880,13 +866,8 @@ def write_mode(orig_func): return orig_func(self,*args,**kwargs) return f -def get_network_id(coin,testnet): - assert type(testnet) == bool - return coin.lower() + ('','_tn')[testnet] - -def run_session(callback,do_rpc_init=True,proto=None,backend=None): +def run_session(callback,backend=None): backend = backend or opt.rpc_backend - proto = proto or g.proto import asyncio async def do(): if backend == 'aiohttp': @@ -895,16 +876,10 @@ def run_session(callback,do_rpc_init=True,proto=None,backend=None): headers = { 'Content-Type': 'application/json' }, connector = aiohttp.TCPConnector(limit_per_host=g.aiohttp_rpc_queue_len), ) as g.session: - if do_rpc_init: - from .rpc import rpc_init - await rpc_init(proto=proto,backend=backend) ret = await callback g.session = None return ret else: - if do_rpc_init: - from .rpc import rpc_init - await rpc_init(proto=proto,backend=backend) return await callback # return asyncio.run(do()) # Python 3.7+ diff --git a/scripts/create-token.py b/scripts/create-token.py index ff7af777..55def5e9 100755 --- a/scripts/create-token.py +++ b/scripts/create-token.py @@ -52,9 +52,13 @@ opts_data = { } cmd_args = opts.init(opts_data) -assert g.coin in ('ETH','ETC'),'--coin option must be set to ETH or ETC' -if not len(cmd_args) == 1 or not is_coin_addr(cmd_args[0].lower()): +from mmgen.protocol import init_proto_from_opts +proto = init_proto_from_opts() + +assert proto.coin in ('ETH','ETC'),'--coin option must be set to ETH or ETC' + +if not len(cmd_args) == 1 or not is_coin_addr(proto,cmd_args[0].lower()): opts.usage() owner_addr = '0x' + cmd_args[0] diff --git a/scripts/tx-btc2bch.py b/scripts/tx-btc2bch.py deleted file mode 100755 index 03aa6cf9..00000000 --- a/scripts/tx-btc2bch.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution -# Copyright (C)2013-2020 The MMGen Project -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -tx-btc2bch: Convert MMGen transaction files from BTC to BCH format -""" - -from mmgen.common import * - -opts_data = { - 'text': { - 'desc': """Convert {pnm} transaction files from BTC to BCH format""".format(pnm=g.proj_name), - 'usage':'[opts] [mmgen transaction file]', - 'options': """ --h, --help Print this help message ---, --longhelp Print help message for long options (common options) --v, --verbose Produce more verbose output -""" - } -} - -cmd_args = opts.init(opts_data) - -if g.coin != 'BTC': - die(1,"This program must be run with --coin set to 'BTC'") - -if len(cmd_args) != 1: opts.usage() - -import mmgen.tx -tx = mmgen.tx.MMGenTX(cmd_args[0]) - -if opt.verbose: - gmsg(f'Original transaction is in {g.coin} format') - -from mmgen.protocol import init_proto -g.proto = init_proto('bch') - -if opt.verbose: - gmsg('Converting transaction to {} format'.format(g.coin)) - -tx.inputs.convert_coin(verbose=opt.verbose) -tx.outputs.convert_coin(verbose=opt.verbose) - -tx.desc = 'converted transaction' -tx.write_to_file(ask_write=False,ask_overwrite=False) diff --git a/setup.py b/setup.py index 7884aca4..f5529a1a 100755 --- a/setup.py +++ b/setup.py @@ -113,6 +113,7 @@ setup( 'mmgen.exception', 'mmgen.filename', 'mmgen.globalvars', + 'mmgen.help', 'mmgen.keccak', 'mmgen.led', 'mmgen.license', diff --git a/test/gentest.py b/test/gentest.py index fd014638..59920059 100755 --- a/test/gentest.py +++ b/test/gentest.py @@ -106,7 +106,7 @@ Supported external tools: prog='test/gentest.py', pnm=g.proj_name, snum=rounds, - dn=g.proto.daemon_name) + dn=proto.daemon_name) } } @@ -117,6 +117,9 @@ cmd_args = opts.init(opts_data,add_opts=['exact_output','use_old_ed25519']) if not 1 <= len(cmd_args) <= 2: opts.usage() +from mmgen.protocol import init_proto_from_opts +proto = init_proto_from_opts() + from subprocess import run,PIPE,DEVNULL def get_cmd_output(cmd,input=None): return run(cmd,input=input,stdout=PIPE,stderr=DEVNULL).stdout.decode().splitlines() @@ -127,15 +130,15 @@ gtr = namedtuple('gen_tool_result',['wif','addr','vk']) class GenTool(object): def run_tool(self,sec): - vcoin = 'BTC' if g.coin == 'BCH' else g.coin + vcoin = 'BTC' if proto.coin == 'BCH' else proto.coin return self.run(sec,vcoin) class GenToolEthkey(GenTool): desc = 'ethkey' def __init__(self): - g.proto = init_proto('eth') + proto = init_proto('eth') global addr_type - addr_type = MMGenAddrType('E') + addr_type = MMGenAddrType(proto,'E') def run(self,sec,vcoin): o = get_cmd_output(['ethkey','info',sec]) @@ -150,9 +153,9 @@ class GenToolKeyconv(GenTool): class GenToolZcash_mini(GenTool): desc = 'zcash-mini' def __init__(self): - g.proto = init_proto('zec') + proto = init_proto('zec') global addr_type - addr_type = MMGenAddrType('Z') + addr_type = MMGenAddrType(proto,'Z') def run(self,sec,vcoin): o = get_cmd_output(['zcash-mini','-key','-simple'],input=(sec.wif+'\n').encode()) @@ -172,7 +175,7 @@ class GenToolPycoin(GenTool): self.nfnc = network_for_netcode def run(self,sec,vcoin): - if g.proto.testnet: + if proto.testnet: vcoin = ci.external_tests['testnet']['pycoin'][vcoin] network = self.nfnc(vcoin) key = network.keys.private(secret_exponent=int(sec,16),is_compressed=addr_type.name != 'legacy') @@ -200,10 +203,10 @@ class GenToolMoneropy(GenTool): raise ImportError(m) self.mpa = moneropy.account - g.proto = init_proto('xmr') + proto = init_proto('xmr') global addr_type - addr_type = MMGenAddrType('M') + addr_type = MMGenAddrType(proto,'M') def run(self,sec,vcoin): sk_t,vk_t,addr_t = self.mpa.account_from_spend_key(sec) # VERY slow! @@ -212,7 +215,7 @@ class GenToolMoneropy(GenTool): def get_tool(arg): if arg not in ext_progs + ['ext']: - die(1,'{!r}: unsupported tool for network {}'.format(arg,g.proto.network)) + die(1,'{!r}: unsupported tool for network {}'.format(arg,proto.network)) if opt.all: if arg == 'ext': @@ -220,9 +223,9 @@ def get_tool(arg): return arg else: tool = ci.get_test_support( - g.coin, + proto.coin, addr_type.name, - g.proto.network, + proto.network, verbose = not opt.quiet, tool = arg if arg in ext_progs else None ) if not tool: @@ -251,12 +254,12 @@ def test_equal(desc,a_val,b_val,in_bytes,sec,wif,a_desc,b_desc): def gentool_test(kg_a,kg_b,ag,rounds): m = "Comparing address generators '{A}' and '{B}' for {N} {c} ({n}), addrtype {a!r}" - e = ci.get_entry(g.coin,g.proto.network) + e = ci.get_entry(proto.coin,proto.network) qmsg(green(m.format( A = kg_a.desc, B = kg_b.desc, - N = g.proto.network, - c = g.coin, + N = proto.network, + c = proto.coin, n = e.name if e else '---', a = addr_type.name ))) @@ -268,7 +271,7 @@ def gentool_test(kg_a,kg_b,ag,rounds): if opt.verbose or time.time() - last_t >= 0.1: qmsg_r('\rRound {}/{} '.format(i+1,trounds)) last_t = time.time() - sec = PrivKey(in_bytes,compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type) + sec = PrivKey(proto,in_bytes,compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type) a_ph = kg_a.to_pubhex(sec) a_addr = ag.to_addr(a_ph) a_vk = None @@ -311,7 +314,7 @@ def gentool_test(kg_a,kg_b,ag,rounds): def speed_test(kg,ag,rounds): m = "Testing speed of address generator '{}' for coin {}" - qmsg(green(m.format(kg.desc,g.coin))) + qmsg(green(m.format(kg.desc,proto.coin))) from struct import pack,unpack seed = os.urandom(28) qmsg('Incrementing key with each round') @@ -323,7 +326,7 @@ def speed_test(kg,ag,rounds): if time.time() - last_t >= 0.1: qmsg_r('\rRound {}/{} '.format(i+1,rounds)) last_t = time.time() - sec = PrivKey(seed+pack('I',i),compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type) + sec = PrivKey(proto,seed+pack('I',i),compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type) addr = ag.to_addr(kg.to_pubhex(sec)) vmsg('\nkey: {}\naddr: {}\n'.format(sec.wif,addr)) qmsg_r('\rRound {}/{} '.format(i+1,rounds)) @@ -341,9 +344,9 @@ def dump_test(kg,ag,fh): for count,(b_wif,b_addr) in enumerate(dump,1): qmsg_r('\rKey {}/{} '.format(count,len(dump))) try: - b_sec = PrivKey(wif=b_wif) + b_sec = PrivKey(proto,wif=b_wif) except: - die(2,'\nInvalid {} WIF address in dump file: {}'.format(g.proto.network,b_wif)) + die(2,'\nInvalid {} WIF address in dump file: {}'.format(proto.network,b_wif)) a_addr = ag.to_addr(kg.to_pubhex(b_sec)) vmsg('\nwif: {}\naddr: {}\n'.format(b_wif,b_addr)) tinfo = (bytes.fromhex(b_sec),b_sec,b_wif,kg.desc,fh.name) @@ -366,12 +369,12 @@ def parse_arg1(arg,arg_id): if arg_id == 'a': if is_int(arg): a_num = check_gen_num(arg) - return (KeyGenerator(addr_type,a_num),a_num) + return (KeyGenerator(proto,addr_type,a_num),a_num) else: die(1,m1) elif arg_id == 'b': if is_int(arg): - return KeyGenerator(addr_type,check_gen_num(arg)) + return KeyGenerator(proto,addr_type,check_gen_num(arg)) elif arg in ext_progs + ['ext']: return init_tool(get_tool(arg)) else: @@ -395,8 +398,10 @@ from mmgen.altcoin import CoinInfo as ci from mmgen.obj import MMGenAddrType,PrivKey from mmgen.addr import KeyGenerator,AddrGenerator -addr_type = MMGenAddrType(opt.type or g.proto.dfl_mmtype) -ext_progs = list(ci.external_tests[g.proto.network]) +addr_type = MMGenAddrType( + proto = proto, + id_str = opt.type or proto.dfl_mmtype ) +ext_progs = list(ci.external_tests[proto.network]) arg1 = cmd_args[0].split(':') if len(arg1) == 1: @@ -413,7 +418,8 @@ if type(a) == type(b): arg2 = parse_arg2() -ag = AddrGenerator(addr_type) +if not opt.all: + ag = AddrGenerator(proto,addr_type) if not b and type(arg2) == int: speed_test(a,ag,arg2) @@ -422,18 +428,18 @@ elif not b and hasattr(arg2,'read'): elif a and b and type(arg2) == int: if opt.all: from mmgen.protocol import CoinProtocol,init_genonly_altcoins - init_genonly_altcoins(testnet=g.proto.testnet) - for coin in ci.external_tests[g.proto.network][b.desc]: + init_genonly_altcoins(testnet=proto.testnet) + for coin in ci.external_tests[proto.network][b.desc]: if coin.lower() not in CoinProtocol.coins: # ymsg('Coin {} not configured'.format(coin)) continue - g.proto = init_proto(coin) - if addr_type not in g.proto.mmtypes: + proto = init_proto(coin) + if addr_type not in proto.mmtypes: continue - # g.proto has changed, so reinit kg and ag just to be on the safe side: - a = KeyGenerator(addr_type,a_num) - ag = AddrGenerator(addr_type) - b_chk = ci.get_test_support(g.coin,addr_type.name,g.proto.network,tool=b.desc,verbose=not opt.quiet) + # proto has changed, so reinit kg and ag + a = KeyGenerator(proto,addr_type,a_num) + ag = AddrGenerator(proto,addr_type) + b_chk = ci.get_test_support(proto.coin,addr_type.name,proto.network,tool=b.desc,verbose=not opt.quiet) if b_chk == b.desc: gentool_test(a,b,ag,arg2) else: diff --git a/test/include/common.py b/test/include/common.py index 0efd1822..d3923068 100755 --- a/test/include/common.py +++ b/test/include/common.py @@ -74,6 +74,9 @@ def getrandstr(num_chars,no_space=False): if no_space: n,m = 94,33 return ''.join([chr(i%n+m) for i in list(os.urandom(num_chars))]) +def get_data_dir(): + return os.path.join('test','data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))]) + # Windows uses non-UTF8 encodings in filesystem, so use raw bytes here def cleandir(d,do_msg=False): d_enc = d.encode() diff --git a/test/objattrtest.py b/test/objattrtest.py index 50f59dd6..3e8f3d03 100755 --- a/test/objattrtest.py +++ b/test/objattrtest.py @@ -147,16 +147,16 @@ def test_object(test_data,objname): def do_loop(): import importlib - modname = 'test.objattrtest_py_d.oat_{}_{}'.format(g.coin.lower(),g.proto.network) + modname = f'test.objattrtest_py_d.oat_{proto.coin.lower()}_{proto.network}' test_data = importlib.import_module(modname).tests - gmsg('Running immutable attribute tests for {} {}'.format(g.coin,g.proto.network)) + gmsg(f'Running immutable attribute tests for {proto.coin} {proto.network}') utests = cmd_args for obj in test_data: if utests and obj not in utests: continue - clr = blue if opt.verbose else nocolor - msg(clr('Testing {}'.format(obj))) + msg((blue if opt.verbose else nocolor)(f'Testing {obj}')) test_object(test_data,obj) -g.proto = init_proto(g.coin) +from mmgen.protocol import init_proto_from_opts +proto = init_proto_from_opts() do_loop() diff --git a/test/objattrtest_py_d/oat_btc_mainnet.py b/test/objattrtest_py_d/oat_btc_mainnet.py index d6b3b78f..7c2f0357 100755 --- a/test/objattrtest_py_d/oat_btc_mainnet.py +++ b/test/objattrtest_py_d/oat_btc_mainnet.py @@ -9,14 +9,17 @@ objects """ from .oat_common import * +from mmgen.protocol import init_proto + +proto = init_proto('btc') sample_objs.update({ - 'PrivKey': PrivKey(seed_bin,compressed=True,pubkey_type='std'), - 'WifKey': WifKey('5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX'), - 'CoinAddr': CoinAddr('1111111111111111111114oLvT2'), + 'PrivKey': PrivKey(proto,seed_bin,compressed=True,pubkey_type='std'), + 'WifKey': WifKey(proto,'5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX'), + 'CoinAddr': CoinAddr(proto,'1111111111111111111114oLvT2'), 'BTCAmt': BTCAmt('0.01'), - 'MMGenID': MMGenID('F00F00BB:B:1'), - 'TwMMGenID': TwMMGenID('F00F00BB:S:23'), + 'MMGenID': MMGenID(proto,'F00F00BB:B:1'), + 'TwMMGenID': TwMMGenID(proto,'F00F00BB:S:23'), }) tests = { @@ -29,7 +32,7 @@ tests = { # 'viewkey': (0b001, ViewKey), # TODO # 'wallet_passwd': (0b001, WalletPassword), # TODO }, - [], + (proto,), {} ), 'PasswordListEntry': atd({ @@ -38,7 +41,7 @@ tests = { 'label': (0b101, TwComment), 'sec': (0b001, PrivKey), }, - [], + (proto,), {'passwd':'ΑlphaΩmega', 'idx':1 }, ), # obj.py @@ -46,7 +49,7 @@ tests = { 'compressed': (0b001, bool), 'wif': (0b001, WifKey), }, - [seed_bin], + (proto,seed_bin), {'compressed':True, 'pubkey_type':'std'}, ), 'MMGenAddrType': atd({ @@ -59,7 +62,7 @@ tests = { 'extra_attrs': (0b001, tuple), 'desc': (0b001, str), }, - ['S'], + (proto,'S'), {}, ), # seed.py @@ -118,7 +121,7 @@ tests = { 'scriptPubKey': (0b001, HexStr), 'skip': (0b101, str), }, - [], + (proto,), { 'amt':BTCAmt('0.01'), 'twmmid':'F00F00BB:B:17', @@ -126,7 +129,6 @@ tests = { 'confs': 100000, 'scriptPubKey':'ff', }, - ), # tx.py 'MMGenTxInput': atd({ @@ -141,7 +143,7 @@ tests = { 'scriptPubKey': (0b001, HexStr), 'sequence': (0b001, int), }, - [], + (proto,), { 'amt':BTCAmt('0.01'), 'addr':sample_objs['CoinAddr'] }, ), 'MMGenTxOutput': atd({ @@ -155,9 +157,9 @@ tests = { 'have_wif': (0b011, bool), 'is_chg': (0b001, bool), }, - [], + (proto,), { 'amt':BTCAmt('0.01'), 'addr':sample_objs['CoinAddr'] }, ), } -tests['MMGenPasswordType'] = atd(tests['MMGenAddrType'].attrs, ['P'], {}) +tests['MMGenPasswordType'] = atd(tests['MMGenAddrType'].attrs, [proto,'P'], {}) diff --git a/test/objtest.py b/test/objtest.py index fab56c9a..6c6c2015 100755 --- a/test/objtest.py +++ b/test/objtest.py @@ -55,8 +55,10 @@ def run_test(test,arg,input_data): arg_copy = arg kwargs = {'on_fail':'silent'} if opt.silent else {'on_fail':'die'} ret_chk = arg + ret_idx = None exc_type = None - if input_data == 'good' and type(arg) == tuple: arg,ret_chk = arg + if input_data == 'good' and type(arg) == tuple: + arg,ret_chk = arg if type(arg) == dict: # pass one arg + kwargs to constructor arg_copy = arg.copy() if 'arg' in arg: @@ -70,6 +72,10 @@ def run_test(test,arg,input_data): ret_chk = arg['ret'] del arg['ret'] del arg_copy['ret'] + if 'ret_idx' in arg: + ret_idx = arg['ret_idx'] + del arg['ret_idx'] + del arg_copy['ret_idx'] if 'ExcType' in arg: exc_type = arg['ExcType'] del arg['ExcType'] @@ -94,8 +100,11 @@ def run_test(test,arg,input_data): raise UserWarning("Non-'None' return value {} with bad input data".format(repr(ret))) if opt.silent and input_data=='good' and ret==bad_ret: raise UserWarning("'None' returned with good input data") - if input_data=='good' and ret != ret_chk and repr(ret) != repr(ret_chk): - raise UserWarning("Return value ({!r}) doesn't match expected value ({!r})".format(ret,ret_chk)) + if input_data=='good': + if ret_idx: + ret_chk = arg[list(arg.keys())[ret_idx]].encode() + if ret != ret_chk and repr(ret) != repr(ret_chk): + raise UserWarning("Return value ({!r}) doesn't match expected value ({!r})".format(ret,ret_chk)) if not opt.super_silent: try: ret_disp = ret.decode() except: ret_disp = ret @@ -119,9 +128,9 @@ def run_test(test,arg,input_data): def do_loop(): import importlib - modname = 'test.objtest_py_d.ot_{}_{}'.format(g.coin.lower(),g.proto.network) + modname = f'test.objtest_py_d.ot_{proto.coin.lower()}_{proto.network}' test_data = importlib.import_module(modname).tests - gmsg('Running data object tests for {} {}'.format(g.coin,g.proto.network)) + gmsg(f'Running data object tests for {proto.coin} {proto.network}') clr = None utests = cmd_args @@ -136,4 +145,6 @@ def do_loop(): for arg in test_data[test][k]: run_test(test,arg,input_data=k) +from mmgen.protocol import init_proto_from_opts +proto = init_proto_from_opts() do_loop() diff --git a/test/objtest_py_d/ot_btc_mainnet.py b/test/objtest_py_d/ot_btc_mainnet.py index 5a13d3ef..b97d58c1 100755 --- a/test/objtest_py_d/ot_btc_mainnet.py +++ b/test/objtest_py_d/ot_btc_mainnet.py @@ -11,6 +11,10 @@ from mmgen.obj import * from mmgen.seed import * from .ot_common import * +from mmgen.protocol import init_proto +proto = init_proto('btc') +tw_pfx = proto.base_coin.lower() + ':' + ssm = str(SeedShareCount.max_val) tests = { @@ -82,8 +86,15 @@ tests = { ) }, 'CoinAddr': { - 'bad': (1,'x','я'), - 'good': ('1MjjELEy6EJwk8fSNfpS8b5teFRo4X5fZr','32GiSWo9zJQgkCmjAaLRrbPwXhKry2jHhj'), + 'good': ( + {'addr':'1MjjELEy6EJwk8fSNfpS8b5teFRo4X5fZr', 'proto':proto}, + {'addr':'32GiSWo9zJQgkCmjAaLRrbPwXhKry2jHhj', 'proto':proto}, + ), + 'bad': ( + {'addr':1, 'proto':proto}, + {'addr':'x', 'proto':proto}, + {'addr':'я', 'proto':proto}, + ), }, 'SeedID': { 'bad': ( @@ -93,7 +104,8 @@ tests = { {'sid':1}, {'sid':'F00BAA123'}, {'sid':'f00baa12'}, - 'я',r32,'abc'), + 'я',r32,'abc' + ), 'good': (({'sid':'F00BAA12'},'F00BAA12'),(Seed(r16),Seed(r16).sid)) }, 'SubSeedIdx': { @@ -101,12 +113,41 @@ tests = { 'good': (('1','1L'),('1s','1S'),'20S','30L',('300l','300L'),('200','200L'),str(SubSeedIdxRange.max_idx)+'S') }, 'MMGenID': { - 'bad': ('x',1,'f00f00f','a:b','x:L:3','F00BAA12:0','F00BAA12:Z:99'), - 'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:99') + 'bad': ( + {'id_str':'x', 'proto':proto}, + {'id_str':1, 'proto':proto}, + {'id_str':'f00f00f', 'proto':proto}, + {'id_str':'a:b', 'proto':proto}, + {'id_str':'x:L:3', 'proto':proto}, + {'id_str':'F00BAA12', 'proto':proto}, + {'id_str':'F00BAA12:Z:99', 'proto':proto}, + ), + 'good': ( + {'id_str':'F00BAA12:99', 'proto':proto, 'ret':'F00BAA12:L:99'}, + {'id_str':'F00BAA12:L:99', 'proto':proto}, + {'id_str':'F00BAA12:S:99', 'proto':proto}, + ), }, 'TwMMGenID': { - 'bad': ('x','я','я:я',1,'f00f00f','a:b','x:L:3','F00BAA12:0','F00BAA12:Z:99',tw_pfx,tw_pfx+'я'), - 'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:9999999',tw_pfx+'x') + 'bad': ( + {'id_str':'x', 'proto':proto}, + {'id_str':'я', 'proto':proto}, + {'id_str':'я:я', 'proto':proto}, + {'id_str':1, 'proto':proto}, + {'id_str':'f00f00f', 'proto':proto}, + {'id_str':'a:b', 'proto':proto}, + {'id_str':'x:L:3', 'proto':proto}, + {'id_str':'F00BAA12:0', 'proto':proto}, + {'id_str':'F00BAA12:Z:99', 'proto':proto}, + {'id_str':tw_pfx, 'proto':proto}, + {'id_str':tw_pfx+'я', 'proto':proto}, + ), + 'good': ( + {'id_str':tw_pfx+'x', 'proto':proto}, + {'id_str':'F00BAA12:99', 'proto':proto, 'ret':'F00BAA12:L:99'}, + {'id_str':'F00BAA12:L:99', 'proto':proto}, + {'id_str':'F00BAA12:S:9999999', 'proto':proto}, + ), }, 'TwLabel': { 'bad': ('x x','x я','я:я',1,'f00f00f','a:b','x:L:3','F00BAA12:0 x', @@ -120,6 +161,30 @@ tests = { 'F00BAA12:S:9999999 comment', tw_pfx+'x comment') }, + 'TwLabel': { + 'bad': ( + {'text':'x x', 'proto':proto}, + {'text':'x я', 'proto':proto}, + {'text':'я:я', 'proto':proto}, + {'text':1, 'proto':proto}, + {'text':'f00f00f', 'proto':proto}, + {'text':'a:b', 'proto':proto}, + {'text':'x:L:3', 'proto':proto}, + {'text':'F00BAA12:0 x', 'proto':proto}, + {'text':'F00BAA12:Z:99', 'proto':proto}, + {'text':tw_pfx+' x', 'proto':proto}, + {'text':tw_pfx+'я x', 'proto':proto}, + {'text':utf8_ctrl[:40], 'proto':proto}, + {'text':'F00BAA12:S:1 '+ utf8_ctrl[:40], 'proto':proto, 'on_fail':'raise','ExcType':'BadTwComment'}, + ), + 'good': ( + {'text':'F00BAA12:99 a comment', 'proto':proto, 'ret':'F00BAA12:L:99 a comment'}, + {'text':'F00BAA12:L:99 a comment', 'proto':proto}, + {'text': 'F00BAA12:L:99 comment (UTF-8) α', 'proto':proto}, + {'text':'F00BAA12:S:9999999 comment', 'proto':proto}, + {'text':tw_pfx+'x comment', 'proto':proto}, + ), + }, 'MMGenTxID': { 'bad': (1,[],'\0','\1','я','g','gg','FF','f00','F00F0012'), 'good': ('DEADBE','F00BAA') @@ -129,9 +194,23 @@ tests = { 'good': (r32.hex(),) }, 'WifKey': { - 'bad': (1,[],'\0','\1','я','g','gg','FF','f00',r16.hex(),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'), - 'good': ('5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb', - 'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk'), + 'bad': ( + {'proto':proto, 'wif':1}, + {'proto':proto, 'wif':[]}, + {'proto':proto, 'wif':'\0'}, + {'proto':proto, 'wif':'\1'}, + {'proto':proto, 'wif':'я'}, + {'proto':proto, 'wif':'g'}, + {'proto':proto, 'wif':'gg'}, + {'proto':proto, 'wif':'FF'}, + {'proto':proto, 'wif':'f00'}, + {'proto':proto, 'wif':r16.hex()}, + {'proto':proto, 'wif':'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'}, + ), + 'good': ( + {'proto':proto, 'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb', 'ret_idx':1}, + {'proto':proto, 'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk', 'ret_idx':1}, + ) }, 'PubKey': { 'bad': ({'arg':1,'compressed':False},{'arg':'F00BAA12','compressed':False},), @@ -139,24 +218,24 @@ tests = { }, 'PrivKey': { 'bad': ( - {'wif':1}, - {'wif':'1'}, - {'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR'}, - {'s':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'}, - {'pubkey_type':'std'}, - {'s':r32}, - {'s':r32,'compressed':'yes'}, - {'s':r32,'compressed':'yes','pubkey_type':'std'}, - {'s':r32,'compressed':True,'pubkey_type':'nonstd'}, - {'s':r32+b'x','compressed':True,'pubkey_type':'std'} + {'proto':proto, 'wif':1}, + {'proto':proto, 'wif':'1'}, + {'proto':proto, 'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR'}, + {'proto':proto, 's':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'}, + {'proto':proto, 'pubkey_type':'std'}, + {'proto':proto, 's':r32}, + {'proto':proto, 's':r32,'compressed':'yes'}, + {'proto':proto, 's':r32,'compressed':'yes','pubkey_type':'std'}, + {'proto':proto, 's':r32,'compressed':True,'pubkey_type':'nonstd'}, + {'proto':proto, 's':r32+b'x','compressed':True,'pubkey_type':'std'} ), 'good': ( - {'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb', + {'proto':proto, 'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb', 'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'}, - {'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk', + {'proto':proto, 'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk', 'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'}, - {'s':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()}, - {'s':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()} + {'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()}, + {'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()} ) }, 'AddrListID': { # a rather pointless test, but do it anyway @@ -164,8 +243,8 @@ tests = { {'sid':SeedID(sid='F00BAA12'),'mmtype':'Z','ret':'F00BAA12:Z'}, ), 'good': ( - {'sid':SeedID(sid='F00BAA12'),'mmtype':MMGenAddrType('S'),'ret':'F00BAA12:S'}, - {'sid':SeedID(sid='F00BAA12'),'mmtype':MMGenAddrType('L'),'ret':'F00BAA12:L'}, + {'sid':SeedID(sid='F00BAA12'),'mmtype':proto.addr_type(id_str='S'),'ret':'F00BAA12:S'}, + {'sid':SeedID(sid='F00BAA12'),'mmtype':proto.addr_type(id_str='L'),'ret':'F00BAA12:L'}, ) }, 'MMGenWalletLabel': { @@ -193,23 +272,34 @@ tests = { 'good': ('qwerty@яяя',) }, 'MMGenAddrType': { - 'bad': ('U','z','xx',1,'dogecoin'), + 'bad': ( + {'proto':proto, 'id_str':'U', 'ret':'L'}, + {'proto':proto, 'id_str':'z', 'ret':'L'}, + {'proto':proto, 'id_str':'xx', 'ret':'C'}, + {'proto':proto, 'id_str':'dogecoin', 'ret':'C'}, + ), 'good': ( - {'s':'legacy','ret':'L'}, - {'s':'L','ret':'L'}, - {'s':'compressed','ret':'C'}, - {'s':'C','ret':'C'}, - {'s':'segwit','ret':'S'}, - {'s':'S','ret':'S'}, - {'s':'bech32','ret':'B'}, - {'s':'B','ret':'B'} + {'proto':proto, 'id_str':'legacy', 'ret':'L'}, + {'proto':proto, 'id_str':'L', 'ret':'L'}, + {'proto':proto, 'id_str':'compressed','ret':'C'}, + {'proto':proto, 'id_str':'C', 'ret':'C'}, + {'proto':proto, 'id_str':'segwit', 'ret':'S'}, + {'proto':proto, 'id_str':'S', 'ret':'S'}, + {'proto':proto, 'id_str':'bech32', 'ret':'B'}, + {'proto':proto, 'id_str':'B', 'ret':'B'} ) }, 'MMGenPasswordType': { - 'bad': ('U','z','я',1,'passw0rd'), + 'bad': ( + {'proto':proto, 'id_str':'U', 'ret':'L'}, + {'proto':proto, 'id_str':'z', 'ret':'L'}, + {'proto':proto, 'id_str':'я', 'ret':'C'}, + {'proto':proto, 'id_str':1, 'ret':'C'}, + {'proto':proto, 'id_str':'passw0rd', 'ret':'C'}, + ), 'good': ( - {'s':'password','ret':'P'}, - {'s':'P','ret':'P'}, + {'proto':proto, 'id_str':'password', 'ret':'P'}, + {'proto':proto, 'id_str':'P', 'ret':'P'}, ) }, 'SeedSplitSpecifier': { diff --git a/test/objtest_py_d/ot_btc_testnet.py b/test/objtest_py_d/ot_btc_testnet.py index 0ab80af2..9c9e43b2 100755 --- a/test/objtest_py_d/ot_btc_testnet.py +++ b/test/objtest_py_d/ot_btc_testnet.py @@ -10,36 +10,60 @@ test.objtest_py_d.ot_btc_testnet: BTC testnet test vectors for MMGen data object from mmgen.obj import * from .ot_common import * +from mmgen.protocol import init_proto +proto = init_proto('btc',network='testnet') + tests = { 'CoinAddr': { - 'bad': (1,'x','я'), - 'good': ('n2FgXPKwuFkCXF946EnoxWJDWF2VwQ6q8J','2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'), + 'bad': ( + {'addr':1, 'proto':proto}, + {'addr':'x', 'proto':proto}, + {'addr':'я', 'proto':proto}, + ), + 'good': ( + {'addr':'n2FgXPKwuFkCXF946EnoxWJDWF2VwQ6q8J', 'proto':proto}, + {'addr':'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN','proto':proto}, + ), }, 'WifKey': { - 'bad': (1,[],'\0','\1','я','g','gg','FF','f00',r16.hex(),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'), - 'good': ('93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6', - 'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR'), + 'bad': ( + {'proto':proto, 'wif':1}, + {'proto':proto, 'wif':[]}, + {'proto':proto, 'wif':'\0'}, + {'proto':proto, 'wif':'\1'}, + {'proto':proto, 'wif':'я'}, + {'proto':proto, 'wif':'g'}, + {'proto':proto, 'wif':'gg'}, + {'proto':proto, 'wif':'FF'}, + {'proto':proto, 'wif':'f00'}, + {'proto':proto, 'wif':r16.hex()}, + {'proto':proto, 'wif':'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'}, + ), + 'good': ( + {'proto':proto, 'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6', 'ret_idx':1}, + {'proto':proto, 'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR', 'ret_idx':1}, + ) }, 'PrivKey': { 'bad': ( - {'wif':1}, - {'wif':'1'}, - {'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'}, - {'s':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'}, - {'pubkey_type':'std'}, - {'s':r32}, - {'s':r32,'compressed':'yes'}, - {'s':r32,'compressed':'yes','pubkey_type':'std'}, - {'s':r32,'compressed':True,'pubkey_type':'nonstd'}, - {'s':r32+b'x','compressed':True,'pubkey_type':'std'} + {'proto':proto, 'wif':1}, + {'proto':proto, 'wif':'1'}, + {'proto':proto, 'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'}, + {'proto':proto, 's':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'}, + {'proto':proto, 'pubkey_type':'std'}, + {'proto':proto, 's':r32}, + {'proto':proto, 's':r32,'compressed':'yes'}, + {'proto':proto, 's':r32,'compressed':'yes','pubkey_type':'std'}, + {'proto':proto, 's':r32,'compressed':True,'pubkey_type':'nonstd'}, + {'proto':proto, 's':r32+b'x','compressed':True,'pubkey_type':'std'} ), 'good': ( - {'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6', + {'proto':proto, 'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6', 'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'}, - {'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR', + {'proto':proto, 'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR', 'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'}, - {'s':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()}, - {'s':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()} + {'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()}, + {'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()} ), }, } diff --git a/test/objtest_py_d/ot_common.py b/test/objtest_py_d/ot_common.py index 75162ae2..cc489bc5 100755 --- a/test/objtest_py_d/ot_common.py +++ b/test/objtest_py_d/ot_common.py @@ -12,4 +12,3 @@ from mmgen.globalvars import g from ..include.common import * r32,r24,r16,r17,r18 = os.urandom(32),os.urandom(24),os.urandom(16),os.urandom(17),os.urandom(18) -tw_pfx = g.proto.base_coin.lower()+':' diff --git a/test/objtest_py_d/ot_ltc_mainnet.py b/test/objtest_py_d/ot_ltc_mainnet.py index 21047b04..0e4bdee6 100755 --- a/test/objtest_py_d/ot_ltc_mainnet.py +++ b/test/objtest_py_d/ot_ltc_mainnet.py @@ -10,40 +10,64 @@ test.objtest_py_d.ot_ltc_mainnet: LTC mainnet test vectors for MMGen data object from mmgen.obj import * from .ot_common import * +from mmgen.protocol import init_proto +proto = init_proto('ltc') + tests = { 'LTCAmt': { 'bad': ('-3.2','0.123456789',123,'123L','88000000',80999999.12345678), 'good': (('80999999.12345678',Decimal('80999999.12345678')),) }, 'CoinAddr': { - 'bad': (1,'x','я'), - 'good': ('LXYx4j8PDGE8GEwDFnEQhcLyHFGsRxSJwt','MEnuCzUGHaQx9fK5WYvLwR1NK4SAo8HmSr'), + 'bad': ( + {'addr':1, 'proto':proto}, + {'addr':'x', 'proto':proto}, + {'addr':'я', 'proto':proto}, + ), + 'good': ( + {'addr':'LXYx4j8PDGE8GEwDFnEQhcLyHFGsRxSJwt', 'proto':proto}, + {'addr':'MEnuCzUGHaQx9fK5WYvLwR1NK4SAo8HmSr', 'proto':proto}, + ), }, 'WifKey': { - 'bad': (1,[],'\0','\1','я','g','gg','FF','f00',r16.hex(),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'), - 'good': ('6udBAGS6B9RfGyvEQDkVDsWy3Kqv9eTULqtEfVkJtTJyHdLvojw', - 'T7kCSp5E71jzV2zEJW4q5qU1SMB5CSz8D9VByxMBkamv1uM3Jjca'), + 'bad': ( + {'proto':proto, 'wif':1}, + {'proto':proto, 'wif':[]}, + {'proto':proto, 'wif':'\0'}, + {'proto':proto, 'wif':'\1'}, + {'proto':proto, 'wif':'я'}, + {'proto':proto, 'wif':'g'}, + {'proto':proto, 'wif':'gg'}, + {'proto':proto, 'wif':'FF'}, + {'proto':proto, 'wif':'f00'}, + {'proto':proto, 'wif':r16.hex()}, + {'proto':proto, 'wif':'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'}, + ), + 'good': ( + {'proto':proto, 'wif':'6udBAGS6B9RfGyvEQDkVDsWy3Kqv9eTULqtEfVkJtTJyHdLvojw', 'ret_idx':1}, + {'proto':proto, 'wif':'T7kCSp5E71jzV2zEJW4q5qU1SMB5CSz8D9VByxMBkamv1uM3Jjca', 'ret_idx':1}, + ) }, 'PrivKey': { 'bad': ( - {'wif':1}, - {'wif':'1'}, - {'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR'}, - {'s':r32,'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh'}, - {'pubkey_type':'std'}, - {'s':r32}, - {'s':r32,'compressed':'yes'}, - {'s':r32,'compressed':'yes','pubkey_type':'std'}, - {'s':r32,'compressed':True,'pubkey_type':'nonstd'}, - {'s':r32+b'x','compressed':True,'pubkey_type':'std'} + {'proto':proto, 'wif':1}, + {'proto':proto, 'wif':'1'}, + {'proto':proto, 'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR'}, + {'proto':proto, 's':r32,'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh'}, + {'proto':proto, 'pubkey_type':'std'}, + {'proto':proto, 's':r32}, + {'proto':proto, 's':r32,'compressed':'yes'}, + {'proto':proto, 's':r32,'compressed':'yes','pubkey_type':'std'}, + {'proto':proto, 's':r32,'compressed':True,'pubkey_type':'nonstd'}, + {'proto':proto, 's':r32+b'x','compressed':True,'pubkey_type':'std'} ), 'good': ( - {'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh', + {'proto':proto, 'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh', 'ret':'470a974ffca9fca1299b706b09142077bea3acbab6d6480b87dbba79d5fd279b'}, - {'wif':'T41Fm7J3mtZLKYPMCLVSFARz4QF8nvSDhLAfW97Ds56Zm9hRJgn8', + {'proto':proto, 'wif':'T41Fm7J3mtZLKYPMCLVSFARz4QF8nvSDhLAfW97Ds56Zm9hRJgn8', 'ret':'1c6feab55a4c3b4ad1823d4ecacd1565c64228c01828cf44fb4db1e2d82c3d56'}, - {'s':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()}, - {'s':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()} + {'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()}, + {'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()} ) }, } diff --git a/test/objtest_py_d/ot_ltc_testnet.py b/test/objtest_py_d/ot_ltc_testnet.py index 1183c413..4b8d3ab4 100755 --- a/test/objtest_py_d/ot_ltc_testnet.py +++ b/test/objtest_py_d/ot_ltc_testnet.py @@ -10,36 +10,60 @@ test.objtest_py_d.ot_ltc_testnet: LTC testnet test vectors for MMGen data object from mmgen.obj import * from .ot_common import * +from mmgen.protocol import init_proto +proto = init_proto('ltc',network='testnet') + tests = { 'CoinAddr': { - 'bad': (1,'x','я'), - 'good': ('n2D3joAy3yE5fqxUeCp38X6uPUcVn7EFw9','QN59YbnHsPQcbKWSq9PmTpjrhBnHGQqRmf') + 'bad': ( + {'addr':1, 'proto':proto}, + {'addr':'x', 'proto':proto}, + {'addr':'я', 'proto':proto}, + ), + 'good': ( + {'addr':'n2D3joAy3yE5fqxUeCp38X6uPUcVn7EFw9', 'proto':proto}, + {'addr':'QN59YbnHsPQcbKWSq9PmTpjrhBnHGQqRmf', 'proto':proto}, + ), }, 'WifKey': { - 'bad': (1,[],'\0','\1','я','g','gg','FF','f00',r16.hex(),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'), - 'good': ('936Fd4qs3Zy2ZiYHH7vZ3UpT23KtCAiGiG2xBTkjHo7jE9aWA2f', - 'cQY3EumdaSNuttvDSUuPdiMYLyw8aVmYfFqxo9kdPuWbJBN4Ny66') + 'bad': ( + {'proto':proto, 'wif':1}, + {'proto':proto, 'wif':[]}, + {'proto':proto, 'wif':'\0'}, + {'proto':proto, 'wif':'\1'}, + {'proto':proto, 'wif':'я'}, + {'proto':proto, 'wif':'g'}, + {'proto':proto, 'wif':'gg'}, + {'proto':proto, 'wif':'FF'}, + {'proto':proto, 'wif':'f00'}, + {'proto':proto, 'wif':r16.hex()}, + {'proto':proto, 'wif':'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'}, + ), + 'good': ( + {'proto':proto, 'wif':'936Fd4qs3Zy2ZiYHH7vZ3UpT23KtCAiGiG2xBTkjHo7jE9aWA2f', 'ret_idx':1}, + {'proto':proto, 'wif':'cQY3EumdaSNuttvDSUuPdiMYLyw8aVmYfFqxo9kdPuWbJBN4Ny66', 'ret_idx':1}, + ) }, 'PrivKey': { 'bad': ( - {'wif':1}, - {'wif':'1'}, - {'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'}, - {'s':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'}, - {'pubkey_type':'std'}, - {'s':r32}, - {'s':r32,'compressed':'yes'}, - {'s':r32,'compressed':'yes','pubkey_type':'std'}, - {'s':r32,'compressed':True,'pubkey_type':'nonstd'}, - {'s':r32+b'x','compressed':True,'pubkey_type':'std'} + {'proto':proto, 'wif':1}, + {'proto':proto, 'wif':'1'}, + {'proto':proto, 'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'}, + {'proto':proto, 's':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'}, + {'proto':proto, 'pubkey_type':'std'}, + {'proto':proto, 's':r32}, + {'proto':proto, 's':r32,'compressed':'yes'}, + {'proto':proto, 's':r32,'compressed':'yes','pubkey_type':'std'}, + {'proto':proto, 's':r32,'compressed':True,'pubkey_type':'nonstd'}, + {'proto':proto, 's':r32+b'x','compressed':True,'pubkey_type':'std'} ), 'good': ( - {'wif':'92iqzh6NqiKawyB1ronw66YtEHrU4rxRJ5T4aHniZqvuSVZS21f', + {'proto':proto, 'wif':'92iqzh6NqiKawyB1ronw66YtEHrU4rxRJ5T4aHniZqvuSVZS21f', 'ret':'95b2aa7912550eacdd3844dcc14bee08ce7bc2434ad4858beb136021e945afeb'}, - {'wif':'cSaJAXBAm9ooHpVJgoxqjDG3AcareFy29Cz8mhnNTRijjv2HLgta', + {'proto':proto, 'wif':'cSaJAXBAm9ooHpVJgoxqjDG3AcareFy29Cz8mhnNTRijjv2HLgta', 'ret':'94fa8b90c11fea8fb907c9376b919534b0a75b9a9621edf71a78753544b4101c'}, - {'s':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()}, - {'s':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()} + {'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()}, + {'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()} ) }, } diff --git a/test/test.py b/test/test.py index cb32712b..ecb43c3c 100755 --- a/test/test.py +++ b/test/test.py @@ -22,8 +22,8 @@ test/test.py: Test suite for the MMGen wallet system def check_segwit_opts(): for k,m in (('segwit','S'),('segwit_random','S'),('bech32','B')): - if getattr(opt,k) and m not in g.proto.mmtypes: - die(1,f'--{k.replace("_","-")} option incompatible with {g.proto.cls_name}') + if getattr(opt,k) and m not in proto.mmtypes: + die(1,f'--{k.replace("_","-")} option incompatible with {proto.cls_name}') def create_shm_dir(data_dir,trash_dir): # Laggy flash media can cause pexpect to fail, so create a temporary directory @@ -140,7 +140,7 @@ If no command is given, the whole test suite is run. } } -data_dir = os.path.join('test','data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))]) +data_dir = get_data_dir() # include/common.py # we need some opt values before running opts.init, so parse without initializing: _uopts = opts.init(opts_data,parse_only=True).user_opts @@ -150,21 +150,35 @@ if not ('resume' in _uopts or 'skip_deps' in _uopts): try: os.unlink(data_dir) except: pass -def get_coin(): - return (_uopts.get('coin') or 'btc').lower() +def add_cmdline_opts(): + """ + These are set automatically now when g.test_suite == True: + --data-dir in opts.init() + --daemon-data-dir and --rpc-port by CoinDaemon() + """ + def get_coin(): + return (_uopts.get('coin') or 'btc').lower() -network_id = get_network_id(get_coin(),bool(_uopts.get('testnet'))) + network_id = get_coin().lower() + ('_tn' if _uopts.get('testnet') else '') -sys.argv.insert(1,'--data-dir=' + data_dir) -sys.argv.insert(1,'--daemon-data-dir=test/daemons/' + get_coin()) -sys.argv.insert(1,'--rpc-port={}'.format(CoinDaemon(network_id,test_suite=True).rpc_port)) + sys.argv.insert(1,'--data-dir=' + data_dir) + sys.argv.insert(1,'--daemon-data-dir=test/daemons/' + get_coin()) + sys.argv.insert(1,'--rpc-port={}'.format(CoinDaemon(network_id,test_suite=True).rpc_port)) + +# add_cmdline_opts() # step 2: opts.init will create new data_dir in ./test (if not 'resume' or 'skip_deps'): usr_args = opts.init(opts_data) +network_id = g.coin.lower() + ('_tn' if opt.testnet else '') + +from mmgen.protocol import init_proto_from_opts +proto = init_proto_from_opts() + # step 3: move data_dir to /dev/shm and symlink it back to ./test: trash_dir = os.path.join('test','trash') -if not ('resume' in _uopts or 'skip_deps' in _uopts): + +if not (opt.resume or opt.skip_deps): shm_dir = create_shm_dir(data_dir,trash_dir) check_segwit_opts() @@ -673,7 +687,7 @@ class TestSuiteRunner(object): if opt.log: self.log_fd.write('[{}][{}:{}] {}\n'.format( - g.coin.lower(), + proto.coin.lower(), self.ts.group_name, self.ts.test_name, cmd_disp)) @@ -699,7 +713,7 @@ class TestSuiteRunner(object): def gen_msg(): yield ('{g}:{c}' if cmd else 'test group {g!r}').format(g=gname,c=cmd) if len(ts_cls.networks) != 1: - yield ' for {} {}'.format(g.proto.coin,g.proto.network) + yield ' for {} {}'.format(proto.coin,proto.network) if segwit_opt: yield ' (--{})'.format(segwit_opt.replace('_','-')) @@ -712,8 +726,8 @@ class TestSuiteRunner(object): # 'networks = ()' means all networks allowed nws = [(e.split('_')[0],'testnet') if '_' in e else (e,'mainnet') for e in ts_cls.networks] if nws: - coin = g.coin.lower() - nw = ('mainnet','testnet')[g.proto.testnet] + coin = proto.coin.lower() + nw = ('mainnet','testnet')[proto.testnet] for a,b in nws: if a == coin and b == nw: break diff --git a/test/test_py_d/ts_autosign.py b/test/test_py_d/ts_autosign.py index 43bf2388..38f000e5 100755 --- a/test/test_py_d/ts_autosign.py +++ b/test/test_py_d/ts_autosign.py @@ -230,7 +230,6 @@ class TestSuiteAutosign(TestSuiteBase): if simulate and not opt.exact_output: rmsg('This command must be run with --exact-output enabled!') return False - network_ids = [c+'_tn' for c in daemon_coins] + daemon_coins start_test_daemons(*network_ids) diff --git a/test/test_py_d/ts_base.py b/test/test_py_d/ts_base.py index fd9eeeed..a7396ede 100755 --- a/test/test_py_d/ts_base.py +++ b/test/test_py_d/ts_base.py @@ -35,16 +35,18 @@ class TestSuiteBase(object): segwit_opts_ok = False def __init__(self,trunner,cfgs,spawn): + from mmgen.protocol import init_proto_from_opts + self.proto = init_proto_from_opts() self.tr = trunner self.cfgs = cfgs self.spawn = spawn self.have_dfl_wallet = False self.usr_rand_chars = (5,30)[bool(opt.usr_random)] self.usr_rand_arg = '-r{}'.format(self.usr_rand_chars) - self.altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin - self.tn_ext = ('','.testnet')[g.proto.testnet] + self.altcoin_pfx = '' if self.proto.base_coin == 'BTC' else '-'+self.proto.base_coin + self.tn_ext = ('','.testnet')[self.proto.testnet] d = {'bch':'btc','btc':'btc','ltc':'ltc'} - self.fork = d[g.coin.lower()] if g.coin.lower() in d else None + self.fork = d[self.proto.coin.lower()] if self.proto.coin.lower() in d else None @property def tmpdir(self): diff --git a/test/test_py_d/ts_chainsplit.py b/test/test_py_d/ts_chainsplit.py index 3e4e76ef..b3ccdaf1 100755 --- a/test/test_py_d/ts_chainsplit.py +++ b/test/test_py_d/ts_chainsplit.py @@ -58,19 +58,20 @@ class TestSuiteChainsplit(TestSuiteRegtest): ) def split_setup(self): - if g.coin != 'BTC': die(1,'Test valid only for coin BTC') - opt.coin = 'BTC' + if self.proto.coin != 'BTC': + die(1,'Test valid only for coin BTC') + self.coin = 'BTC' return self.setup() def split_fork(self): - opt.coin = 'B2X' + self.coin = 'B2X' t = self.spawn('mmgen-regtest',['fork','btc']) t.expect('Creating fork from coin') t.expect('successfully created') t.ok() def split_start(self,coin): - opt.coin = coin + self.coin = coin t = self.spawn('mmgen-regtest',['bob']) t.expect('Starting') t.expect('done') @@ -83,7 +84,7 @@ class TestSuiteChainsplit(TestSuiteRegtest): def split_gen_b2x2(self): self.regtest_generate(coin='B2X') def split_do_split(self): - opt.coin = 'B2X' + self.coin = 'B2X' sid = self.regtest_user_sid('bob') t = self.spawn('mmgen-split',[ '--bob', @@ -105,7 +106,7 @@ class TestSuiteChainsplit(TestSuiteRegtest): def split_sign(self,coin,ext): wf = get_file_with_ext(self.regtest_user_dir('bob',coin=coin.lower()),'mmdat') txfile = self.get_file_with_ext(ext,no_dot=True) - opt.coin = coin + self.coin = coin self.txsign(txfile,wf,extra_opts=['--bob']) def split_sign_b2x(self): @@ -115,7 +116,7 @@ class TestSuiteChainsplit(TestSuiteRegtest): return self.regtest_sign(coin='BTC',ext='9997].rawtx') def split_send(self,coin,ext): - opt.coin = coin + self.coin = coin txfile = self.get_file_with_ext(ext,no_dot=True) self.txsend(txfile,bogus_send=False,extra_opts=['--bob']) @@ -126,7 +127,7 @@ class TestSuiteChainsplit(TestSuiteRegtest): return self.regtest_send(coin='BTC',ext='9997].sigtx') def split_txdo_timelock(self,coin,locktime,bad_locktime): - opt.coin = coin + self.coin = coin sid = self.regtest_user_sid('bob') self.regtest_user_txdo( 'bob','0.0001',[sid+':S:5'],'1',pw=rt_pw, extra_args=['--locktime='+str(locktime)], diff --git a/test/test_py_d/ts_ethdev.py b/test/test_py_d/ts_ethdev.py index 3af48bd2..499d00ba 100755 --- a/test/test_py_d/ts_ethdev.py +++ b/test/test_py_d/ts_ethdev.py @@ -141,6 +141,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): 'Ethereum transacting, token deployment and tracking wallet operations' networks = ('eth','etc') passthru_opts = ('coin',) + extra_spawn_args = ['--regtest=1'] tmpdir_nums = [22] solc_vers = ('0.5.1','0.5.3') # 0.5.1: Raspbian Stretch, 0.5.3: Ubuntu Bionic cmd_group = ( @@ -152,7 +153,9 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): ('addrimport_dev_addr', "importing Parity dev address 'Ox00a329c..'"), ('txcreate1', 'creating a transaction (spend from dev address to address :1)'), + ('txview1_raw', 'viewing the raw transaction'), ('txsign1', 'signing the transaction'), + ('txview1_sig', 'viewing the signed transaction'), ('tx_status0_bad', 'getting the transaction status'), ('txsign1_ni', 'signing the transaction (non-interactive)'), ('txsend1', 'sending the transaction'), @@ -220,8 +223,10 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): ('token_bal1', 'the {} balance and token balance'.format(coin)), ('token_txcreate1', 'creating a token transaction'), + ('token_txview1_raw', 'viewing the raw transaction'), ('token_txsign1', 'signing the transaction'), ('token_txsend1', 'sending the transaction'), + ('token_txview1_sig', 'viewing the signed transaction'), ('tx_status3', 'getting the transaction status'), ('token_bal2', 'the {} balance and token balance'.format(coin)), @@ -301,14 +306,16 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): ) def __init__(self,trunner,cfgs,spawn): + TestSuiteBase.__init__(self,trunner,cfgs,spawn) + from mmgen.protocol import init_proto + self.proto = init_proto(g.coin,network='regtest') from mmgen.daemon import CoinDaemon - self.rpc_port = CoinDaemon(g.coin,test_suite=True).rpc_port + self.rpc_port = CoinDaemon(proto=self.proto,test_suite=True).rpc_port os.environ['MMGEN_BOGUS_WALLET_DATA'] = '' - return TestSuiteBase.__init__(self,trunner,cfgs,spawn) @property def eth_args(self): - return ['--outdir={}'.format(self.tmpdir),'--coin='+g.coin,'--rpc-port={}'.format(self.rpc_port),'--quiet'] + return ['--outdir={}'.format(self.tmpdir),'--coin='+self.proto.coin,'--rpc-port={}'.format(self.rpc_port),'--quiet'] def setup(self): self.spawn('',msg_only=True) @@ -322,15 +329,15 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): from shutil import copytree for d in ('mm1','mm2'): copytree(os.path.join(srcdir,d),os.path.join(self.tmpdir,d)) - restart_test_daemons(g.coin) + restart_test_daemons(self.proto.coin) return 'ok' def wallet_upgrade(self,src_file): - if g.coin == 'ETC': + if self.proto.coin == 'ETC': msg('skipping test {!r} for ETC'.format(self.test_name)) return 'skip' src_dir = joinpath(ref_dir,'ethereum') - dest_dir = joinpath(self.tr.data_dir,'altcoins',g.coin.lower()) + dest_dir = joinpath(self.tr.data_dir,'altcoins',self.proto.coin.lower()) w_from = joinpath(src_dir,src_file) w_to = joinpath(dest_dir,'tracking-wallet.json') os.makedirs(dest_dir,mode=0o750,exist_ok=True) @@ -345,13 +352,12 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): def wallet_upgrade2(self): return self.wallet_upgrade('tracking-wallet-v2.json') def addrgen(self,addrs='1-3,11-13,21-23'): - from mmgen.addr import MMGenAddrType t = self.spawn('mmgen-addrgen', self.eth_args + [dfl_words_file,addrs]) t.written_to_file('Addresses') t.read() return t - def addrimport(self,ext='21-23]{}.addrs',expect='9/9',add_args=[],bad_input=False): + def addrimport(self,ext='21-23]{}.regtest.addrs',expect='9/9',add_args=[],bad_input=False): ext = ext.format('-α' if g.debug_utf8 else '') fn = self.get_file_with_ext(ext,no_dot=True,delete=False) t = self.spawn('mmgen-addrimport', self.eth_args[1:-1] + add_args + [fn]) @@ -379,40 +385,42 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): eth_fee_res = None, fee_res_fs = '0.00105 {} (50 gas price in Gwei)', fee_desc = 'gas price', - no_read = False): - fee_res = fee_res_fs.format(g.coin) + no_read = False, + tweaks = [] ): + fee_res = fee_res_fs.format(self.proto.coin) t = self.spawn('mmgen-'+caller, self.eth_args + ['-B'] + args) t.expect(r'add \[l\]abel, .*?:.','p', regex=True) t.written_to_file('Account balances listing') t = self.txcreate_ui_common( t, menu=menu, caller=caller, input_sels_prompt = 'to spend from', inputs = acct, - file_desc = 'Ethereum transaction', + file_desc = 'transaction', bad_input_sels = True, non_mmgen_inputs = non_mmgen_inputs, interactive_fee = interactive_fee, fee_res = fee_res, fee_desc = fee_desc, eth_fee_res = eth_fee_res, - add_comment = tx_label_jp ) + add_comment = tx_label_jp, + tweaks = tweaks ) if not no_read: t.read() return t - def txsign(self,ni=False,ext='{}.rawtx',add_args=[]): + def txsign(self,ni=False,ext='{}.regtest.rawtx',add_args=[]): ext = ext.format('-α' if g.debug_utf8 else '') keyfile = joinpath(self.tmpdir,parity_key_fn) write_to_file(keyfile,dfl_privkey+'\n') txfile = self.get_file_with_ext(ext,no_dot=True) t = self.spawn( 'mmgen-txsign', - ['--outdir={}'.format(self.tmpdir),'--coin='+g.coin,'--quiet'] + ['--outdir={}'.format(self.tmpdir),'--coin='+self.proto.coin,'--quiet'] + ['--rpc-host=bad_host'] # ETH signing must work without RPC + add_args + ([],['--yes'])[ni] + ['-k', keyfile, txfile, dfl_words_file] ) return self.txsign_ui_common(t,ni=ni,has_label=True) - def txsend(self,ni=False,bogus_send=False,ext='{}.sigtx',add_args=[]): + def txsend(self,ni=False,bogus_send=False,ext='{}.regtest.sigtx',add_args=[]): ext = ext.format('-α' if g.debug_utf8 else '') txfile = self.get_file_with_ext(ext,no_dot=True) if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = '' @@ -421,31 +429,41 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): txid = self.txsend_ui_common(t,quiet=not g.debug,bogus_send=bogus_send,has_label=True) return t + def txview(self,ext_fs): + ext = ext_fs.format('-α' if g.debug_utf8 else '') + txfile = self.get_file_with_ext(ext,no_dot=True) + t = self.spawn( 'mmgen-tool',['--verbose','txview',txfile] ) + t.read() + return t + def txcreate1(self): # valid_keypresses = EthereumTwUnspentOutputs.key_mappings.keys() menu = ['a','d','r','M','X','e','m','m'] # include one invalid keypress, 'X' args = ['98831F3A:E:1,123.456'] - return self.txcreate(args=args,menu=menu,acct='1',non_mmgen_inputs=1) - + return self.txcreate(args=args,menu=menu,acct='1',non_mmgen_inputs=1,tweaks=['confirm_non_mmgen']) + def txview1_raw(self): + return self.txview(ext_fs='{}.regtest.rawtx') def txsign1(self): return self.txsign(add_args=['--use-internal-keccak-module']) def tx_status0_bad(self): - return self.tx_status(ext='{}.sigtx',expect_str='neither in mempool nor blockchain',exit_val=1) + return self.tx_status(ext='{}.regtest.sigtx',expect_str='neither in mempool nor blockchain',exit_val=1) def txsign1_ni(self): return self.txsign(ni=True) def txsend1(self): return self.txsend() + def txview1_sig(self): # do after send so that TxID is displayed + return self.txview(ext_fs='{}.regtest.sigtx') def bal1(self): return self.bal(n='1') def txcreate2(self): args = ['98831F3A:E:11,1.234'] - return self.txcreate(args=args,acct='10',non_mmgen_inputs=1) - def txsign2(self): return self.txsign(ni=True,ext='1.234,50000]{}.rawtx') - def txsend2(self): return self.txsend(ext='1.234,50000]{}.sigtx') + return self.txcreate(args=args,acct='10',non_mmgen_inputs=1,tweaks=['confirm_non_mmgen']) + def txsign2(self): return self.txsign(ni=True,ext='1.234,50000]{}.regtest.rawtx') + def txsend2(self): return self.txsend(ext='1.234,50000]{}.regtest.sigtx') def bal2(self): return self.bal(n='2') def txcreate3(self): args = ['98831F3A:E:21,2.345'] - return self.txcreate(args=args,acct='10',non_mmgen_inputs=1) - def txsign3(self): return self.txsign(ni=True,ext='2.345,50000]{}.rawtx') - def txsend3(self): return self.txsend(ext='2.345,50000]{}.sigtx') + return self.txcreate(args=args,acct='10',non_mmgen_inputs=1,tweaks=['confirm_non_mmgen']) + def txsign3(self): return self.txsign(ni=True,ext='2.345,50000]{}.regtest.rawtx') + def txsend3(self): return self.txsend(ext='2.345,50000]{}.regtest.sigtx') def bal3(self): return self.bal(n='3') def tx_status(self,ext,expect_str,expect_str2='',add_args=[],exit_val=0): @@ -460,10 +478,10 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): return t def tx_status1(self): - return self.tx_status(ext='2.345,50000]{}.sigtx',expect_str='has 1 confirmation') + return self.tx_status(ext='2.345,50000]{}.regtest.sigtx',expect_str='has 1 confirmation') def tx_status1a(self): - return self.tx_status(ext='2.345,50000]{}.sigtx',expect_str='has 2 confirmations') + return self.tx_status(ext='2.345,50000]{}.regtest.sigtx',expect_str='has 2 confirmations') def txcreate4(self): args = ['98831F3A:E:2,23.45495'] @@ -476,7 +494,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): fee_res_fs = fee_res_fs, eth_fee_res = True ) - def txbump(self,ext=',40000]{}.rawtx',fee='50G',add_args=[]): + def txbump(self,ext=',40000]{}.regtest.rawtx',fee='50G',add_args=[]): ext = ext.format('-α' if g.debug_utf8 else '') txfile = self.get_file_with_ext(ext,no_dot=True) t = self.spawn('mmgen-txbump', self.eth_args + add_args + ['--yes',txfile]) @@ -484,23 +502,26 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): t.read() return t - def txsign4(self): return self.txsign(ni=True,ext='.45495,50000]{}.rawtx') - def txsend4(self): return self.txsend(ext='.45495,50000]{}.sigtx') + def txsign4(self): return self.txsign(ni=True,ext='.45495,50000]{}.regtest.rawtx') + def txsend4(self): return self.txsend(ext='.45495,50000]{}.regtest.sigtx') def bal4(self): return self.bal(n='4') def txcreate5(self): args = [burn_addr + ','+amt1] - return self.txcreate(args=args,acct='10',non_mmgen_inputs=1) - def txsign5(self): return self.txsign(ni=True,ext=amt1+',50000]{}.rawtx') - def txsend5(self): return self.txsend(ext=amt1+',50000]{}.sigtx') + return self.txcreate(args=args,acct='10',non_mmgen_inputs=1,tweaks=['confirm_non_mmgen']) + def txsign5(self): return self.txsign(ni=True,ext=amt1+',50000]{}.regtest.rawtx') + def txsend5(self): return self.txsend(ext=amt1+',50000]{}.regtest.sigtx') def bal5(self): return self.bal(n='5') - bal_corr = Decimal('0.0000032') # gas use for token sends varies between ETH and ETC! + #bal_corr = Decimal('0.0000032') # gas use for token sends varies between ETH and ETC! + bal_corr = Decimal('0.0000000') # update: Parity team seems to have corrected this + def bal(self,n=None): t = self.spawn('mmgen-tool', self.eth_args + ['twview','wide=1']) for b in bals[n]: addr,amt,adj = b if len(b) == 3 else b + (False,) - if adj and g.coin == 'ETC': amt = str(Decimal(amt) + Decimal(adj[1]) * self.bal_corr) + if adj and self.proto.coin == 'ETC': + amt = str(Decimal(amt) + Decimal(adj[1]) * self.bal_corr) pat = r'{}\s+{}\s'.format(addr,amt.replace('.',r'\.')) t.expect(pat,regex=True) t.read() @@ -510,7 +531,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): t = self.spawn('mmgen-tool', self.eth_args + ['--token=mm1','twview','wide=1']) for b in token_bals[n]: addr,_amt1,_amt2,adj = b if len(b) == 4 else b + (False,) - if adj and g.coin == 'ETC': + if adj and self.proto.coin == 'ETC': _amt2 = str(Decimal(_amt2) + Decimal(adj[1]) * self.bal_corr) pat = r'{}\s+{}\s+{}\s'.format(addr,_amt1.replace('.',r'\.'),_amt2.replace('.',r'\.')) t.expect(pat,regex=True) @@ -522,7 +543,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): bal1 = token_bals_getbalance[idx][0] bal2 = token_bals_getbalance[idx][1] bal1 = Decimal(bal1) - if etc_adj and g.coin == 'ETC': + if etc_adj and self.proto.coin == 'ETC': bal1 += self.bal_corr t = self.spawn('mmgen-tool', self.eth_args + extra_args + ['getbalance']) t.expect(r'\n[0-9A-F]{8}: .* '+str(bal1),regex=True) @@ -565,7 +586,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): cmd = [ 'scripts/traceback_run.py', 'scripts/create-token.py', - '--coin=' + g.coin, + '--coin=' + self.proto.coin, '--outdir=' + odir ] + cmd_args + [dfl_addr_chk] imsg("Executing: {}".format(' '.join(cmd))) @@ -583,6 +604,13 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): token_data = { 'name':'MMGen Token 2', 'symbol':'MM2', 'supply':10**18, 'decimals':10 } return self.token_compile(token_data) + async def get_exec_status(self,txid): + from mmgen.tx import MMGenTX + tx = MMGenTX.New(proto=self.proto) + from mmgen.rpc import rpc_init + tx.rpc = await rpc_init(self.proto) + return await tx.get_exec_status(txid,True) + async def token_deploy(self,num,key,gas,mmgen_cmd='txdo',tx_fee='8G'): keyfile = joinpath(self.tmpdir,parity_key_fn) fn = joinpath(self.tmpdir,'mm'+str(num),key+'.bin') @@ -596,8 +624,8 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): if mmgen_cmd == 'txdo': args += ['-k',keyfile] t = self.spawn( 'mmgen-'+mmgen_cmd, self.eth_args + args) if mmgen_cmd == 'txcreate': - t.written_to_file('Ethereum transaction') - ext = '[0,8000]{}.rawtx'.format('-α' if g.debug_utf8 else '') + t.written_to_file('transaction') + ext = '[0,8000]{}.regtest.rawtx'.format('-α' if g.debug_utf8 else '') txfile = self.get_file_with_ext(ext,no_dot=True) t = self.spawn('mmgen-txsign', self.eth_args + ['--yes','-k',keyfile,txfile],no_msg=True) self.txsign_ui_common(t,ni=True) @@ -609,12 +637,10 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): quiet = mmgen_cmd == 'txdo' or not g.debug, bogus_send=False) addr = t.expect_getend('Contract address: ') - from mmgen.altcoins.eth.tx import EthereumMMGenTX as etx - assert (await etx.get_exec_status(txid,True)) != 0,( - "Contract '{}:{}' failed to execute. Aborting".format(num,key)) + assert (await self.get_exec_status(txid)) != 0, f'Contract {num}:{key} failed to execute. Aborting' if key == 'Token': - self.write_to_tmpfile('token_addr{}'.format(num),addr+'\n') - imsg('\nToken MM{} deployed!'.format(num)) + self.write_to_tmpfile( f'token_addr{num}', addr+'\n' ) + imsg(f'\nToken MM{num} deployed!') return t async def token_deploy1a(self): return await self.token_deploy(num=1,key='SafeMath',gas=200000) @@ -622,7 +648,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): async def token_deploy1c(self): return await self.token_deploy(num=1,key='Token',gas=1100000,tx_fee='7G') def tx_status2(self): - return self.tx_status(ext=g.coin+'[0,7000]{}.sigtx',expect_str='successfully executed') + return self.tx_status(ext=self.proto.coin+'[0,7000]{}.regtest.sigtx',expect_str='successfully executed') def bal6(self): return self.bal5() @@ -638,17 +664,19 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): sid = dfl_sid from mmgen.tool import MMGenToolCmdWallet usr_mmaddrs = ['{}:E:{}'.format(sid,i) for i in (11,21)] - usr_addrs = [MMGenToolCmdWallet().gen_addr(addr,dfl_words_file) for addr in usr_mmaddrs] + usr_addrs = [MMGenToolCmdWallet(proto=self.proto).gen_addr(addr,dfl_words_file) for addr in usr_mmaddrs] from mmgen.altcoins.eth.contract import TokenResolve from mmgen.altcoins.eth.tx import EthereumMMGenTX as etx - async def do_transfer(): + async def do_transfer(rpc): for i in range(2): tk = await TokenResolve( + self.proto, + rpc, self.read_from_tmpfile(f'token_addr{i+1}').strip() ) imsg_r( '\n' + await tk.info() ) imsg('dev token balance (pre-send): {}'.format(await tk.get_balance(dfl_addr))) - imsg('Sending {} {} to address {} ({})'.format(amt,g.coin,usr_addrs[i],usr_mmaddrs[i])) + imsg('Sending {} {} to address {} ({})'.format(amt,self.proto.dcoin,usr_addrs[i],usr_mmaddrs[i])) from mmgen.obj import ETHAmt txid = await tk.transfer( dfl_addr, @@ -657,22 +685,27 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): dfl_privkey, start_gas = ETHAmt(60000,'wei'), gasPrice = ETHAmt(8,'Gwei') ) - assert (await etx.get_exec_status(txid,True)) != 0,'Transfer of token funds failed. Aborting' + assert (await self.get_exec_status(txid)) != 0,'Transfer of token funds failed. Aborting' - async def show_bals(): + async def show_bals(rpc): for i in range(2): tk = await TokenResolve( + self.proto, + rpc, self.read_from_tmpfile(f'token_addr{i+1}').strip() ) imsg('Token: {}'.format(await tk.get_symbol())) imsg('dev token balance: {}'.format(await tk.get_balance(dfl_addr))) imsg('usr token balance: {} ({} {})'.format( await tk.get_balance(usr_addrs[i]),usr_mmaddrs[i],usr_addrs[i])) + from mmgen.rpc import rpc_init + rpc = await rpc_init(self.proto) + silence() if op == 'show_bals': - await show_bals() + await show_bals(rpc) elif op == 'do_transfer': - await do_transfer() + await do_transfer(rpc) end_silence() return 'ok' @@ -688,19 +721,22 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): return self.addrgen(addrs='21-23') def token_addrimport_badaddr1(self): - t = self.addrimport(ext='[11-13]{}.addrs',add_args=['--token=abc'],bad_input=True) + t = self.addrimport(ext='[11-13]{}.regtest.addrs',add_args=['--token=abc'],bad_input=True) t.req_exit_val = 2 return t def token_addrimport_badaddr2(self): - t = self.addrimport(ext='[11-13]{}.addrs',add_args=['--token='+'00deadbeef'*4],bad_input=True) + t = self.addrimport(ext='[11-13]{}.regtest.addrs',add_args=['--token='+'00deadbeef'*4],bad_input=True) t.req_exit_val = 2 return t def token_addrimport(self,extra_args=[],expect='3/3'): for n,r in ('1','11-13'),('2','21-23'): tk_addr = self.read_from_tmpfile('token_addr'+n).strip() - t = self.addrimport(ext='['+r+']{}.addrs',expect=expect,add_args=['--token='+tk_addr]+extra_args) + t = self.addrimport( + ext = f'[{r}]{{}}.regtest.addrs', + expect = expect, + add_args = ['--token-addr='+tk_addr]+extra_args ) t.p.wait() ok_msg() t.skip_ok = True @@ -719,7 +755,6 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): menu = [], inputs = inputs, input_sels_prompt = 'to spend from', - file_desc = 'Ethereum token transaction', add_comment = tx_label_lat_cyr_gr ) t.read() return t @@ -730,14 +765,18 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): def token_txcreate1(self): return self.token_txcreate(args=['98831F3A:E:12,1.23456'],token='mm1') + def token_txview1_raw(self): + return self.txview(ext_fs='1.23456,50000]{}.regtest.rawtx') def token_txsign1(self): - return self.token_txsign(ext='1.23456,50000]{}.rawtx',token='mm1') + return self.token_txsign(ext='1.23456,50000]{}.regtest.rawtx',token='mm1') def token_txsend1(self): - return self.token_txsend(ext='1.23456,50000]{}.sigtx',token='mm1') + return self.token_txsend(ext='1.23456,50000]{}.regtest.sigtx',token='mm1') + def token_txview1_sig(self): + return self.txview(ext_fs='1.23456,50000]{}.regtest.sigtx') def tx_status3(self): return self.tx_status( - ext='1.23456,50000]{}.sigtx', + ext='1.23456,50000]{}.regtest.sigtx', add_args=['--token=mm1'], expect_str='successfully executed', expect_str2='has 1 confirmation') @@ -756,11 +795,11 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): def token_txcreate2(self): return self.token_txcreate(args=[burn_addr+','+amt2],token='mm1') def token_txbump(self): - return self.txbump(ext=amt2+',50000]{}.rawtx',fee='56G',add_args=['--token=mm1']) + return self.txbump(ext=amt2+',50000]{}.regtest.rawtx',fee='56G',add_args=['--token=mm1']) def token_txsign2(self): - return self.token_txsign(ext=amt2+',50000]{}.rawtx',token='mm1') + return self.token_txsign(ext=amt2+',50000]{}.regtest.rawtx',token='mm1') def token_txsend2(self): - return self.token_txsend(ext=amt2+',50000]{}.sigtx',token='mm1') + return self.token_txsend(ext=amt2+',50000]{}.regtest.sigtx',token='mm1') def token_bal3(self): return self.token_bal(n='3') @@ -785,9 +824,9 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): def txcreate_noamt(self): return self.txcreate(args=['98831F3A:E:12'],eth_fee_res=True) def txsign_noamt(self): - return self.txsign(ext='99.99895,50000]{}.rawtx') + return self.txsign(ext='99.99895,50000]{}.regtest.rawtx') def txsend_noamt(self): - return self.txsend(ext='99.99895,50000]{}.sigtx') + return self.txsend(ext='99.99895,50000]{}.regtest.sigtx') def bal8(self): return self.bal(n='8') def token_bal5(self): return self.token_bal(n='5') @@ -795,9 +834,9 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): def token_txcreate_noamt(self): return self.token_txcreate(args=['98831F3A:E:13'],token='mm1',inputs='2',fee='51G') def token_txsign_noamt(self): - return self.token_txsign(ext='1.23456,51000]{}.rawtx',token='mm1') + return self.token_txsign(ext='1.23456,51000]{}.regtest.rawtx',token='mm1') def token_txsend_noamt(self): - return self.token_txsend(ext='1.23456,51000]{}.sigtx',token='mm1') + return self.token_txsend(ext='1.23456,51000]{}.regtest.sigtx',token='mm1') def bal9(self): return self.bal(n='9') def token_bal6(self): return self.token_bal(n='6') @@ -851,8 +890,12 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): args=['-B','--cached-balances','-i'], total= '1000126.14829832312345678', adj_total=True, - total_coin=g.coin): - if g.coin == 'ETC' and adj_total: + total_coin=None ): + + if total_coin is None: + total_coin = self.proto.coin + + if self.proto.coin == 'ETC' and adj_total: total = str(Decimal(total) + self.bal_corr) t = self.spawn('mmgen-txcreate', self.eth_args + args) for n in bals: @@ -936,5 +979,5 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): def stop(self): self.spawn('',msg_only=True) - stop_test_daemons(g.coin) + stop_test_daemons(self.proto.coin) return 'ok' diff --git a/test/test_py_d/ts_main.py b/test/test_py_d/ts_main.py index 75e77db8..f9bc12be 100755 --- a/test/test_py_d/ts_main.py +++ b/test/test_py_d/ts_main.py @@ -23,6 +23,7 @@ ts_main.py: Basic operations tests for the test.py test suite from mmgen.globalvars import g from mmgen.opts import opt from mmgen.wallet import Wallet,MMGenWallet,MMGenMnemonic,IncogWallet,MMGenSeedFile +from mmgen.rpc import rpc_init from ..include.common import * from .common import * from .ts_base import * @@ -144,21 +145,20 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): ) def __init__(self,trunner,cfgs,spawn): - if g.coin.lower() not in self.networks: + TestSuiteBase.__init__(self,trunner,cfgs,spawn) + if self.proto.coin.lower() not in self.networks: return - from mmgen.rpc import rpc_init - self.rpc = run_session(rpc_init()) + self.rpc = run_session(rpc_init(self.proto)) self.lbl_id = ('account','label')['label_api' in self.rpc.caps] - if g.coin in ('BTC','BCH','LTC'): - self.tx_fee = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[g.coin.lower()] - self.txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[g.coin.lower()] - return TestSuiteBase.__init__(self,trunner,cfgs,spawn) + if self.proto.coin in ('BTC','BCH','LTC'): + self.tx_fee = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[self.proto.coin.lower()] + self.txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[self.proto.coin.lower()] def _get_addrfile_checksum(self,display=False): addrfile = self.get_file_with_ext('addrs') silence() from mmgen.addr import AddrList - chk = AddrList(addrfile).chksum + chk = AddrList(self.proto,addrfile).chksum if opt.verbose and display: msg('Checksum: {}'.format(cyan(chk))) end_silence() return chk @@ -295,20 +295,20 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): sys.stderr.write("Fake transaction wallet data written to file {!r}\n".format(unspent_data_file)) def _create_fake_unspent_entry(self,coinaddr,al_id=None,idx=None,lbl=None,non_mmgen=False,segwit=False): - if 'S' not in g.proto.mmtypes: segwit = False + if 'S' not in self.proto.mmtypes: segwit = False if lbl: lbl = ' ' + lbl k = coinaddr.addr_fmt if not segwit and k == 'p2sh': k = 'p2pkh' s_beg,s_end = { 'p2pkh': ('76a914','88ac'), 'p2sh': ('a914','87'), - 'bech32': (g.proto.witness_vernum_hex + '14','') }[k] - amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[g.coin.lower()] + 'bech32': (self.proto.witness_vernum_hex + '14','') }[k] + amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[self.proto.coin.lower()] ret = { - self.lbl_id: '{}:{}'.format(g.proto.base_coin.lower(),coinaddr) if non_mmgen \ + self.lbl_id: '{}:{}'.format(self.proto.base_coin.lower(),coinaddr) if non_mmgen \ else ('{}:{}{}'.format(al_id,idx,lbl)), 'vout': int(getrandnum(4) % 8), 'txid': os.urandom(32).hex(), - 'amount': g.proto.coin_amt('{}.{}'.format(amt1 + getrandnum(4) % amt2, getrandnum(4) % 100000000)), + 'amount': self.proto.coin_amt('{}.{}'.format(amt1 + getrandnum(4) % amt2, getrandnum(4) % 100000000)), 'address': coinaddr, 'spendable': False, 'scriptPubKey': '{}{}{}'.format(s_beg,coinaddr.hex,s_end), @@ -330,18 +330,20 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): if non_mmgen_input: from mmgen.obj import PrivKey privkey = PrivKey( + self.proto, os.urandom(32), compressed = non_mmgen_input_compressed, pubkey_type = 'std' ) from mmgen.addr import AddrGenerator,KeyGenerator rand_coinaddr = AddrGenerator( + self.proto, 'p2pkh' - ).to_addr(KeyGenerator(g.proto,'std').to_pubhex(privkey)) + ).to_addr(KeyGenerator(self.proto,'std').to_pubhex(privkey)) of = joinpath(self.cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn) write_data_to_file( outfile = of, data = privkey.wif + '\n', - desc = f'compressed {g.proto.name} key', + desc = f'compressed {self.proto.name} key', quiet = True, ignore_opt_outdir = True ) out.append(self._create_fake_unspent_entry(rand_coinaddr,non_mmgen=True,segwit=False)) @@ -351,10 +353,10 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): def _create_tx_data(self,sources,addrs_per_wallet=addrs_per_wallet): from mmgen.addr import AddrData,AddrList from mmgen.obj import AddrIdxList - tx_data,ad = {},AddrData() + tx_data,ad = {},AddrData(self.proto) for s in sources: afile = get_file_with_ext(self.cfgs[s]['tmpdir'],'addrs') - al = AddrList(afile) + al = AddrList(self.proto,afile) ad.add(al) aix = AddrIdxList(fmt_str=self.cfgs[s]['addr_idx_list']) if len(aix) != addrs_per_wallet: @@ -371,13 +373,13 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): def _make_txcreate_cmdline(self,tx_data): from mmgen.obj import PrivKey - privkey = PrivKey(os.urandom(32),compressed=True,pubkey_type='std') - t = ('p2pkh','segwit')['S' in g.proto.mmtypes] + privkey = PrivKey(self.proto,os.urandom(32),compressed=True,pubkey_type='std') + t = ('p2pkh','segwit')['S' in self.proto.mmtypes] from mmgen.addr import AddrGenerator,KeyGenerator - rand_coinaddr = AddrGenerator(t).to_addr(KeyGenerator('std').to_pubhex(privkey)) + rand_coinaddr = AddrGenerator(self.proto,t).to_addr(KeyGenerator(self.proto,'std').to_pubhex(privkey)) # total of two outputs must be < 10 BTC (<1000 LTC) - mods = {'btc':(6,4),'bch':(6,4),'ltc':(600,400)}[g.coin.lower()] + mods = {'btc':(6,4),'bch':(6,4),'ltc':(600,400)}[self.proto.coin.lower()] for k in self.cfgs: self.cfgs[k]['amts'] = [None,None] for idx,mod in enumerate(mods): @@ -405,7 +407,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): view = 'n', addrs_per_wallet = addrs_per_wallet, non_mmgen_input_compressed = True, - cmdline_inputs = False ) + cmdline_inputs = False, + tweaks = [] ): if opt.verbose or opt.exact_output: sys.stderr.write(green('Generating fake tracking wallet info\n')) @@ -415,13 +418,15 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): dfake = self._create_fake_unspent_data(ad,tx_data,non_mmgen_input,non_mmgen_input_compressed) self._write_fake_data_to_file(repr(dfake)) cmd_args = self._make_txcreate_cmdline(tx_data) + if cmdline_inputs: from mmgen.tx import TwLabel cmd_args = ['--inputs={},{},{},{},{},{}'.format( - TwLabel(dfake[0][self.lbl_id]).mmid,dfake[1]['address'], - TwLabel(dfake[2][self.lbl_id]).mmid,dfake[3]['address'], - TwLabel(dfake[4][self.lbl_id]).mmid,dfake[5]['address'] + TwLabel(self.proto,dfake[0][self.lbl_id]).mmid,dfake[1]['address'], + TwLabel(self.proto,dfake[2][self.lbl_id]).mmid,dfake[3]['address'], + TwLabel(self.proto,dfake[4][self.lbl_id]).mmid,dfake[5]['address'] ),'--outdir='+self.tr.trash_dir] + cmd_args[1:] + end_silence() if opt.verbose or opt.exact_output: @@ -429,10 +434,10 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): t = self.spawn( 'mmgen-'+('txcreate','txdo')[bool(txdo_args)], - ([],['--rbf'])[g.proto.cap('rbf')] + + ([],['--rbf'])[self.proto.cap('rbf')] + ['-f',self.tx_fee,'-B'] + add_args + cmd_args + txdo_args) - if t.expect([('Get','Transac')[cmdline_inputs],'Unable to connect to \S+'],regex=True) == 1: + if t.expect([('Get','Unsigned transac')[cmdline_inputs],'Unable to connect to \S+'],regex=True) == 1: raise TestSuiteException('\n'+t.p.after) if cmdline_inputs: @@ -441,9 +446,6 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): t.license() - if txdo_args and add_args: # txdo4 - t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd']) - for num in tx_data: t.expect_getend('ting address data from file ') chk=t.expect_getend(r'Checksum for address data .*?: ',regex=True) @@ -462,7 +464,11 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): inputs = ' '.join(map(str,outputs_list)), add_comment = ('',tx_label_lat_cyr_gr)[do_label], non_mmgen_inputs = (0,1)[bool(non_mmgen_input and not txdo_args)], - view = view ) + view = view, + tweaks = tweaks ) + + if txdo_args and add_args: # txdo4 + t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd']) return t @@ -470,7 +476,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): return self.txcreate_common(sources=['1'],add_args=['--vsize-adj=1.01']) def txbump(self,txfile,prepend_args=[],seed_args=[]): - if not g.proto.cap('rbf'): + if not self.proto.cap('rbf'): msg('Skipping RBF'); return 'skip' args = prepend_args + ['--quiet','--outdir='+self.tmpdir,txfile] + seed_args t = self.spawn('mmgen-txbump',args) @@ -490,8 +496,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): t.written_to_file('Transaction') else: t.do_comment(False) - t.expect('Save transaction? (y/N): ','y') - t.written_to_file('Transaction') + t.expect('Save fee-bumped transaction? (y/N): ','y') + t.written_to_file('Fee-bumped transaction') os.unlink(txfile) # our tx file replaces the original cmd = 'touch ' + joinpath(self.tmpdir,'txbump') os.system(cmd) @@ -619,8 +625,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): def txsign_keyaddr(self,keyaddr_file,txfile): t = self.spawn('mmgen-txsign', ['-d',self.tmpdir,'-p1','-M',keyaddr_file,txfile]) t.license() - t.do_decrypt_ka_data(hp='1',pw=self.kapasswd) t.view_tx('n') + t.do_decrypt_ka_data(hp='1',pw=self.kapasswd) self.txsign_end(t) return t @@ -694,7 +700,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): sources = ['1', '2', '3', '4', '14'], non_mmgen_input = '4', do_label = True, - view = 'y' ) + view = 'y', + tweaks = ['confirm_non_mmgen'] ) def txsign4(self,f1,f2,f3,f4,f5,f6): non_mm_file = joinpath(self.tmpdir,non_mmgen_fn) @@ -708,8 +715,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): f1, f2, f3, f4, f5 ] t = self.spawn('mmgen-txsign',add_args) t.license() - t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd']) t.view_tx('t') + t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd']) for cnum,wcls in (('1',IncogWallet),('3',MMGenWallet)): t.passphrase('{}'.format(wcls.desc),self.cfgs[cnum]['wpasswd']) @@ -754,7 +761,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): return self.txcreate_common( sources = ['20'], non_mmgen_input = '20', - non_mmgen_input_compressed = False ) + non_mmgen_input_compressed = False, + tweaks = ['confirm_non_mmgen'] ) def txsign5(self,wf,txf,bad_vsize=True,add_args=[]): non_mm_file = joinpath(self.tmpdir,non_mmgen_fn) @@ -786,7 +794,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): sources = ['21'], non_mmgen_input = '21', non_mmgen_input_compressed = False, - add_args = ['--vsize-adj=1.08'] ) + add_args = ['--vsize-adj=1.08'], + tweaks = ['confirm_non_mmgen'] ) def txsign6(self,txf,wf): return self.txsign5(txf,wf,bad_vsize=False,add_args=['--vsize-adj=1.08']) diff --git a/test/test_py_d/ts_misc.py b/test/test_py_d/ts_misc.py index ae632163..cc38f2c3 100755 --- a/test/test_py_d/ts_misc.py +++ b/test/test_py_d/ts_misc.py @@ -137,7 +137,7 @@ class TestSuiteRefTX(TestSuiteMain,TestSuiteBase): return TestSuiteMain.__init__(self,trunner,cfgs,spawn) def ref_tx_addrgen(self,atype): - if atype not in g.proto.mmtypes: + if atype not in self.proto.mmtypes: return t = self.spawn('mmgen-addrgen',['--outdir='+self.tmpdir,'--type='+atype,dfl_words_file,'1-2']) t.read() @@ -150,8 +150,8 @@ class TestSuiteRefTX(TestSuiteMain,TestSuiteBase): def ref_tx_txcreate(self,f1,f2,f3,f4): sources = ['31','32'] - if 'S' in g.proto.mmtypes: sources += ['33'] - if 'B' in g.proto.mmtypes: sources += ['34'] + if 'S' in self.proto.mmtypes: sources += ['33'] + if 'B' in self.proto.mmtypes: sources += ['34'] return self.txcreate_common( addrs_per_wallet = 2, sources = sources, diff --git a/test/test_py_d/ts_ref.py b/test/test_py_d/ts_ref.py index 8b22a2fd..a515ec9a 100755 --- a/test/test_py_d/ts_ref.py +++ b/test/test_py_d/ts_ref.py @@ -31,7 +31,6 @@ from .ts_base import * from .ts_shared import * wpasswd = 'reference password' -nw_name = '{} {}'.format(g.coin,('Mainnet','Testnet')[g.proto.testnet]) class TestSuiteRef(TestSuiteBase,TestSuiteShared): 'saved reference address, password and transaction files' @@ -136,6 +135,10 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): ('ref_tool_decrypt', 'decryption of saved MMGen-encrypted file'), ) + @property + def nw_desc(self): + return '{} {}'.format(self.proto.coin,('Mainnet','Testnet')[self.proto.testnet]) + def _get_ref_subdir_by_coin(self,coin): return {'btc': '', 'bch': '', @@ -148,7 +151,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): @property def ref_subdir(self): - return self._get_ref_subdir_by_coin(g.coin) + return self._get_ref_subdir_by_coin(self.proto.coin) def ref_words_to_subwallet_chk1(self): return self.ref_words_to_subwallet_chk('32L') @@ -209,7 +212,9 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): mmtype = None, add_args = [], id_key = None, - pat = '{}.*Legacy'.format(nw_name)): + pat = None ): + + pat = pat or f'{self.nw_desc}.*Legacy' af_key = 'ref_{}file'.format(ftype) + ('_' + id_key if id_key else '') af_fn = TestSuiteRef.sources[af_key].format(pfx or self.altcoin_pfx,'' if coin else self.tn_ext) af = joinpath(ref_dir,(subdir or self.ref_subdir,'')[ftype=='passwd'],af_fn) @@ -220,7 +225,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): t.do_decrypt_ka_data(hp=ref_kafile_hash_preset,pw=ref_kafile_pass,have_yes_opt=True) chksum_key = '_'.join([af_key,'chksum'] + ([coin.lower()] if coin else []) + ([mmtype] if mmtype else [])) rc = self.chk_data[chksum_key] - ref_chksum = rc if (ftype == 'passwd' or coin) else rc[g.proto.base_coin.lower()][g.proto.testnet] + ref_chksum = rc if (ftype == 'passwd' or coin) else rc[self.proto.base_coin.lower()][self.proto.testnet] if pat: t.expect(pat,regex=True) t.expect(chksum_pat,regex=True) @@ -230,14 +235,14 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): return t def ref_segwitaddrfile_chk(self): - if not 'S' in g.proto.mmtypes: - return skip(f'not supported by {g.protocol.cls_name} protocol') - return self.ref_addrfile_chk(ftype='segwitaddr',pat='{}.*Segwit'.format(nw_name)) + if not 'S' in self.proto.mmtypes: + return skip(f'not supported by {self.proto.cls_name} protocol') + return self.ref_addrfile_chk(ftype='segwitaddr',pat='{}.*Segwit'.format(self.nw_desc)) def ref_bech32addrfile_chk(self): - if not 'B' in g.proto.mmtypes: - return skip(f'not supported by {g.protocol.cls_name} protocol') - return self.ref_addrfile_chk(ftype='bech32addr',pat='{}.*Bech32'.format(nw_name)) + if not 'B' in self.proto.mmtypes: + return skip(f'not supported by {self.proto.cls_name} protocol') + return self.ref_addrfile_chk(ftype='bech32addr',pat='{}.*Bech32'.format(self.nw_desc)) def ref_keyaddrfile_chk(self): return self.ref_addrfile_chk(ftype='keyaddr') @@ -259,7 +264,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): def ref_passwdfile_chk_hex2bip39_12(self): return self.ref_passwdfile_chk(key='hex2bip39_12',pat=r'BIP39.*len.* 12\b') def ref_tx_chk(self): - fn = self.sources['ref_tx_file'][g.coin.lower()][bool(self.tn_ext)] + fn = self.sources['ref_tx_file'][self.proto.coin.lower()][bool(self.tn_ext)] if not fn: return tf = joinpath(ref_dir,self.ref_subdir,fn) wf = dfl_words_file diff --git a/test/test_py_d/ts_ref_altcoin.py b/test/test_py_d/ts_ref_altcoin.py index aca24ef7..6190b274 100755 --- a/test/test_py_d/ts_ref_altcoin.py +++ b/test/test_py_d/ts_ref_altcoin.py @@ -77,44 +77,42 @@ class TestSuiteRefAltcoin(TestSuiteRef,TestSuiteBase): ('ref_keyaddrfile_chk_zec_z','reference key-address file (ZEC-Z)'), ('ref_keyaddrfile_chk_xmr', 'reference key-address file (XMR)'), ) - # Check saved transaction files for *all* configured altcoins - # Though this basically duplicates the autosign test, here we do everything - # via the command line, so it's worth doing + def ref_altcoin_tx_chk(self): + """ + Check saved transaction files for *all* configured altcoins + Though this basically duplicates the autosign test, here we do everything + via the command line, so it's worth doing + """ self.write_to_tmpfile(pwfile,dfl_wpasswd) - pf = joinpath(self.tmpdir,pwfile) - from mmgen.protocol import init_proto - from mmgen.daemon import CoinDaemon - for k in ('bch','eth','mm1','etc'): - coin,token = ('eth','mm1') if k == 'mm1' else (k,None) - ref_subdir = self._get_ref_subdir_by_coin(coin) - for tn in (False,True): - extra_opts = ['--coin='+coin,f'--testnet={int(tn)}'] - if tn and coin == 'etc': + passfile = joinpath(self.tmpdir,pwfile) + from mmgen.txfile import MMGenTxFile + src = TestSuiteRef.sources['ref_tx_file'] + for coin,files in src.items(): + if coin == 'mm1': + coin = 'eth' + token_desc = ':MM1' + else: + token_desc = '' + for fn in files: + if not fn: # no etc testnet TX file continue - if coin == 'bch': - network_id = get_network_id('bch',tn) - start_test_daemons(network_id) - extra_opts += [ - '--daemon-data-dir=test/daemons/bch', - '--rpc-port={}'.format(CoinDaemon(network_id,test_suite=True).rpc_port) ] - g.proto = init_proto(coin,testnet=tn) - fn = TestSuiteRef.sources['ref_tx_file'][token or coin][bool(tn)] - tf = joinpath(ref_dir,ref_subdir,fn) - wf = dfl_words_file - if token: - extra_opts += ['--token='+token] - t = self.txsign(wf, tf, pf, - save = False, - has_label = True, - extra_desc = '({}{})'.format(token or coin,' testnet' if tn else ''), - extra_opts = extra_opts ) - if coin == 'bch': - stop_test_daemons(network_id) - ok_msg() - g.proto = init_proto('btc') - t.skip_ok = True - return t + txfile = joinpath( + ref_dir, + self._get_ref_subdir_by_coin(coin), + fn ) + proto = MMGenTxFile.get_proto(txfile,quiet_open=True) + if proto.sign_mode == 'daemon': + start_test_daemons(proto.network_id) + t = self.spawn( + 'mmgen-txsign', + ['--yes', f'--passwd-file={passfile}', dfl_words_file, txfile], + extra_desc = f'{proto.coin}{token_desc} {proto.network}') + t.read() + t.ok() + if proto.sign_mode == 'daemon': + stop_test_daemons(proto.network_id) + return 'ok' def ref_altcoin_addrgen(self,coin,mmtype,gen_what='addr',coin_suf='',add_args=[]): wf = dfl_words_file diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index 80073473..ad615922 100755 --- a/test/test_py_d/ts_regtest.py +++ b/test/test_py_d/ts_regtest.py @@ -138,6 +138,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): 'transacting and tracking wallet operations via regtest mode' networks = ('btc','ltc','bch') passthru_opts = ('coin',) + extra_spawn_args = ['--regtest=1'] tmpdir_nums = [17] cmd_group = ( ('setup', 'regtest (Bob and Alice) mode setup'), @@ -244,18 +245,19 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): usr_subsids = { 'bob': {}, 'alice': {} } def __init__(self,trunner,cfgs,spawn): + TestSuiteBase.__init__(self,trunner,cfgs,spawn) os.environ['MMGEN_TEST_SUITE_REGTEST'] = '1' - from mmgen.regtest import MMGenRegtest - rt = MMGenRegtest(g.coin) - coin = g.coin.lower() + if self.proto.testnet: + die(2,'--testnet and --regtest options incompatible with regtest test suite') + self.proto = init_proto(self.proto.coin,network='regtest') + coin = self.proto.coin.lower() for k in rt_data: globals()[k] = rt_data[k][coin] if coin in rt_data[k] else None - return TestSuiteBase.__init__(self,trunner,cfgs,spawn) def _add_comments_to_addr_file(self,addrfile,outfile,use_labels=False): silence() gmsg("Adding comments to address file '{}'".format(addrfile)) - a = AddrList(addrfile) + a = AddrList(self.proto,addrfile) for n,idx in enumerate(a.idxs(),1): if use_labels: a.set_comment(idx,get_label()) @@ -267,8 +269,6 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): def setup(self): os.environ['MMGEN_BOGUS_WALLET_DATA'] = '' - if g.proto.testnet: - die(2,'--testnet option incompatible with regtest test suite') try: shutil.rmtree(joinpath(self.tr.data_dir,'regtest')) except: pass os.environ['MMGEN_TEST_SUITE'] = '' # mnemonic is piped to stdin, so stop being a terminal @@ -295,7 +295,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): def walletgen_alice(self): return self.walletgen('alice') def _user_dir(self,user,coin=None): - return joinpath(self.tr.data_dir,'regtest',coin or g.coin.lower(),user) + return joinpath(self.tr.data_dir,'regtest',coin or self.proto.coin.lower(),user) def _user_sid(self,user): return os.path.basename(get_file_with_ext(self._user_dir(user),'mmdat'))[:8] @@ -315,7 +315,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): def addrgen(self,user,wf=None,addr_range='1-5',subseed_idx=None,mmtypes=[]): from mmgen.addr import MMGenAddrType - for mmtype in mmtypes or g.proto.mmtypes: + for mmtype in mmtypes or self.proto.mmtypes: t = self.spawn('mmgen-addrgen', ['--quiet','--'+user,'--type='+mmtype,'--outdir={}'.format(self._user_dir(user))] + ([wf] if wf else []) + @@ -335,17 +335,14 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): id_strs = { 'legacy':'', 'compressed':'-C', 'segwit':'-S', 'bech32':'-B' } if not sid: sid = self._user_sid(user) from mmgen.addr import MMGenAddrType - for mmtype in mmtypes or g.proto.mmtypes: + for mmtype in mmtypes or self.proto.mmtypes: desc = MMGenAddrType.mmtypes[mmtype].name addrfile = joinpath(self._user_dir(user), - '{}{}{}[{}]{x}.testnet.addrs'.format( + '{}{}{}[{}]{x}.regtest.addrs'.format( sid,self.altcoin_pfx,id_strs[desc],addr_range, x='-α' if g.debug_utf8 else '')) - if mmtype == g.proto.mmtypes[0] and user == 'bob': - psave = g.proto - g.proto = init_proto(g.coin,regtest=True) + if mmtype == self.proto.mmtypes[0] and user == 'bob': self._add_comments_to_addr_file(addrfile,addrfile,use_labels=True) - g.proto = psave t = self.spawn( 'mmgen-addrimport', ['--quiet', '--'+user, '--batch', addrfile], extra_desc='({})'.format(desc)) @@ -365,7 +362,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): if not sid: sid = self._user_sid(user) addr = self.get_addr_from_addrlist(user,sid,mmtype,0,addr_range=addr_range) t = self.spawn('mmgen-regtest', ['send',str(addr),str(amt)]) - t.expect('Sending {} miner {}'.format(amt,g.coin)) + t.expect(f'Sending {amt} miner {self.proto.coin}') t.expect('Mined 1 block') return t @@ -373,7 +370,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): return self.fund_wallet('bob','C',rtFundAmt) def fund_alice(self): - return self.fund_wallet('alice',('L','S')[g.proto.cap('segwit')],rtFundAmt) + return self.fund_wallet('alice',('L','S')[self.proto.cap('segwit')],rtFundAmt) def user_twview(self,user,chk=None,sort='age'): t = self.spawn('mmgen-tool',['--'+user,'twview','sort='+sort]) @@ -388,8 +385,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): if skip_check: t.read() else: - total = t.expect_getend('TOTAL: ') - cmp_or_die('{} {}'.format(bal,g.coin),total) + cmp_or_die(f'{bal} {self.proto.coin}',t.expect_getend('TOTAL: ')) t.req_exit_val = exit_val return t @@ -451,9 +447,9 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): def bob_subwallet_fund(self): sid1 = self._get_user_subsid('bob','29L') sid2 = self._get_user_subsid('bob','127S') - chg_addr = self._user_sid('bob') + (':B:1',':L:1')[g.coin=='BCH'] + chg_addr = self._user_sid('bob') + (':B:1',':L:1')[self.proto.coin=='BCH'] outputs_cl = [sid1+':C:2,0.29',sid2+':C:3,0.127',chg_addr] - inputs = ('3','1')[g.coin=='BCH'] + inputs = ('3','1')[self.proto.coin=='BCH'] return self.user_txdo('bob',rtFee[1],outputs_cl,inputs,extra_args=['--subseeds=127']) def bob_twview2(self): @@ -471,7 +467,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): t = self.spawn('mmgen-txcreate',['-d',self.tmpdir,'-B','--bob'] + outputs_cl) return self.txcreate_ui_common(t, menu = ['a'], - inputs = ('1,2','2,3')[g.coin=='BCH'], + inputs = ('1,2','2,3')[self.proto.coin=='BCH'], interactive_fee = '0.00001') def bob_subwallet_txsign(self): @@ -486,12 +482,12 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): def bob_subwallet_txdo(self): outputs_cl = [self._user_sid('bob')+':L:5'] - inputs = ('1,2','2,3')[g.coin=='BCH'] + inputs = ('1,2','2,3')[self.proto.coin=='BCH'] return self.user_txdo('bob',rtFee[5],outputs_cl,inputs,menu=['a'],extra_args=['--subseeds=127']) # sort: amt def bob_twview4(self): sid = self._user_sid('bob') - amt = ('0.4169328','0.41364')[g.coin=='LTC'] + amt = ('0.4169328','0.41364')[self.proto.coin=='LTC'] return self.user_twview('bob',chk=r'\b{}:L:5\b\s+.*\s+\b{}\b'.format(sid,amt),sort='twmmid') def bob_getbalance(self,bals,confs=1): @@ -499,7 +495,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): assert Decimal(bals['mmgen'][i]) + Decimal(bals['nonmm'][i]) == Decimal(bals['total'][i]) t = self.spawn('mmgen-tool',['--bob','getbalance','minconf={}'.format(confs)]) for k in ('mmgen','nonmm','total'): - t.expect(r'\n\S+:\s+{} {c}\s+{} {c}\s+{} {c}'.format(*bals[k],c=g.coin),regex=True) + t.expect(r'\n\S+:\s+{} {c}\s+{} {c}\s+{} {c}'.format(*bals[k],c=self.proto.coin),regex=True) t.read() return t @@ -566,15 +562,12 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): def get_addr_from_addrlist(self,user,sid,mmtype,idx,addr_range='1-5'): id_str = { 'L':'', 'S':'-S', 'C':'-C', 'B':'-B' }[mmtype] - ext = '{}{}{}[{}]{x}.testnet.addrs'.format( + ext = '{}{}{}[{}]{x}.regtest.addrs'.format( sid,self.altcoin_pfx,id_str,addr_range,x='-α' if g.debug_utf8 else '') addrfile = get_file_with_ext(self._user_dir(user),ext,no_dot=True) - psave = g.proto - g.proto = init_proto(g.coin,regtest=True) silence() - addr = AddrList(addrfile).data[idx].addr + addr = AddrList(self.proto,addrfile).data[idx].addr end_silence() - g.proto = psave return addr def _create_tx_outputs(self,user,data): @@ -582,16 +575,16 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): return [self.get_addr_from_addrlist(user,sid,mmtype,idx-1)+amt_str for mmtype,idx,amt_str in data] def bob_rbf_1output_create(self): - if g.coin != 'BTC': - return 'skip' # non-coin-dependent test, so run just once for BTC + if self.proto.coin != 'BTC': # non-coin-dependent test, so run just once for BTC + return 'skip' out_addr = self._create_tx_outputs('alice',(('B',5,''),)) t = self.spawn('mmgen-txcreate',['-d',self.tr.trash_dir,'-B','--bob','--rbf'] + out_addr) return self.txcreate_ui_common(t,menu=[],inputs='3',interactive_fee='3s') # out amt: 199.99999343 def bob_rbf_1output_bump(self): - if g.coin != 'BTC': + if self.proto.coin != 'BTC': return 'skip' - ext = '9343,3]{x}.testnet.rawtx'.format(x='-α' if g.debug_utf8 else '') + ext = '9343,3]{x}.regtest.rawtx'.format(x='-α' if g.debug_utf8 else '') txfile = get_file_with_ext(self.tr.trash_dir,ext,delete=False,no_dot=True) return self.user_txbump('bob', self.tr.trash_dir, @@ -605,12 +598,12 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): outputs_cl = self._create_tx_outputs('alice',(('L',1,',60'),('C',1,',40'))) # alice_sid:L:1, alice_sid:C:1 outputs_cl += [self._user_sid('bob')+':'+rtBobOp3] return self.user_txdo('bob',rtFee[1],outputs_cl,'3', - extra_args=([],['--rbf'])[g.proto.cap('rbf')]) + extra_args=([],['--rbf'])[self.proto.cap('rbf')]) def bob_send_non_mmgen(self): outputs_cl = self._create_tx_outputs('alice',( - (('L','S')[g.proto.cap('segwit')],2,',10'), - (('L','S')[g.proto.cap('segwit')],3,'') + (('L','S')[self.proto.cap('segwit')],2,',10'), + (('L','S')[self.proto.cap('segwit')],3,'') )) # alice_sid:S:2, alice_sid:S:3 keyfile = joinpath(self.tmpdir,'non-mmgen.keys') return self.user_txdo('bob',rtFee[3],outputs_cl,'1,4-10', @@ -621,7 +614,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): return self.user_txdo('alice',None,outputs_cl,'1') # fee=None def user_txbump(self,user,outdir,txfile,fee,add_args=[],has_label=True,signed_tx=True,one_output=False): - if not g.proto.cap('rbf'): + if not self.proto.cap('rbf'): return 'skip' os.environ['MMGEN_BOGUS_SEND'] = '' t = self.spawn('mmgen-txbump', @@ -636,19 +629,18 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): t.written_to_file('Signed transaction') self.txsend_ui_common(t,caller='txdo',bogus_send=False,file_desc='Signed transaction') else: - t.expect('Save transaction? (y/N): ','y') - t.written_to_file('Transaction') + t.expect('Save fee-bumped transaction? (y/N): ','y') + t.written_to_file('Fee-bumped transaction') t.read() return t def bob_rbf_bump(self): - ext = ',{}]{x}.testnet.sigtx'.format(rtFee[1][:-1],x='-α' if g.debug_utf8 else '') + ext = ',{}]{x}.regtest.sigtx'.format(rtFee[1][:-1],x='-α' if g.debug_utf8 else '') txfile = self.get_file_with_ext(ext,delete=False,no_dot=True) return self.user_txbump('bob',self.tmpdir,txfile,rtFee[2],add_args=['--send']) def generate(self,coin=None,num_blocks=1): int(num_blocks) - if coin: opt.coin = coin t = self.spawn('mmgen-regtest',['generate',str(num_blocks)]) t.expect('Mined {} block'.format(num_blocks)) return t @@ -670,19 +662,19 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): return 'ok' def bob_rbf_status(self,fee,exp1,exp2=''): - if not g.proto.cap('rbf'): + if not self.proto.cap('rbf'): return 'skip' - ext = ',{}]{x}.testnet.sigtx'.format(fee[:-1],x='-α' if g.debug_utf8 else '') + ext = ',{}]{x}.regtest.sigtx'.format(fee[:-1],x='-α' if g.debug_utf8 else '') txfile = self.get_file_with_ext(ext,delete=False,no_dot=True) return self.user_txsend_status('bob',txfile,exp1,exp2) def bob_rbf_status1(self): - if not g.proto.cap('rbf'): + if not self.proto.cap('rbf'): return 'skip' return self.bob_rbf_status(rtFee[1],'in mempool, replaceable') def get_mempool2(self): - if not g.proto.cap('rbf'): + if not self.proto.cap('rbf'): return 'skip' mp = self._get_mempool() if len(mp) != 1: @@ -694,19 +686,19 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): return 'ok' def bob_rbf_status2(self): - if not g.proto.cap('rbf'): + if not self.proto.cap('rbf'): return 'skip' new_txid = self.read_from_tmpfile('rbf_txid2').strip() return self.bob_rbf_status(rtFee[1], 'Transaction has been replaced','{} in mempool'.format(new_txid)) def bob_rbf_status3(self): - if not g.proto.cap('rbf'): + if not self.proto.cap('rbf'): return 'skip' return self.bob_rbf_status(rtFee[2],'status: in mempool, replaceable') def bob_rbf_status4(self): - if not g.proto.cap('rbf'): + if not self.proto.cap('rbf'): return 'skip' new_txid = self.read_from_tmpfile('rbf_txid2').strip() return self.bob_rbf_status(rtFee[1], @@ -714,12 +706,12 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): 'Replacing transactions:\s+{}'.format(new_txid)) def bob_rbf_status5(self): - if not g.proto.cap('rbf'): + if not self.proto.cap('rbf'): return 'skip' return self.bob_rbf_status(rtFee[2],'Transaction has 1 confirmation') def bob_rbf_status6(self): - if not g.proto.cap('rbf'): + if not self.proto.cap('rbf'): return 'skip' new_txid = self.read_from_tmpfile('rbf_txid2').strip() return self.bob_rbf_status(rtFee[1], @@ -730,7 +722,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): def _gen_pairs(n): disable_debug() from subprocess import run,PIPE - ret = [run(['python3',joinpath('cmds','mmgen-tool'),'--testnet=1'] + + ret = [run(['python3',joinpath('cmds','mmgen-tool'),'--regtest=1'] + (['--type=compressed'],[])[i==0] + ['-r0','randpair'], stdout=PIPE,check=True @@ -771,8 +763,8 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): outputs1 = list(map('{},{}'.format,addrs,amts)) sid = self._user_sid('bob') l1,l2 = ( - (':S',':B') if 'B' in g.proto.mmtypes else - (':S',':S') if g.proto.cap('segwit') else + (':S',':B') if 'B' in self.proto.mmtypes else + (':S',':S') if self.proto.cap('segwit') else (':L',':L') ) outputs2 = [sid+':C:2,6.333', sid+':L:3,6.667',sid+l1+':4,0.123',sid+l2+':5'] return self.user_txdo('bob',rtFee[5],outputs1+outputs2,'1-2') @@ -799,20 +791,20 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): sid = self._user_sid('alice') return self.user_add_label('alice',sid+':C:1','Replacement Label') - def alice_add_label_coinaddr(self): - mmid = self._user_sid('alice') + (':S:1',':L:1')[g.coin=='BCH'] - t = self.spawn('mmgen-tool',['--alice','listaddress',mmid],no_msg=True) - btcaddr = [i for i in t.read().splitlines() if i.lstrip()[0:len(mmid)] == mmid][0].split()[1] - return self.user_add_label('alice',btcaddr,'Label added using coin address') - - def user_chk_label(self,user,addr,label): + def _user_chk_label(self,user,addr,label): t = self.spawn('mmgen-tool',['--'+user,'listaddresses','all_labels=1']) t.expect(r'{}\s+\S{{30}}\S+\s+{}\s+'.format(addr,label),regex=True) return t + def alice_add_label_coinaddr(self): + mmid = self._user_sid('alice') + (':S:1',':L:1')[self.proto.coin=='BCH'] + t = self.spawn('mmgen-tool',['--alice','listaddress',mmid],no_msg=True) + addr = [i for i in t.read().splitlines() if i.startswith(mmid)][0].split()[1] + return self.user_add_label('alice',addr,'Label added using coin address of MMGen address') + def alice_chk_label_coinaddr(self): - mmid = self._user_sid('alice') + (':S:1',':L:1')[g.coin=='BCH'] - return self.user_chk_label('alice',mmid,'Label added using coin address') + mmid = self._user_sid('alice') + (':S:1',':L:1')[self.proto.coin=='BCH'] + return self._user_chk_label('alice',mmid,'Label added using coin address of MMGen address') def alice_add_label_badaddr(self,addr,reply): t = self.spawn('mmgen-tool',['--alice','add_label',addr,'(none)']) @@ -820,21 +812,19 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): return t def alice_add_label_badaddr1(self): - return self.alice_add_label_badaddr(rt_pw,'Invalid coin address for this chain: ') + return self.alice_add_label_badaddr( rt_pw,'Invalid coin address for this chain: ') def alice_add_label_badaddr2(self): - addr = g.proto.pubhash2addr('00'*20,False) # mainnet zero address - return self.alice_add_label_badaddr(addr,'Invalid coin address for this chain: '+addr) + addr = init_proto(self.proto.coin,network='mainnet').pubhash2addr('00'*20,False) # mainnet zero address + return self.alice_add_label_badaddr( addr, f'Invalid coin address for this chain: {addr}' ) def alice_add_label_badaddr3(self): addr = self._user_sid('alice') + ':C:123' - return self.alice_add_label_badaddr(addr, - "MMGen address '{}' not found in tracking wallet".format(addr)) + return self.alice_add_label_badaddr( addr, f'MMGen address {addr!r} not found in tracking wallet' ) def alice_add_label_badaddr4(self): - addr = init_proto(g.coin,regtest=True).pubhash2addr('00'*20,False) # testnet zero address - return self.alice_add_label_badaddr(addr, - "Address '{}' not found in tracking wallet".format(addr)) + addr = self.proto.pubhash2addr('00'*20,False) # regtest (testnet) zero address + return self.alice_add_label_badaddr( addr, f'Address {addr!r} not found in tracking wallet' ) def alice_bal_rpcfail(self): addr = self._user_sid('alice') + ':C:2' @@ -848,29 +838,29 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): def alice_remove_label1(self): sid = self._user_sid('alice') - mmid = sid + (':S:3',':L:3')[g.coin=='BCH'] + mmid = sid + (':S:3',':L:3')[self.proto.coin=='BCH'] return self.user_remove_label('alice',mmid) def alice_chk_label1(self): sid = self._user_sid('alice') - return self.user_chk_label('alice',sid+':C:1','Original Label - 月へ') + return self._user_chk_label('alice',sid+':C:1','Original Label - 月へ') def alice_chk_label2(self): sid = self._user_sid('alice') - return self.user_chk_label('alice',sid+':C:1','Replacement Label') + return self._user_chk_label('alice',sid+':C:1','Replacement Label') def alice_edit_label1(self): return self.user_edit_label('alice','4',tw_label_lat_cyr_gr) def alice_edit_label2(self): return self.user_edit_label('alice','3',tw_label_zh) def alice_chk_label3(self): sid = self._user_sid('alice') - mmid = sid + (':S:3',':L:3')[g.coin=='BCH'] - return self.user_chk_label('alice',mmid,tw_label_lat_cyr_gr) + mmid = sid + (':S:3',':L:3')[self.proto.coin=='BCH'] + return self._user_chk_label('alice',mmid,tw_label_lat_cyr_gr) def alice_chk_label4(self): sid = self._user_sid('alice') - mmid = sid + (':S:3',':L:3')[g.coin=='BCH'] - return self.user_chk_label('alice',mmid,'-') + mmid = sid + (':S:3',':L:3')[self.proto.coin=='BCH'] + return self._user_chk_label('alice',mmid,'-') def user_edit_label(self,user,output,label): t = self.spawn('mmgen-txcreate',['-B','--'+user,'-i']) diff --git a/test/test_py_d/ts_shared.py b/test/test_py_d/ts_shared.py index da802955..622bceb2 100755 --- a/test/test_py_d/ts_shared.py +++ b/test/test_py_d/ts_shared.py @@ -35,7 +35,7 @@ class TestSuiteShared(object): caller = None, menu = [], inputs = '1', - file_desc = 'Transaction', + file_desc = 'Unsigned transaction', input_sels_prompt = 'to spend', bad_input_sels = False, non_mmgen_inputs = 0, @@ -45,7 +45,8 @@ class TestSuiteShared(object): eth_fee_res = None, add_comment = '', view = 't', - save = True ): + save = True, + tweaks = [] ): txdo = (caller or self.test_name)[:4] == 'txdo' @@ -54,11 +55,8 @@ class TestSuiteShared(object): if bad_input_sels: for r in ('x','3-1','9999'): t.expect(input_sels_prompt+': ',r+'\n') - t.expect(input_sels_prompt+': ',inputs+'\n') - if not txdo: - for i in range(non_mmgen_inputs): - t.expect('Accept? (y/N): ','y') + t.expect(input_sels_prompt+': ',inputs+'\n') have_est_fee = t.expect([fee_desc+': ','OK? (Y/n): ']) == 1 if have_est_fee and not interactive_fee: @@ -66,7 +64,7 @@ class TestSuiteShared(object): else: if have_est_fee: t.send('n') - if g.coin == 'BCH' or g.proto.base_coin == 'ETH': # TODO: pexpect race condition? + if self.proto.coin == 'BCH' or self.proto.base_coin == 'ETH': # TODO: pexpect race condition? time.sleep(0.1) if eth_fee_res: t.expect('or gas price: ',interactive_fee+'\n') @@ -76,6 +74,10 @@ class TestSuiteShared(object): t.expect('OK? (Y/n): ','y') t.expect('(Y/n): ','\n') # chg amt OK? + + if 'confirm_non_mmgen' in tweaks: + t.expect('Continue? (Y/n)','\n') + t.do_comment(add_comment) t.view_tx(view) if not txdo: @@ -225,7 +227,7 @@ class TestSuiteShared(object): t.read() if stdout else t.written_to_file(('Addresses','Password list')[passgen]) if check_ref: chk_ref = (self.chk_data[self.test_name] if passgen else - self.chk_data[self.test_name][self.fork][g.proto.testnet]) + self.chk_data[self.test_name][self.fork][self.proto.testnet]) cmp_or_die(chk,chk_ref,desc='{}list data checksum'.format(ftype)) return t @@ -241,7 +243,7 @@ class TestSuiteShared(object): t.passphrase(wcls.desc,self.wpasswd) chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True) if check_ref: - chk_ref = self.chk_data[self.test_name][self.fork][g.proto.testnet] + chk_ref = self.chk_data[self.test_name][self.fork][self.proto.testnet] cmp_or_die(chk,chk_ref,desc='key-address list data checksum') t.expect('Encrypt key list? (y/N): ','y') t.usr_rand(self.usr_rand_chars) diff --git a/test/test_py_d/ts_wallet.py b/test/test_py_d/ts_wallet.py index 265b90dc..6efbf55b 100755 --- a/test/test_py_d/ts_wallet.py +++ b/test/test_py_d/ts_wallet.py @@ -48,7 +48,7 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared): 'hic_wallet_old': '1378FC64-B55E9958-D85FF20C[192,1].incog-old.offset123', }, '256': { - 'ref_wallet': '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.proto.testnet]), + 'ref_wallet': '98831F3A-27F2BF93[256,1].mmdat', 'ic_wallet': '98831F3A-5482381C-18460FB1[256,1].mmincog', 'ic_wallet_hex': '98831F3A-1630A9F2-870376A9[256,1].mmincox', diff --git a/test/tooltest.py b/test/tooltest.py index 8e4fb205..abeac04b 100755 --- a/test/tooltest.py +++ b/test/tooltest.py @@ -54,6 +54,9 @@ sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:] cmd_args = opts.init(opts_data,add_opts=['exact_output','profile']) +from mmgen.protocol import init_proto_from_opts +proto = init_proto_from_opts() + cmd_data = { 'cryptocoin': { 'desc': 'Cryptocoin address/key commands', @@ -83,7 +86,7 @@ cmd_data = { }, } -if g.coin in ('BTC','LTC'): +if proto.coin in ('BTC','LTC'): cmd_data['cryptocoin']['cmd_data'].update({ 'pubhex2redeem_script': ('privhex2pubhex','o3'), 'wif2redeem_script': ('randpair','o3'), @@ -117,9 +120,9 @@ cfg = { } } -ref_subdir = '' if g.proto.base_coin == 'BTC' else g.proto.name.lower() -altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin -tn_ext = ('','.testnet')[g.proto.testnet] +ref_subdir = '' if proto.base_coin == 'BTC' else proto.name.lower() +altcoin_pfx = '' if proto.base_coin == 'BTC' else '-'+proto.base_coin +tn_ext = ('','.testnet')[proto.testnet] mmgen_cmd = 'mmgen-tool' @@ -180,18 +183,22 @@ if opt.list_names: die(0,'\n{}\n {}'.format(yellow('Untested commands:'),'\n '.join(uc))) from mmgen.tx import is_wif,is_coin_addr +def is_wif_loc(s): + return is_wif(proto,s) +def is_coin_addr_loc(s): + return is_coin_addr(proto,s) msg_w = 35 def test_msg(m): m2 = 'Testing {}'.format(m) msg_r(green(m2+'\n') if opt.verbose else '{:{w}}'.format(m2,w=msg_w+8)) -compressed = ('','compressed')['C' in g.proto.mmtypes] -segwit = ('','segwit')['S' in g.proto.mmtypes] -bech32 = ('','bech32')['B' in g.proto.mmtypes] -type_compressed_arg = ([],['--type=compressed'])['C' in g.proto.mmtypes] -type_segwit_arg = ([],['--type=segwit'])['S' in g.proto.mmtypes] -type_bech32_arg = ([],['--type=bech32'])['B' in g.proto.mmtypes] +compressed = ('','compressed')['C' in proto.mmtypes] +segwit = ('','segwit')['S' in proto.mmtypes] +bech32 = ('','bech32')['B' in proto.mmtypes] +type_compressed_arg = ([],['--type=compressed'])['C' in proto.mmtypes] +type_segwit_arg = ([],['--type=segwit'])['S' in proto.mmtypes] +type_bech32_arg = ([],['--type=bech32'])['B' in proto.mmtypes] class MMGenToolTestUtils(object): @@ -293,13 +300,13 @@ class MMGenToolTestCmds(object): for n,k in enumerate(['',compressed]): ao = ['--type='+k] if k else [] ret = tu.run_cmd_out(name,add_opts=ao,Return=True,fn_idx=n+1) - ok_or_die(ret,is_wif,'WIF key') + ok_or_die(ret,is_wif_loc,'WIF key') def randpair(self,name): for n,k in enumerate(['',compressed,segwit,bech32]): ao = ['--type='+k] if k else [] wif,addr = tu.run_cmd_out(name,add_opts=ao,Return=True,fn_idx=n+1,literal=True).split() - ok_or_die(wif,is_wif,'WIF key',skip_ok=True) - ok_or_die(addr,is_coin_addr,'Coin address') + ok_or_die(wif,is_wif_loc,'WIF key',skip_ok=True) + ok_or_die(addr,is_coin_addr_loc,'Coin address') def wif2addr(self,name,f1,f2,f3,f4): for n,f,k in ( (1,f1,''), diff --git a/test/tooltest2.py b/test/tooltest2.py index c6d7b8c3..16a3b5bf 100755 --- a/test/tooltest2.py +++ b/test/tooltest2.py @@ -30,16 +30,23 @@ from decimal import Decimal from include.tests_header import repo_root from mmgen.common import * from test.include.common import * -from mmgen.obj import is_wif,is_coin_addr from mmgen.wallet import is_bip39_mnemonic,is_mmgen_mnemonic from mmgen.addr import is_xmrseed from mmgen.baseconv import * +skipped_tests = ['mn2hex_interactive'] + NL = ('\n','\r\n')[g.platform=='win'] def is_str(s): return type(s) == str +from mmgen.obj import is_wif,is_coin_addr +def is_wif_loc(s): + return is_wif(proto,s) +def is_coin_addr_loc(s): + return is_coin_addr(proto,s) + def md5_hash(s): from hashlib import md5 return md5(s.encode()).hexdigest() @@ -559,12 +566,12 @@ tests = { ], }, 'randpair': { - 'btc_mainnet': [ ( [], [is_wif,is_coin_addr], ['-r0'] ) ], - 'btc_testnet': [ ( [], [is_wif,is_coin_addr], ['-r0'] ) ], + 'btc_mainnet': [ ( [], [is_wif_loc,is_coin_addr_loc], ['-r0'] ) ], + 'btc_testnet': [ ( [], [is_wif_loc,is_coin_addr_loc], ['-r0'] ) ], }, 'randwif': { - 'btc_mainnet': [ ( [], is_wif, ['-r0'] ) ], - 'btc_testnet': [ ( [], is_wif, ['-r0'] ) ], + 'btc_mainnet': [ ( [], is_wif_loc, ['-r0'] ) ], + 'btc_testnet': [ ( [], is_wif_loc, ['-r0'] ) ], }, 'wif2addr': { 'btc_mainnet': [ @@ -773,11 +780,13 @@ tests = { coin_dependent_groups = ('Coin','File') # TODO: do this as attr of each group in tool.py -def run_test(gid,cmd_name): +async def run_test(gid,cmd_name): data = tests[gid][cmd_name] - # behavior is like test.py: run coin-dependent tests only if g.proto.testnet or g.coin != BTC + # behavior is like test.py: run coin-dependent tests only if proto.testnet or proto.coin != BTC if gid in coin_dependent_groups: - k = '{}_{}net'.format((g.token.lower() if g.token else g.coin.lower()),('main','test')[g.proto.testnet]) + k = '{}_{}'.format( + ( g.token.lower() if proto.tokensym else proto.coin.lower() ), + ('mainnet','testnet')[proto.testnet] ) if k in data: data = data[k] m2 = ' ({})'.format(k) @@ -785,7 +794,7 @@ def run_test(gid,cmd_name): qmsg(f'-- no data for {cmd_name} ({k}) - skipping') return else: - if g.coin != 'BTC' or g.proto.testnet: + if proto.coin != 'BTC' or proto.testnet: return m2 = '' m = '{} {}{}'.format(purple('Testing'), cmd_name if opt.names else docstring_head(tc[cmd_name]),m2) @@ -810,7 +819,7 @@ def run_test(gid,cmd_name): return cmd_out.strip() - def run_func(cmd_name,args,out,opts,exec_code): + async def run_func(cmd_name,args,out,opts,exec_code): vmsg('{}: {}{}'.format(purple('Running'), ' '.join([cmd_name]+[repr(e) for e in args]), ' '+exec_code if exec_code else '' )) @@ -837,6 +846,8 @@ def run_test(gid,cmd_name): sys.exit(0) else: ret = tc.call(cmd_name,*aargs,**kwargs) + if type(ret).__name__ == 'coroutine': + ret = await ret opt.quiet = oq_save return ret @@ -873,7 +884,7 @@ def run_test(gid,cmd_name): if stdin_input and g.platform == 'win': msg('Skipping for MSWin - no os.fork()') continue - cmd_out = run_func(cmd_name,args,out,opts,exec_code) + cmd_out = await run_func(cmd_name,args,out,opts,exec_code) try: vmsg('Output:\n{}\n'.format(cmd_out)) except: vmsg('Output:\n{}\n'.format(repr(cmd_out))) @@ -925,12 +936,14 @@ def run_test(gid,cmd_name): def docstring_head(obj): return obj.__doc__.strip().split('\n')[0] -def do_group(gid): +async def do_group(gid): qmsg(blue('Testing ' + f'command group {gid!r}' if opt.names else docstring_head(tc.classes['MMGenToolCmd'+gid]) )) for cname in tc.classes['MMGenToolCmd'+gid].user_commands: + if cname in skipped_tests: + continue if cname not in tests[gid]: m = f'No test for command {cname!r} in group {gid!r}!' if opt.die_on_missing: @@ -938,7 +951,7 @@ def do_group(gid): else: msg(m) continue - run_test(gid,cname) + await run_test(gid,cname) def do_cmd_in_group(cmd): for gid in tests: @@ -956,6 +969,9 @@ sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:] cmd_args = opts.init(opts_data,add_opts=['use_old_ed25519']) +from mmgen.protocol import init_proto_from_opts +proto = init_proto_from_opts() + if opt.tool_api: del tests['Wallet'] del tests['File'] @@ -1003,7 +1019,7 @@ else: start_time = int(time.time()) -def main(): +async def main(): try: if cmd_args: for cmd in cmd_args: @@ -1018,7 +1034,7 @@ def main(): except KeyboardInterrupt: die(1,green('\nExiting at user request')) -main() +run_session(main()) t = int(time.time()) - start_time gmsg('All requested tests finished OK, elapsed time: {:02}:{:02}'.format(t//60,t%60)) diff --git a/test/unit_tests_d/ut_rpc.py b/test/unit_tests_d/ut_rpc.py index 93289124..04285a87 100755 --- a/test/unit_tests_d/ut_rpc.py +++ b/test/unit_tests_d/ut_rpc.py @@ -7,10 +7,10 @@ from mmgen.common import * from mmgen.exception import * from mmgen.protocol import init_proto -from mmgen.rpc import MoneroWalletRPCClient +from mmgen.rpc import rpc_init,MoneroWalletRPCClient from mmgen.daemon import CoinDaemon,MoneroWalletDaemon -def auth_test(d): +def auth_test(proto,d): d.stop() if g.platform != 'win': qmsg(f'\n Testing authentication with credentials from bitcoin.conf:') @@ -24,77 +24,66 @@ def auth_test(d): d.start() async def do(): - assert g.rpc.auth.user == 'ut_rpc', 'user is not ut_rpc!' + rpc = await rpc_init(proto) + assert rpc.auth.user == 'ut_rpc', 'user is not ut_rpc!' run_session(do()) d.stop() +def do_msg(rpc): + qmsg(' Testing backend {!r}'.format(type(rpc.backend).__name__)) + +class init_test: + + async def btc(proto,backend): + rpc = await rpc_init(proto,backend) + do_msg(rpc) + addrs = ( + ('bc1qvmqas4maw7lg9clqu6kqu9zq9cluvlln5hw97q','test address #1'), # deadbeef * 8 + ('bc1qe50rj25cldtskw5huxam335kyshtqtlrf4pt9x','test address #2'), # deadbeef * 7 + deadbeee + ) + await rpc.batch_call('importaddress',addrs,timeout=120) + ret = await rpc.batch_call('getaddressesbylabel',[(l,) for a,l in addrs]) + assert list(ret[0].keys())[0] == addrs[0][0] + + bh = (await rpc.call('getblockchaininfo',timeout=300))['bestblockhash'] + await rpc.gathered_call('getblock',((bh,),(bh,1)),timeout=300) + await rpc.gathered_call(None,(('getblock',(bh,)),('getblock',(bh,1))),timeout=300) + + async def bch(proto,backend): + rpc = await rpc_init(proto,backend) + do_msg(rpc) + + async def eth(proto,backend): + rpc = await rpc_init(proto,backend) + do_msg(rpc) + await rpc.call('parity_versionInfo',timeout=300) + +def run_test(coin,auth): + proto = init_proto(coin,network=('mainnet','regtest')[coin=='eth']) # FIXME CoinDaemon's network handling broken + d = CoinDaemon(network_id=coin,test_suite=True) + if auth: + d.remove_datadir() + d.start() + + for backend in g.autoset_opts['rpc_backend'].choices: + run_session(getattr(init_test,coin)(proto,backend),backend=backend) + + if auth: + auth_test(proto,d) + qmsg(' OK') + return True + class unit_tests: def bch(self,name,ut): - - async def run_test(): - qmsg(' Testing backend {!r}'.format(type(g.rpc.backend).__name__)) - - d = CoinDaemon('bch',test_suite=True) - d.remove_datadir() - d.start() - g.proto.daemon_data_dir = d.datadir # location of cookie file - g.rpc_port = d.rpc_port - - for backend in g.autoset_opts['rpc_backend'].choices: - run_session(run_test(),backend=backend) - - auth_test(d) - qmsg(' OK') - return True + return run_test('bch',auth=True) def btc(self,name,ut): - - async def run_test(): - c = g.rpc - qmsg(' Testing backend {!r}'.format(type(c.backend).__name__)) - addrs = ( - ('bc1qvmqas4maw7lg9clqu6kqu9zq9cluvlln5hw97q','test address #1'), # deadbeef * 8 - ('bc1qe50rj25cldtskw5huxam335kyshtqtlrf4pt9x','test address #2'), # deadbeef * 7 + deadbeee - ) - - await c.batch_call('importaddress',addrs,timeout=120) - ret = await c.batch_call('getaddressesbylabel',[(l,) for a,l in addrs]) - assert list(ret[0].keys())[0] == addrs[0][0] - - bh = (await c.call('getblockchaininfo',timeout=300))['bestblockhash'] - await c.gathered_call('getblock',((bh,),(bh,1)),timeout=300) - await c.gathered_call(None,(('getblock',(bh,)),('getblock',(bh,1))),timeout=300) - - - d = CoinDaemon('btc',test_suite=True) - d.remove_datadir() - d.start() - g.proto.daemon_data_dir = d.datadir # used by BitcoinRPCClient.set_auth() to find the cookie - g.rpc_port = d.rpc_port - - for backend in g.autoset_opts['rpc_backend'].choices: - run_session(run_test(),backend=backend) - - auth_test(d) - qmsg(' OK') - return True + return run_test('btc',auth=True) def eth(self,name,ut): - ed = CoinDaemon('eth',test_suite=True) - ed.start() - g.rpc_port = CoinDaemon('eth',test_suite=True).rpc_port - - async def run_test(): - qmsg(' Testing backend {!r}'.format(type(g.rpc.backend).__name__)) - ret = await g.rpc.call('parity_versionInfo',timeout=300) - - for backend in g.autoset_opts['rpc_backend'].choices: - run_session(run_test(),proto=init_proto('eth'),backend=backend) - - ed.stop() - return True + return run_test('eth',auth=False) def xmr_wallet(self,name,ut): @@ -124,5 +113,5 @@ class unit_tests: md.wait = False md.stop() - run_session(run(),do_rpc_init=False) + run_session(run()) return True diff --git a/test/unit_tests_d/ut_tx.py b/test/unit_tests_d/ut_tx.py index 88c4a791..5148e484 100755 --- a/test/unit_tests_d/ut_tx.py +++ b/test/unit_tests_d/ut_tx.py @@ -7,11 +7,31 @@ import re from mmgen.common import * from mmgen.tx import MMGenTX from mmgen.txfile import MMGenTxFile +from mmgen.rpc import rpc_init +from mmgen.daemon import CoinDaemon +from mmgen.protocol import init_proto class unit_tests: - def txfile(self,name,ut): + def tx(self,name,ut): + qmsg(' Testing transaction objects') + proto = init_proto('btc') + d = CoinDaemon('btc',test_suite=True) + d.start() + proto.daemon_data_dir = d.datadir # location of cookie file + proto.rpc_port = d.rpc_port + async def do(): + tx = MMGenTX.New(proto=proto) + tx.rpc = await rpc_init(proto=proto) + + run_session(do()) + + d.stop() + qmsg(' OK') + return True + + def txfile(self,name,ut): qmsg(' Testing TX file operations') fns = ( # TODO: add altcoin TX files @@ -21,22 +41,24 @@ class unit_tests: '25EFA3[2.34].testnet.rawtx', ) for fn in fns: + vmsg(f' parsing: {fn}') fpath = os.path.join('test','ref',fn) - tx = MMGenTX(filename=fpath,quiet_open=True) + tx = MMGenTX.Unsigned(filename=fpath,quiet_open=True) f = MMGenTxFile(tx) fn_gen = f.make_filename() - vmsg(f' parsed: {fn_gen}') assert fn_gen == fn, f'{fn_gen} != {fn}' text = f.format() # New in version 3.3: Support for the unicode legacy literal (u'value') was # reintroduced to simplify the maintenance of dual Python 2.x and 3.x codebases. # See PEP 414 for more information. - chk = re.subn(r"\bu'",r"'",open(fpath).read())[0] # remove Python2 'u' string prefixes from ref files - nLines = len([i for i in get_ndiff(chk,text) if i.startswith('-')]) - assert nLines == 1, f'{nLines} lines differ: only checksum line should differ' - break # FIXME - test BCH, testnet + chk = re.subn(r"\bu(['\"])",r"\1",open(fpath).read())[0] # remove Python2 'u' string prefixes from ref files + diff = get_ndiff(chk,text) + #print('\n'.join(diff)) + nLines = len([i for i in diff if i.startswith('-')]) + assert nLines in (0,1), f'{nLines} lines differ: only checksum line may differ' + #break # FIXME - test BCH, testnet qmsg(' OK') return True diff --git a/test/unit_tests_d/ut_tx_deserialize.py b/test/unit_tests_d/ut_tx_deserialize.py index 597dcbb7..941b7dc5 100755 --- a/test/unit_tests_d/ut_tx_deserialize.py +++ b/test/unit_tests_d/ut_tx_deserialize.py @@ -21,7 +21,7 @@ class unit_test(object): def run_test(self,name,ut): - async def test_tx(txhex,desc,n): + async def test_tx(tx_proto,tx_hex,desc,n): def has_nonstandard_outputs(outputs): for o in outputs: @@ -30,11 +30,12 @@ class unit_test(object): return True return False - d = await g.rpc.call('decoderawtransaction',txhex) + rpc = await rpc_init(proto=tx_proto) + d = await rpc.call('decoderawtransaction',tx_hex) if has_nonstandard_outputs(d['vout']): return False - dt = DeserializedTX(txhex) + dt = DeserializedTX(tx_proto,tx_hex) if opt.verbose: Msg('\n====================================================') @@ -101,8 +102,11 @@ class unit_test(object): n = 1 for e in data: if type(e[0]) == list: - await rpc_init() - await test_tx(e[1],desc,n) + await test_tx( + tx_proto = init_proto('btc'), + tx_hex = e[1], + desc = desc, + n = n ) n += 1 else: desc = e[0] @@ -114,18 +118,18 @@ class unit_test(object): # ('bch',False,'test/ref/460D4D-BCH[10.19764,tl=1320969600].rawtx') ) print_info('test/ref/*rawtx','MMGen reference') - g.rpc_port = None for n,(coin,testnet,fn) in enumerate(fns): - g.proto = init_proto(coin,testnet=testnet) - g.proto.daemon_data_dir = 'test/daemons/' + coin - g.proto.rpc_port = CoinDaemon(coin + ('','_tn')[testnet],test_suite=True).rpc_port - await rpc_init() - await test_tx(MMGenTX(fn).hex,fn,n+1) + tx = MMGenTX.Unsigned(filename=fn) + await test_tx( + tx_proto = tx.proto, + tx_hex = tx.hex, + desc = fn, + n = n+1 ) Msg('OK') start_test_daemons('btc','btc_tn') # ,'bch') - run_session(test_mmgen_txs(),do_rpc_init=False) - run_session(test_core_vectors(),do_rpc_init=False) + run_session(test_core_vectors()) + run_session(test_mmgen_txs()) stop_test_daemons('btc','btc_tn') # ,'bch') return True