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.
This commit is contained in:
The MMGen Project 2020-05-28 09:53:34 +00:00
commit c3f185e8b0
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
63 changed files with 4295 additions and 3616 deletions

View file

@ -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))))

View file

@ -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)

View file

@ -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())]

View file

@ -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

View file

@ -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 <output_of_step_A> <output_of_step_B> <output_of_step_C>
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 <output_of_step_D> <output_of_step_E>
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 <output_of_step_X> <output_of_step_Y>
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 <output_of_step_X> <output_of_step_Z>
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 <output_of_step_N>
""".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 <seed ID>:<index>.
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)

View file

@ -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'

View file

@ -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

View file

@ -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()

256
mmgen/help.py Executable file
View file

@ -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 <mmgen@tuta.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
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 <seed ID>:<index>.
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 <output_of_step_A> <output_of_step_B> <output_of_step_C>
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 <output_of_step_D> <output_of_step_E>
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 <output_of_step_X> <output_of_step_Y>
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 <output_of_step_X> <output_of_step_Z>
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 <output_of_step_N>
""".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 '')

View file

@ -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:

View file

@ -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())

View file

@ -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())

View file

@ -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,

View file

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

View file

@ -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

View file

@ -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:

View file

@ -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())

View file

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

View file

@ -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)

View file

@ -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())

View file

@ -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],

View file

@ -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:

View file

@ -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_') }

View file

@ -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):
"""

View file

@ -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))

View file

@ -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

View file

@ -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):

View file

@ -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 <seed ID>:[<type>:]<idx list>)"
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

View file

@ -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()

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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

View file

@ -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+

View file

@ -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]

View file

@ -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 <mmgen@tuta.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
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)

View file

@ -113,6 +113,7 @@ setup(
'mmgen.exception',
'mmgen.filename',
'mmgen.globalvars',
'mmgen.help',
'mmgen.keccak',
'mmgen.led',
'mmgen.license',

View file

@ -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:

View file

@ -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()

View file

@ -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()

View file

@ -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'], {})

View file

@ -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()

View file

@ -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': {

View file

@ -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()}
),
},
}

View file

@ -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()+':'

View file

@ -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()}
)
},
}

View file

@ -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()}
)
},
}

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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)],

View file

@ -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'

View file

@ -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'])

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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'])

View file

@ -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)

View file

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

View file

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

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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