TrackingWallet: balance caching, Parity light client optimizations
- activated for Ethereum only, but framework exists for all coins - both session caching and persistent caching in the wallet are supported - network-destined RPC calls are never repeated in a given invocation - RPC balance lookups can be suppressed entirely with --cached-balances
This commit is contained in:
parent
69e09d3b54
commit
d0f8c44b20
25 changed files with 831 additions and 351 deletions
|
|
@ -898,10 +898,10 @@ re-import your addresses.
|
|||
def __new__(cls,*args,**kwargs):
|
||||
return MMGenObject.__new__(altcoin_subclass(cls,'tw','AddrData'))
|
||||
|
||||
def __init__(self,source=None):
|
||||
def __init__(self,source=None,wallet=None):
|
||||
self.al_ids = {}
|
||||
if source == 'tw':
|
||||
self.add_tw_data()
|
||||
self.add_tw_data(wallet)
|
||||
|
||||
def seed_ids(self):
|
||||
return list(self.al_ids.keys())
|
||||
|
|
@ -923,7 +923,7 @@ re-import your addresses.
|
|||
return (list(d.values())[0][0]) if d else None
|
||||
|
||||
@classmethod
|
||||
def get_tw_data(cls):
|
||||
def get_tw_data(cls,wallet=None):
|
||||
vmsg('Getting address data from tracking wallet')
|
||||
if 'label_api' in g.rpch.caps:
|
||||
accts = g.rpch.listlabels()
|
||||
|
|
@ -933,8 +933,8 @@ re-import your addresses.
|
|||
alists = g.rpch.getaddressesbyaccount([[k] for k in accts],batch=True)
|
||||
return list(zip(accts,alists))
|
||||
|
||||
def add_tw_data(self):
|
||||
d,out,i = self.get_tw_data(),{},0
|
||||
def add_tw_data(self,wallet):
|
||||
d,out,i = self.get_tw_data(wallet),{},0
|
||||
for acct,addr_array in d:
|
||||
l = TwLabel(acct,on_fail='silent')
|
||||
if l and l.mmid.type == 'mmgen':
|
||||
|
|
|
|||
|
|
@ -41,14 +41,19 @@ def create_method_id(sig): return keccak_256(sig.encode()).hexdigest()[:8]
|
|||
|
||||
class Token(MMGenObject): # ERC20
|
||||
|
||||
_decimals = None
|
||||
|
||||
# Test that token is in the blockchain by calling constructor w/o decimals arg
|
||||
def __init__(self,addr,decimals=None):
|
||||
self.addr = TokenAddr(addr)
|
||||
if decimals is None:
|
||||
decimals = self.decimals()
|
||||
if not decimals:
|
||||
raise TokenNotInBlockchain("Token '{}' not in blockchain".format(addr))
|
||||
self.base_unit = Decimal('10') ** -decimals
|
||||
if decimals:
|
||||
self._decimals = decimals
|
||||
else:
|
||||
rpc_init()
|
||||
self.decimals()
|
||||
if not self._decimals:
|
||||
raise TokenNotInBlockchain("Token '{}' not in blockchain".format(addr))
|
||||
self.base_unit = Decimal('10') ** -self._decimals
|
||||
|
||||
@staticmethod
|
||||
def transferdata2sendaddr(data): # online
|
||||
|
|
@ -70,14 +75,17 @@ class Token(MMGenObject): # ERC20
|
|||
def strip(self,s):
|
||||
return ''.join([chr(b) for b in s if 32 <= b <= 127]).strip()
|
||||
|
||||
# TODO: make these properties
|
||||
def decimals(self):
|
||||
ret = self.do_call('decimals()')
|
||||
if self._decimals == None:
|
||||
res = self.do_call('decimals()')
|
||||
try:
|
||||
a,b = ret[:2],ret[2:]
|
||||
assert a == '0x' and is_hex_str_lc(b)
|
||||
assert res[:2] == '0x'
|
||||
self._decimals = int(res[2:],16)
|
||||
except:
|
||||
"RPC call to decimals() failed (returned '{}')".format(ret)
|
||||
return int(b,16) if b else None
|
||||
msg("RPC call to decimals() failed (returned '{}')".format(res))
|
||||
return None
|
||||
return self._decimals
|
||||
|
||||
def name(self):
|
||||
return self.strip(bytes.fromhex(self.do_call('name()')[2:]))
|
||||
|
|
@ -105,10 +113,8 @@ class Token(MMGenObject): # ERC20
|
|||
amt_arg = '{:064x}'.format(int(amt//self.base_unit))
|
||||
return create_method_id(method_sig) + from_arg + to_arg + amt_arg
|
||||
|
||||
def txcreate( self,from_addr,to_addr,amt,start_gas,gasPrice,nonce=None,
|
||||
def make_tx_in( self,from_addr,to_addr,amt,start_gas,gasPrice,nonce,
|
||||
method_sig='transfer(address,uint256)',from_addr2=None):
|
||||
if nonce is None:
|
||||
nonce = int(g.rpch.parity_nextNonce('0x'+from_addr),16)
|
||||
data = self.create_data(to_addr,amt,method_sig=method_sig,from_addr=from_addr2)
|
||||
return {'to': bytes.fromhex(self.addr),
|
||||
'startgas': start_gas.toWei(),
|
||||
|
|
@ -145,10 +151,11 @@ class Token(MMGenObject): # ERC20
|
|||
method_sig='transfer(address,uint256)',
|
||||
from_addr2=None,
|
||||
return_data=False):
|
||||
tx_in = self.txcreate( from_addr,to_addr,amt,
|
||||
start_gas,gasPrice,
|
||||
nonce=None,
|
||||
method_sig=method_sig,
|
||||
from_addr2=from_addr2)
|
||||
tx_in = self.make_tx_in(
|
||||
from_addr,to_addr,amt,
|
||||
start_gas,gasPrice,
|
||||
nonce = int(g.rpch.parity_nextNonce('0x'+from_addr),16),
|
||||
method_sig = method_sig,
|
||||
from_addr2 = from_addr2 )
|
||||
(hex_tx,coin_txid) = self.txsign(tx_in,key,from_addr)
|
||||
return self.txsend(hex_tx)
|
||||
|
|
|
|||
|
|
@ -20,157 +20,238 @@
|
|||
altcoins.eth.tw: Ethereum tracking wallet and related classes for the MMGen suite
|
||||
"""
|
||||
|
||||
import json
|
||||
from mmgen.common import *
|
||||
from mmgen.obj import ETHAmt,TwMMGenID,TwComment,TwLabel
|
||||
from mmgen.obj import ETHAmt,TwLabel,is_coin_addr,is_mmgen_id
|
||||
from mmgen.tw import TrackingWallet,TwAddrList,TwUnspentOutputs
|
||||
from mmgen.addr import AddrData
|
||||
from .contract import Token
|
||||
|
||||
class EthereumTrackingWallet(TrackingWallet):
|
||||
|
||||
desc = 'Ethereum tracking wallet'
|
||||
caps = ()
|
||||
data_key = 'accounts'
|
||||
use_tw_file = True
|
||||
|
||||
data_dir = os.path.join(g.altcoin_data_dir,g.coin.lower(),g.proto.data_subdir)
|
||||
tw_file = os.path.join(data_dir,'tracking-wallet.json')
|
||||
|
||||
def __init__(self,mode='r'):
|
||||
def __init__(self,mode='r',no_rpc=False):
|
||||
TrackingWallet.__init__(self,mode=mode)
|
||||
check_or_create_dir(self.data_dir)
|
||||
try:
|
||||
self.orig_data = get_data_from_file(self.tw_file,quiet=True)
|
||||
self.data = json.loads(self.orig_data)
|
||||
except:
|
||||
try: os.stat(self.tw_file)
|
||||
except:
|
||||
self.orig_data = ''
|
||||
self.data = {'coin':g.coin,'accounts':{},'tokens':{}}
|
||||
else: die(2,"File '{}' exists but does not contain valid json data".format(self.tw_file))
|
||||
else:
|
||||
self.upgrade_wallet_maybe()
|
||||
m = 'Tracking wallet coin ({}) does not match current coin ({})!'
|
||||
assert self.data['coin'] == g.coin,m.format(self.data['coin'],g.coin)
|
||||
if not 'tokens' in self.data:
|
||||
self.data['tokens'] = {}
|
||||
def conv_types(ad):
|
||||
for v in ad.values():
|
||||
v['mmid'] = TwMMGenID(v['mmid'],on_fail='raise')
|
||||
v['comment'] = TwComment(v['comment'],on_fail='raise')
|
||||
conv_types(self.data['accounts'])
|
||||
for v in self.data['tokens'].values():
|
||||
conv_types(v)
|
||||
|
||||
for v in self.data['tokens'].values():
|
||||
self.conv_types(v)
|
||||
|
||||
if g.token and not is_coin_addr(g.token):
|
||||
ret = self.sym2addr(g.token,no_rpc=no_rpc)
|
||||
if ret: g.token = ret
|
||||
|
||||
def is_in_wallet(self,addr):
|
||||
return addr in self.data_root
|
||||
|
||||
def init_empty(self):
|
||||
self.data = { 'coin': g.coin, 'accounts': {}, 'tokens': {} }
|
||||
|
||||
def upgrade_wallet_maybe(self):
|
||||
|
||||
upgraded = False
|
||||
|
||||
if not 'accounts' in self.data or not 'coin' in self.data:
|
||||
ymsg('Upgrading {}!'.format(self.desc))
|
||||
ymsg('Upgrading {} (v1->v2: accounts field added)'.format(self.desc))
|
||||
if not 'accounts' in self.data:
|
||||
self.data = {}
|
||||
import json
|
||||
self.data['accounts'] = json.loads(self.orig_data)
|
||||
if not 'coin' in self.data:
|
||||
self.data['coin'] = g.coin
|
||||
mode_save = self.mode
|
||||
self.mode = 'w'
|
||||
self.write()
|
||||
self.mode = mode_save
|
||||
self.orig_data = json.dumps(self.data)
|
||||
upgraded = True
|
||||
|
||||
def have_token_params_fields():
|
||||
for k in self.data['tokens']:
|
||||
if 'params' in self.data['tokens'][k]:
|
||||
return True
|
||||
|
||||
def add_token_params_fields():
|
||||
for k in self.data['tokens']:
|
||||
self.data['tokens'][k]['params'] = {}
|
||||
|
||||
if not 'tokens' in self.data:
|
||||
self.data['tokens'] = {}
|
||||
upgraded = True
|
||||
|
||||
if self.data['tokens'] and not have_token_params_fields():
|
||||
ymsg('Upgrading {} (v2->v3: token params fields added)'.format(self.desc))
|
||||
add_token_params_fields()
|
||||
upgraded = True
|
||||
|
||||
if upgraded:
|
||||
self.force_write()
|
||||
msg('{} upgraded successfully!'.format(self.desc))
|
||||
|
||||
def data_root(self): return self.data['accounts']
|
||||
def data_root_desc(self): return 'accounts'
|
||||
# Don't call rpc_init() for Ethereum, because it may create a wallet instance
|
||||
def rpc_init(self): pass
|
||||
|
||||
def rpc_get_balance(self,addr):
|
||||
return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
|
||||
|
||||
@write_mode
|
||||
def import_address(self,addr,label,foo):
|
||||
ad = self.data_root()
|
||||
if addr in ad:
|
||||
if not ad[addr]['mmid'] and label.mmid:
|
||||
r = self.data_root
|
||||
if addr in r:
|
||||
if not r[addr]['mmid'] and label.mmid:
|
||||
msg("Warning: MMGen ID '{}' was missing in tracking wallet!".format(label.mmid))
|
||||
elif ad[addr]['mmid'] != label.mmid:
|
||||
elif r[addr]['mmid'] != label.mmid:
|
||||
die(3,"MMGen ID '{}' does not match tracking wallet!".format(label.mmid))
|
||||
ad[addr] = { 'mmid': label.mmid, 'comment': label.comment }
|
||||
|
||||
@write_mode
|
||||
def write(self): # use 'check_data' to check wallet hasn't been altered by another program
|
||||
write_data_to_file( self.tw_file,
|
||||
json.dumps(self.data),'Ethereum tracking wallet data',
|
||||
ask_overwrite=False,ignore_opt_outdir=True,quiet=True,
|
||||
check_data=True,cmp_data=self.orig_data)
|
||||
|
||||
@write_mode
|
||||
def delete_all(self):
|
||||
self.data = {}
|
||||
self.write()
|
||||
r[addr] = { 'mmid': label.mmid, 'comment': label.comment }
|
||||
|
||||
@write_mode
|
||||
def remove_address(self,addr):
|
||||
root = self.data_root()
|
||||
r = self.data_root
|
||||
|
||||
from mmgen.obj import is_coin_addr,is_mmgen_id
|
||||
if is_coin_addr(addr):
|
||||
have_match = lambda k: k == addr
|
||||
elif is_mmgen_id(addr):
|
||||
have_match = lambda k: root[k]['mmid'] == addr
|
||||
have_match = lambda k: r[k]['mmid'] == addr
|
||||
else:
|
||||
die(1,"'{}' is not an Ethereum address or MMGen ID".format(addr))
|
||||
|
||||
for k in root:
|
||||
for k in r:
|
||||
if have_match(k):
|
||||
# return the addr resolved to mmid if possible
|
||||
ret = root[k]['mmid'] if is_mmgen_id(root[k]['mmid']) else addr
|
||||
del root[k]
|
||||
ret = r[k]['mmid'] if is_mmgen_id(r[k]['mmid']) else addr
|
||||
del r[k]
|
||||
self.write()
|
||||
return ret
|
||||
else:
|
||||
m = "Address '{}' not found in '{}' section of tracking wallet"
|
||||
msg(m.format(addr,self.data_root_desc()))
|
||||
msg(m.format(addr,self.data_root_desc))
|
||||
return None
|
||||
|
||||
def is_in_wallet(self,addr):
|
||||
return addr in self.data_root()
|
||||
|
||||
def sorted_list(self):
|
||||
return sorted(
|
||||
[{'addr':x[0],'mmid':x[1]['mmid'],'comment':x[1]['comment']} for x in list(self.data_root().items())],
|
||||
key=lambda x: x['mmid'].sort_key+x['addr'] )
|
||||
|
||||
def mmid_ordered_dict(self):
|
||||
from collections import OrderedDict
|
||||
return OrderedDict([(x['mmid'],{'addr':x['addr'],'comment':x['comment']}) for x in self.sorted_list()])
|
||||
|
||||
@write_mode
|
||||
def set_label(self,coinaddr,lbl):
|
||||
for addr,d in list(self.data_root().items()):
|
||||
for addr,d in list(self.data_root.items()):
|
||||
if addr == coinaddr:
|
||||
d['comment'] = lbl.comment
|
||||
self.write()
|
||||
return None
|
||||
else: # emulate on_fail='return' of RPC library
|
||||
m = "Address '{}' not found in '{}' section of tracking wallet"
|
||||
return ('rpcfail',(None,2,m.format(coinaddr,self.data_root_desc())))
|
||||
return ('rpcfail',(None,2,m.format(coinaddr,self.data_root_desc)))
|
||||
|
||||
def addr2sym(self,req_addr):
|
||||
|
||||
for addr in self.data['tokens']:
|
||||
if addr == req_addr:
|
||||
ret = self.data['tokens'][addr]['params'].get('symbol')
|
||||
if ret: return ret
|
||||
else: break
|
||||
|
||||
self.token_obj = Token(req_addr)
|
||||
ret = self.token_obj.symbol().upper()
|
||||
self.force_set_token_param(req_addr,'symbol',ret)
|
||||
return ret
|
||||
|
||||
def sym2addr(self,sym,no_rpc=False):
|
||||
|
||||
for addr in self.data['tokens']:
|
||||
if self.data['tokens'][addr]['params'].get('symbol') == sym.upper():
|
||||
return addr
|
||||
|
||||
if no_rpc: return None
|
||||
|
||||
for addr in self.data['tokens']:
|
||||
if Token(addr).symbol().upper() == sym.upper():
|
||||
self.force_set_token_param(addr,'symbol',sym.upper())
|
||||
return addr
|
||||
|
||||
return None
|
||||
|
||||
def get_token_param(self,token,param):
|
||||
if token in self.data['tokens']:
|
||||
return self.data['tokens'][token]['params'].get(param)
|
||||
return None
|
||||
|
||||
def force_set_token_param(self,*args,**kwargs):
|
||||
mode_save = self.mode
|
||||
self.mode = 'w'
|
||||
self.set_token_param(*args,**kwargs)
|
||||
self.mode = mode_save
|
||||
|
||||
@write_mode
|
||||
def set_token_param(self,token,param,val):
|
||||
if token in self.data['tokens']:
|
||||
self.data['tokens'][token]['params'][param] = val
|
||||
|
||||
class EthereumTokenTrackingWallet(EthereumTrackingWallet):
|
||||
|
||||
def token_is_in_wallet(self,addr):
|
||||
return addr in self.data['tokens']
|
||||
decimals = None
|
||||
symbol = None
|
||||
cur_eth_balances = {}
|
||||
|
||||
def __init__(self,mode='r',no_rpc=False):
|
||||
EthereumTrackingWallet.__init__(self,mode=mode,no_rpc=no_rpc)
|
||||
|
||||
self.desc = 'Ethereum token tracking wallet'
|
||||
|
||||
if not is_coin_addr(g.token):
|
||||
raise UnrecognizedTokenSymbol('Specified token {!r} could not be resolved!'.format(g.token))
|
||||
|
||||
if mode == 'r' and not g.token in self.data['tokens']:
|
||||
raise TokenNotInWallet('Specified token {!r} not in wallet!'.format(g.token))
|
||||
|
||||
self.token = g.token
|
||||
|
||||
if self.token in self.data['tokens']:
|
||||
for k in ('decimals','symbol'):
|
||||
setattr(self,k,self.get_param(k))
|
||||
if getattr(self,k) == None:
|
||||
setattr(self,k,getattr(Token(self.token,self.decimals),k)())
|
||||
if getattr(self,k) != None:
|
||||
self.set_param(k,getattr(self,k))
|
||||
self.write()
|
||||
|
||||
def is_in_wallet(self,addr):
|
||||
return addr in self.data['tokens'][self.token]
|
||||
|
||||
@property
|
||||
def data_root(self):
|
||||
return self.data['tokens'][self.token]
|
||||
|
||||
@property
|
||||
def data_root_desc(self):
|
||||
return 'token ' + Token(g.token).symbol()
|
||||
return 'token ' + Token(self.token,self.decimals).symbol()
|
||||
|
||||
@write_mode
|
||||
def add_token(self,token):
|
||||
msg("Adding token '{}' to tracking wallet.".format(token))
|
||||
self.data['tokens'][token] = {}
|
||||
self.data['tokens'][token] = { 'params': {} }
|
||||
|
||||
def data_root(self): # create the token data root if necessary
|
||||
if g.token not in self.data['tokens']:
|
||||
self.add_token(g.token)
|
||||
return self.data['tokens'][g.token]
|
||||
@write_mode
|
||||
def import_address(self,*args,**kwargs):
|
||||
if self.token not in self.data['tokens']:
|
||||
self.add_token(self.token)
|
||||
EthereumTrackingWallet.import_address(self,*args,**kwargs)
|
||||
|
||||
def sym2addr(self,sym): # online
|
||||
for addr in self.data['tokens']:
|
||||
if Token(addr).symbol().upper() == sym.upper():
|
||||
return addr
|
||||
return None
|
||||
def rpc_get_balance(self,addr):
|
||||
return Token(self.token,self.decimals).balance(addr)
|
||||
|
||||
def get_eth_balance(self,addr,force_rpc=False):
|
||||
cache = self.cur_eth_balances
|
||||
data_root = self.data['accounts']
|
||||
ret = None if force_rpc else self.get_cached_balance(addr,cache,data_root)
|
||||
if ret == None:
|
||||
ret = EthereumTrackingWallet.rpc_get_balance(self,addr)
|
||||
self.cache_balance(addr,ret,cache,data_root)
|
||||
return ret
|
||||
|
||||
def force_set_param(self,*args,**kwargs):
|
||||
mode_save = self.mode
|
||||
self.mode = 'w'
|
||||
self.set_param(*args,**kwargs)
|
||||
self.mode = mode_save
|
||||
|
||||
@write_mode
|
||||
def set_param(self,param,val):
|
||||
self.data['tokens'][self.token]['params'][param] = val
|
||||
|
||||
def get_param(self,param):
|
||||
return self.data['tokens'][self.token]['params'].get(param)
|
||||
|
||||
# No unspent outputs with Ethereum, but naming must be consistent
|
||||
class EthereumTwUnspentOutputs(TwUnspentOutputs):
|
||||
|
|
@ -186,29 +267,33 @@ class EthereumTwUnspentOutputs(TwUnspentOutputs):
|
|||
Sort options: [a]mount, a[d]dress, [r]everse, [M]mgen addr
|
||||
Display options: show [m]mgen addr, r[e]draw screen
|
||||
Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view,
|
||||
add [l]abel, [R]emove address:
|
||||
add [l]abel, [D]elete address, [R]efresh balance:
|
||||
"""
|
||||
key_mappings = {
|
||||
'a':'s_amt','d':'s_addr','r':'d_reverse','M':'s_twmmid',
|
||||
'm':'d_mmid','e':'d_redraw',
|
||||
'q':'a_quit','p':'a_print','v':'a_view','w':'a_view_wide',
|
||||
'l':'a_lbl_add','R':'a_addr_delete' }
|
||||
'l':'a_lbl_add','D':'a_addr_delete','R':'a_balance_refresh' }
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
if g.use_cached_balances:
|
||||
self.hdr_fmt += '\n' + yellow('WARNING: Using cached balances. These may be out of date!')
|
||||
TwUnspentOutputs.__init__(self,*args,**kwargs)
|
||||
|
||||
def do_sort(self,key=None,reverse=False):
|
||||
if key == 'txid': return
|
||||
super(EthereumTwUnspentOutputs,self).do_sort(key=key,reverse=reverse)
|
||||
|
||||
def get_addr_bal(self,addr):
|
||||
return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
|
||||
|
||||
def get_unspent_rpc(self):
|
||||
rpc_init()
|
||||
wl = self.wallet.sorted_list
|
||||
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'),
|
||||
'address': d['addr'],
|
||||
'amount': self.get_addr_bal(d['addr']),
|
||||
'amount': self.wallet.get_balance(d['addr']),
|
||||
'confirmations': 0, # TODO
|
||||
} for d in TrackingWallet().sorted_list()]
|
||||
} for d in wl]
|
||||
|
||||
class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs):
|
||||
|
||||
|
|
@ -218,22 +303,18 @@ class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs):
|
|||
|
||||
def get_display_precision(self): return 10 # truncate precision for narrow display
|
||||
|
||||
def get_addr_bal(self,addr):
|
||||
return Token(g.token).balance(addr)
|
||||
|
||||
# NB: two wallet instances open simultaneously on the same data:
|
||||
def get_unspent_data(self):
|
||||
super(type(self),self).get_unspent_data()
|
||||
for e in self.unspent:
|
||||
e.amt2 = ETHAmt(int(g.rpch.eth_getBalance('0x'+e.addr),16),'wei')
|
||||
e.amt2 = self.wallet.get_eth_balance(e.addr)
|
||||
|
||||
class EthereumTwAddrList(TwAddrList):
|
||||
|
||||
def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels):
|
||||
def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None):
|
||||
|
||||
rpc_init()
|
||||
if g.token: self.token = Token(g.token)
|
||||
|
||||
tw_dict = TrackingWallet().mmid_ordered_dict()
|
||||
self.wallet = wallet or TrackingWallet(mode='w')
|
||||
tw_dict = self.wallet.mmid_ordered_dict
|
||||
self.total = g.proto.coin_amt('0')
|
||||
|
||||
from mmgen.obj import CoinAddr
|
||||
|
|
@ -241,7 +322,7 @@ class EthereumTwAddrList(TwAddrList):
|
|||
# if d['confirmations'] < minconf: continue # cannot get confirmations for eth account
|
||||
label = TwLabel(mmid+' '+d['comment'],on_fail='raise')
|
||||
if usr_addr_list and (label.mmid not in usr_addr_list): continue
|
||||
bal = self.get_addr_balance(d['addr'])
|
||||
bal = self.wallet.get_balance(d['addr'])
|
||||
if bal == 0 and not showempty:
|
||||
if not label.comment: continue
|
||||
if not all_labels: continue
|
||||
|
|
@ -252,21 +333,19 @@ class EthereumTwAddrList(TwAddrList):
|
|||
self[label.mmid]['amt'] += bal
|
||||
self.total += bal
|
||||
|
||||
def get_addr_balance(self,addr):
|
||||
return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
|
||||
|
||||
class EthereumTokenTwAddrList(EthereumTwAddrList):
|
||||
|
||||
def get_addr_balance(self,addr):
|
||||
return self.token.balance(addr)
|
||||
class EthereumTokenTwAddrList(EthereumTwAddrList): pass
|
||||
|
||||
from mmgen.tw import TwGetBalance
|
||||
class EthereumTwGetBalance(TwGetBalance):
|
||||
|
||||
fs = '{w:13} {c}\n' # TODO - for now, just suppress display of meaningless data
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
self.wallet = TrackingWallet(mode='w')
|
||||
TwGetBalance.__init__(self,*args,**kwargs)
|
||||
|
||||
def create_data(self):
|
||||
data = TrackingWallet().mmid_ordered_dict()
|
||||
data = self.wallet.mmid_ordered_dict
|
||||
for d in data:
|
||||
if d.type == 'mmgen':
|
||||
key = d.obj.sid
|
||||
|
|
@ -276,25 +355,19 @@ class EthereumTwGetBalance(TwGetBalance):
|
|||
key = 'Non-MMGen'
|
||||
|
||||
conf_level = 2 # TODO
|
||||
amt = self.get_addr_balance(data[d]['addr'])
|
||||
amt = self.wallet.get_balance(data[d]['addr'])
|
||||
|
||||
self.data['TOTAL'][conf_level] += amt
|
||||
self.data[key][conf_level] += amt
|
||||
|
||||
def get_addr_balance(self,addr):
|
||||
return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
|
||||
|
||||
class EthereumTokenTwGetBalance(EthereumTwGetBalance):
|
||||
|
||||
def get_addr_balance(self,addr):
|
||||
return Token(g.token).balance(addr)
|
||||
class EthereumTokenTwGetBalance(EthereumTwGetBalance): pass
|
||||
|
||||
class EthereumAddrData(AddrData):
|
||||
|
||||
@classmethod
|
||||
def get_tw_data(cls):
|
||||
def get_tw_data(cls,wallet=None):
|
||||
vmsg('Getting address data from tracking wallet')
|
||||
tw = TrackingWallet().mmid_ordered_dict()
|
||||
tw = (wallet or TrackingWallet()).mmid_ordered_dict
|
||||
# emulate the output of RPC 'listaccounts' and 'getaddressesbyaccount'
|
||||
return [(mmid+' '+d['comment'],[d['addr']]) for mmid,d in list(tw.items())]
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ from mmgen.obj import *
|
|||
from mmgen.tx import MMGenTX,MMGenBumpTX,MMGenSplitTX
|
||||
|
||||
class EthereumMMGenTX(MMGenTX):
|
||||
desc = 'Ethereum transaction'
|
||||
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
|
||||
|
|
@ -39,6 +40,7 @@ class EthereumMMGenTX(MMGenTX):
|
|||
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
|
||||
|
|
@ -56,10 +58,6 @@ class EthereumMMGenTX(MMGenTX):
|
|||
self.usr_contract_data = HexStr(open(opt.contract_data).read().strip())
|
||||
self.disable_fee_check = True
|
||||
|
||||
@classmethod
|
||||
def get_receipt(cls,txid):
|
||||
return g.rpch.eth_getTransactionReceipt('0x'+txid)
|
||||
|
||||
@classmethod
|
||||
def get_exec_status(cls,txid,silent=False):
|
||||
d = g.rpch.eth_getTransactionReceipt('0x'+txid)
|
||||
|
|
@ -74,8 +72,7 @@ class EthereumMMGenTX(MMGenTX):
|
|||
return self.fee
|
||||
|
||||
def check_fee(self):
|
||||
if self.disable_fee_check: return
|
||||
assert self.fee <= g.proto.max_tx_fee
|
||||
assert self.disable_fee_check or (self.fee <= g.proto.max_tx_fee)
|
||||
|
||||
def get_hex_locktime(self): return None # TODO
|
||||
|
||||
|
|
@ -87,32 +84,31 @@ class EthereumMMGenTX(MMGenTX):
|
|||
return True
|
||||
return False
|
||||
|
||||
# hex data if signed, json if unsigned: see create_raw()
|
||||
# hex data if signed, json if unsigned
|
||||
def check_txfile_hex_data(self):
|
||||
if self.check_sigs():
|
||||
|
||||
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(''),
|
||||
'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']:
|
||||
self.token_addr = TokenAddr(etx.creates.hex())
|
||||
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"
|
||||
else:
|
||||
d = json.loads(self.hex)
|
||||
o = { 'from': CoinAddr(d['from']),
|
||||
'to': CoinAddr(d['to']) if d['to'] else Str(''),
|
||||
'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']),
|
||||
|
|
@ -120,8 +116,6 @@ class EthereumMMGenTX(MMGenTX):
|
|||
'chainId': Int(d['chainId']),
|
||||
'data': HexStr(d['data']) }
|
||||
self.tx_gas = o['startGas'] # approximate, but better than nothing
|
||||
self.usr_contract_data = o['data']
|
||||
if o['data'] and not o['to']: self.disable_fee_check = True
|
||||
self.fee = self.fee_rel2abs(o['gasPrice'].toWei())
|
||||
self.txobj = o
|
||||
return d # 'token_addr','decimals' required by subclass
|
||||
|
|
@ -129,7 +123,7 @@ class EthereumMMGenTX(MMGenTX):
|
|||
def get_nonce(self):
|
||||
return ETHNonce(int(g.rpch.parity_nextNonce('0x'+self.inputs[0].addr),16))
|
||||
|
||||
def make_txobj(self): # create_raw
|
||||
def make_txobj(self): # called by create_raw()
|
||||
chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in g.rpch.caps]
|
||||
self.txobj = {
|
||||
'from': self.inputs[0].addr,
|
||||
|
|
@ -147,12 +141,12 @@ class EthereumMMGenTX(MMGenTX):
|
|||
# thus removing an attack vector
|
||||
def create_raw(self):
|
||||
assert len(self.inputs) == 1,'Transaction has more than one input!'
|
||||
o_ok = (0,1) if self.usr_contract_data else (1,)
|
||||
o_num = len(self.outputs)
|
||||
assert o_num in o_ok,'Transaction has invalid number of outputs!'.format(o_num)
|
||||
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)
|
||||
self.make_txobj()
|
||||
ol = {k: (v.decode() if issubclass(type(v),bytes) else str(v)) for k,v in self.txobj.items()}
|
||||
self.hex = json.dumps(ol)
|
||||
odict = { k: str(v) for k,v in self.txobj.items() if k != 'token_to' }
|
||||
self.hex = json.dumps(odict)
|
||||
self.update_txid()
|
||||
|
||||
def del_output(self,idx): pass
|
||||
|
|
@ -221,9 +215,7 @@ class EthereumMMGenTX(MMGenTX):
|
|||
abs_fee = self.process_fee_spec(tx_fee,None,on_fail='return')
|
||||
if abs_fee == False:
|
||||
return False
|
||||
elif self.disable_fee_check:
|
||||
return abs_fee
|
||||
elif abs_fee > g.proto.max_tx_fee:
|
||||
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
|
||||
|
|
@ -240,9 +232,9 @@ class EthereumMMGenTX(MMGenTX):
|
|||
|
||||
def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse,sort):
|
||||
m = {}
|
||||
for k in ('in','out'):
|
||||
if len(getattr(self,k+'puts')):
|
||||
m[k] = getattr(self,k+'puts')[0].mmid if len(getattr(self,k+'puts')) else ''
|
||||
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}
|
||||
|
|
@ -252,50 +244,56 @@ class EthereumMMGenTX(MMGenTX):
|
|||
Nonce: {}
|
||||
Data: {d}
|
||||
\n""".replace('\t','')
|
||||
keys = ('from',('to','token_to')['token_to' in self.txobj],'amt','nonce')
|
||||
ld = len(self.txobj['data'])
|
||||
return fs.format( *((self.txobj[k] if self.txobj[k] != '' else Str('None')).hl() for k in keys),
|
||||
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['out'] if len(self.outputs) else '',
|
||||
f_mmid=m['in'])
|
||||
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.usr_contract_data else ''
|
||||
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 set_g_token(self):
|
||||
die(2,"Transaction object mismatch. Have you forgotten to include the '--token' option?")
|
||||
def resolve_g_token_from_tx_file(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)
|
||||
|
||||
def do_sign(self,d,wif,tx_num_str):
|
||||
d_in = {'to': bytes.fromhex(d['to']),
|
||||
'startgas': d['startGas'].toWei(),
|
||||
'gasprice': d['gasPrice'].toWei(),
|
||||
'value': d['amt'].toWei() if d['amt'] else 0,
|
||||
'nonce': d['nonce'],
|
||||
'data': bytes.fromhex(d['data'])}
|
||||
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']) }
|
||||
|
||||
from .pyethereum.transactions import Transaction
|
||||
|
||||
etx = Transaction(**d_in).sign(wif,d['chainId'])
|
||||
assert etx.sender.hex() == d['from'],(
|
||||
etx = Transaction(**o_conv).sign(wif,o['chainId'])
|
||||
assert etx.sender.hex() == o['from'],(
|
||||
'Sender address recovered from signature does not match true sender')
|
||||
|
||||
from . import rlp
|
||||
self.hex = rlp.encode(etx).hex()
|
||||
self.coin_txid = CoinTxID(etx.hash.hex())
|
||||
if d['data']:
|
||||
self.token_addr = TokenAddr(etx.creates.hex())
|
||||
|
||||
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())
|
||||
|
||||
assert self.check_sigs(),'Signature check failed'
|
||||
|
||||
def sign(self,tx_num_str,keys): # return True or False; don't exit or raise exception
|
||||
|
|
@ -310,42 +308,47 @@ class EthereumMMGenTX(MMGenTX):
|
|||
msg_r('Signing transaction{}...'.format(tx_num_str))
|
||||
|
||||
try:
|
||||
self.do_sign(self.txobj,keys[0].sec.wif,tx_num_str)
|
||||
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())))
|
||||
m = "{!r}: transaction signing failed!"
|
||||
msg(m.format(e.args[0]))
|
||||
return False
|
||||
|
||||
def is_in_mempool(self):
|
||||
return '0x'+self.coin_txid in [x['hash'] for x in g.rpch.parity_pendingTransactions()]
|
||||
|
||||
def is_in_wallet(self):
|
||||
d = g.rpch.eth_getTransactionReceipt('0x'+self.coin_txid)
|
||||
if d and 'blockNumber' in d and d['blockNumber'] is not None:
|
||||
return 1 + int(g.rpch.eth_blockNumber(),16) - int(d['blockNumber'],16)
|
||||
return False
|
||||
|
||||
def get_status(self,status=False):
|
||||
if self.is_in_mempool():
|
||||
|
||||
class r(object): pass
|
||||
|
||||
def is_in_mempool():
|
||||
if not 'full_node' in g.rpch.caps:
|
||||
return False
|
||||
return '0x'+self.coin_txid in [x['hash'] for x in g.rpch.parity_pendingTransactions()]
|
||||
|
||||
def is_in_wallet():
|
||||
d = g.rpch.eth_getTransactionReceipt('0x'+self.coin_txid)
|
||||
if d and 'blockNumber' in d and d['blockNumber'] is not None:
|
||||
r.confs = 1 + int(g.rpch.eth_blockNumber(),16) - int(d['blockNumber'],16)
|
||||
r.exec_status = int(d['status'],16)
|
||||
return True
|
||||
return False
|
||||
|
||||
if is_in_mempool():
|
||||
msg('Transaction is in mempool' if status else 'Warning: transaction is in mempool!')
|
||||
return
|
||||
|
||||
confs = self.is_in_wallet()
|
||||
if confs is not False:
|
||||
if self.usr_contract_data:
|
||||
exec_status = type(self).get_exec_status(self.coin_txid)
|
||||
if exec_status == 0:
|
||||
msg('Contract failed to execute!')
|
||||
else:
|
||||
msg('Contract successfully executed with status {}'.format(exec_status))
|
||||
die(0,'Transaction has {} confirmation{}'.format(confs,suf(confs,'s')))
|
||||
|
||||
if status:
|
||||
if 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!')
|
||||
|
||||
def send(self,prompt_user=True,exit_on_fail=False):
|
||||
|
|
@ -357,7 +360,7 @@ class EthereumMMGenTX(MMGenTX):
|
|||
|
||||
fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei())
|
||||
|
||||
if not self.disable_fee_check and fee > g.proto.max_tx_fee:
|
||||
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.capitalize(),g.proto.max_tx_fee,g.coin))
|
||||
|
||||
|
|
@ -383,91 +386,123 @@ class EthereumMMGenTX(MMGenTX):
|
|||
self.add_blockcount()
|
||||
return True
|
||||
|
||||
def get_cmdline_input_addrs(self):
|
||||
ret = []
|
||||
if opt.inputs:
|
||||
from mmgen.tw import TrackingWallet
|
||||
r = 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
|
||||
|
||||
def print_contract_addr(self):
|
||||
if 'token_addr' in self.txobj:
|
||||
msg('Contract address: {}'.format(self.txobj['token_addr'].hl()))
|
||||
|
||||
class EthereumTokenMMGenTX(EthereumMMGenTX):
|
||||
desc = 'Ethereum token transaction'
|
||||
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
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
if not kwargs.get('offline'):
|
||||
from mmgen.tw import TrackingWallet
|
||||
self.decimals = TrackingWallet().get_param('decimals')
|
||||
from .contract import Token
|
||||
self.token_obj = Token(g.token,self.decimals)
|
||||
EthereumMMGenTX.__init__(self,*args,**kwargs)
|
||||
|
||||
def update_change_output(self,change_amt):
|
||||
if self.outputs[0].is_chg:
|
||||
self.update_output_amt(0,self.inputs[0].amt)
|
||||
|
||||
def check_sufficient_funds(self,inputs_sum,sel_unspent):
|
||||
eth_bal = ETHAmt(int(g.rpch.eth_getBalance('0x'+sel_unspent[0].addr),16),'wei')
|
||||
# token transaction, so check both eth and token balances
|
||||
# TODO: add test with insufficient funds
|
||||
def precheck_sufficient_funds(self,inputs_sum,sel_unspent):
|
||||
eth_bal = self.twuo.wallet.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
|
||||
if self.send_amt > inputs_sum:
|
||||
msg(self.msg_low_coin.format(self.send_amt-inputs_sum,g.dcoin))
|
||||
return False
|
||||
return True
|
||||
return super().precheck_sufficient_funds(inputs_sum,sel_unspent)
|
||||
|
||||
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"
|
||||
if self.outputs[0].is_chg:
|
||||
send_acct_tbal = '0'
|
||||
else:
|
||||
from .contract import Token
|
||||
send_acct_tbal = Token(g.token).balance(self.inputs[0].addr) - self.outputs[0].amt
|
||||
return m.format(ETHAmt(change_amt).hl(),g.coin,ETHAmt(send_acct_tbal).hl(),g.dcoin)
|
||||
return m.format( change_amt.hl(), g.coin, token_bal.hl(), g.dcoin )
|
||||
|
||||
def get_change_amt(self): # here we know the fee
|
||||
eth_bal = ETHAmt(int(g.rpch.eth_getBalance('0x'+self.inputs[0].addr),16),'wei')
|
||||
eth_bal = self.twuo.wallet.get_eth_balance(self.inputs[0].addr)
|
||||
return eth_bal - self.fee
|
||||
|
||||
def set_g_token(self):
|
||||
def resolve_g_token_from_tx_file(self):
|
||||
g.dcoin = self.dcoin
|
||||
if is_hex_str(self.hex): return # for txsend we can leave g.token uninitialized
|
||||
d = json.loads(self.hex)
|
||||
if g.token.upper() == self.dcoin:
|
||||
g.token = d['token_addr']
|
||||
elif g.token != d['token_addr']:
|
||||
m1 = "'{p}': invalid --token parameter for {t} Ethereum token transaction file\n"
|
||||
m1 = "'{p}': invalid --token parameter for {t} {n} token transaction file\n"
|
||||
m2 = "Please use '--token={t}'"
|
||||
die(1,(m1+m2).format(p=g.token,t=self.dcoin))
|
||||
die(1,(m1+m2).format(p=g.token,t=self.dcoin,n=capfirst(g.proto.name)))
|
||||
|
||||
def make_txobj(self):
|
||||
def make_txobj(self): # called by create_raw()
|
||||
super(EthereumTokenMMGenTX,self).make_txobj()
|
||||
from .contract import Token
|
||||
t = Token(g.token)
|
||||
o = t.txcreate( self.inputs[0].addr,
|
||||
self.outputs[0].addr,
|
||||
(self.inputs[0].amt if self.outputs[0].is_chg else self.outputs[0].amt),
|
||||
self.start_gas,
|
||||
self.usr_rel_fee or self.fee_abs2rel(self.fee,to_unit='eth'))
|
||||
self.txobj['token_addr'] = self.token_addr = t.addr
|
||||
self.txobj['decimals'] = t.decimals()
|
||||
t = self.token_obj
|
||||
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 check_txfile_hex_data(self):
|
||||
d = super(EthereumTokenMMGenTX,self).check_txfile_hex_data()
|
||||
o = self.txobj
|
||||
from .contract import Token
|
||||
if self.check_sigs(): # online, from rlp
|
||||
rpc_init()
|
||||
|
||||
if self.check_sigs(): # online, from rlp and wallet
|
||||
o['token_addr'] = TokenAddr(o['to'])
|
||||
o['amt'] = Token(o['token_addr']).transferdata2amt(o['data'])
|
||||
else: # offline, from json
|
||||
o['decimals'] = self.decimals
|
||||
else: # offline, from json
|
||||
o['token_addr'] = TokenAddr(d['token_addr'])
|
||||
o['decimals'] = Int(d['decimals'])
|
||||
t = Token(o['token_addr'],o['decimals'])
|
||||
self.usr_contract_data = o['data'] = t.create_data(o['to'],o['amt'])
|
||||
o['decimals'] = Int(d['decimals'])
|
||||
|
||||
from .contract import Token
|
||||
t = self.token_obj = Token(o['token_addr'],o['decimals'])
|
||||
|
||||
if self.check_sigs(): # online, from rlp - 'amt' was eth amt, now token amt
|
||||
o['amt'] = t.transferdata2amt(o['data'])
|
||||
else: # offline, from json - 'amt' is token amt
|
||||
o['data'] = t.create_data(o['to'],o['amt'])
|
||||
|
||||
o['token_to'] = type(t).transferdata2sendaddr(o['data'])
|
||||
|
||||
def format_view_body(self,*args,**kwargs):
|
||||
if self.usr_contract_data:
|
||||
from .contract import Token
|
||||
self.txobj['token_to'] = Token.transferdata2sendaddr(self.usr_contract_data)
|
||||
return 'Token: {d} {c}\n{r}'.format(
|
||||
d=self.txobj['token_addr'].hl(),
|
||||
c=blue('(' + g.dcoin + ')'),
|
||||
r=super(EthereumTokenMMGenTX,self).format_view_body(*args,**kwargs))
|
||||
|
||||
def do_sign(self,d,wif,tx_num_str):
|
||||
from .contract import Token
|
||||
d = self.txobj
|
||||
t = Token(d['token_addr'],decimals=d['decimals'])
|
||||
tx_in = t.txcreate(d['from'],d['to'],d['amt'],self.start_gas,d['gasPrice'],nonce=d['nonce'])
|
||||
(self.hex,self.coin_txid) = t.txsign(tx_in,wif,d['from'],chain_id=d['chainId'])
|
||||
def do_sign(self,wif,tx_num_str):
|
||||
o = self.txobj
|
||||
t = self.token_obj
|
||||
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) = t.txsign(tx_in,wif,o['from'],chain_id=o['chainId'])
|
||||
assert self.check_sigs(),'Signature check failed'
|
||||
|
||||
class EthereumMMGenBumpTX(EthereumMMGenTX,MMGenBumpTX):
|
||||
|
|
|
|||
0
mmgen/devtools.py
Normal file → Executable file
0
mmgen/devtools.py
Normal file → Executable file
|
|
@ -38,6 +38,7 @@ class BadTwComment(Exception): mmcode = 2
|
|||
class RPCFailure(Exception): mmcode = 3
|
||||
class BadTxSizeEstimate(Exception): mmcode = 3
|
||||
class MaxInputSizeExceeded(Exception): mmcode = 3
|
||||
class WalletFileError(Exception): mmcode = 3
|
||||
|
||||
# 4: red hl, 'MMGen Fatal Error' + exception + message
|
||||
class BadMMGenTxID(Exception): mmcode = 4
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ class g(object):
|
|||
rpc_password = ''
|
||||
rpc_fail_on_command = ''
|
||||
rpch = None # global RPC handle
|
||||
use_cached_balances = False
|
||||
|
||||
# regtest:
|
||||
bob = False
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import time
|
|||
|
||||
from mmgen.common import *
|
||||
from mmgen.addr import AddrList,KeyAddrList
|
||||
from mmgen.obj import TwLabel
|
||||
from mmgen.obj import TwLabel,is_coin_addr
|
||||
|
||||
ai_msgs = lambda k: {
|
||||
'rescan': """
|
||||
|
|
@ -81,12 +81,6 @@ def import_mmgen_list(infile):
|
|||
rdie(2,'Segwit is not active on this chain. Cannot import Segwit addresses')
|
||||
return al
|
||||
|
||||
try:
|
||||
rpc_init()
|
||||
except UnrecognizedTokenSymbol as e:
|
||||
m = "When importing addresses for a new token, the token must be specified by address, not symbol."
|
||||
raise type(e)('{}\n{}'.format(e.args[0],m))
|
||||
|
||||
if len(cmd_args) == 1:
|
||||
infile = cmd_args[0]
|
||||
check_infile(infile)
|
||||
|
|
@ -111,6 +105,12 @@ err_msg = None
|
|||
from mmgen.tw import TrackingWallet
|
||||
tw = TrackingWallet(mode='w')
|
||||
|
||||
if g.token:
|
||||
if not is_coin_addr(g.token):
|
||||
m = "When importing addresses for a new token, the token must be specified by address, not symbol."
|
||||
raise InvalidTokenAddress('{!r}: invalid token address\n{}'.format(m))
|
||||
sym = tw.addr2sym(g.token) # check for presence in wallet or blockchain; raises exception on failure
|
||||
|
||||
if opt.rescan and not 'rescan' in tw.caps:
|
||||
msg("'--rescan' ignored: not supported by {}".format(type(tw).__name__))
|
||||
opt.rescan = False
|
||||
|
|
@ -127,6 +127,9 @@ def import_address(addr,label,rescan):
|
|||
except Exception as e:
|
||||
global err_msg
|
||||
err_msg = e.args[0]
|
||||
if g.token and not tw.get_token_param(g.token,'symbol'):
|
||||
tw.set_token_param(g.token,'symbol',sym)
|
||||
tw.set_token_param(g.token,'decimals',tw.token_obj.decimals())
|
||||
|
||||
w_n_of_m = len(str(al.num_addrs)) * 2 + 2
|
||||
w_mmid = 1 if opt.addrlist or opt.address else len(str(max(al.idxs()))) + 13
|
||||
|
|
@ -183,4 +186,4 @@ if opt.batch:
|
|||
ret = tw.batch_import_address(arg_list)
|
||||
msg('OK: {} addresses imported'.format(len(ret)))
|
||||
|
||||
tw.write()
|
||||
del tw
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ def sign_tx_file(txfile,signed_txs):
|
|||
g.token = tmp_tx.dcoin
|
||||
g.dcoin = tmp_tx.dcoin or g.coin
|
||||
|
||||
tx = mmgen.tx.MMGenTX(txfile)
|
||||
tx = mmgen.tx.MMGenTX(txfile,offline=True)
|
||||
|
||||
if g.proto.sign_mode == 'daemon':
|
||||
rpc_init(reinit=True)
|
||||
|
|
@ -200,6 +200,8 @@ def sign_tx_file(txfile,signed_txs):
|
|||
return False
|
||||
except Exception as e:
|
||||
msg('An error occurred: {}'.format(e.args[0]))
|
||||
if g.debug or g.traceback:
|
||||
print_stack_trace('AUTOSIGN {}'.format(txfile))
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ transaction reconfirmed before the timelock expires. Use at your own risk.
|
|||
|
||||
cmd_args = opts.init(opts_data,add_opts=['tx_fee','tx_fee_adj','comment_file'])
|
||||
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ opts_data = {
|
|||
-t, --type=t Specify address type (valid options: 'legacy',
|
||||
'compressed', 'segwit', 'bech32', 'zcash_z')
|
||||
-v, --verbose Produce more verbose output
|
||||
-X, --cached-balances Use cached balances (Ethereum only)
|
||||
""",
|
||||
'notes': """
|
||||
|
||||
|
|
@ -90,6 +91,8 @@ Type '{pn} help <command>' for help on a particular command
|
|||
|
||||
cmd_args = opts.init(opts_data,add_opts=['hidden_incog_input_params','in_fmt','use_old_ed25519'])
|
||||
|
||||
g.use_cached_balances = opt.cached_balances
|
||||
|
||||
if len(cmd_args) < 1: opts.usage()
|
||||
cmd = cmd_args.pop(0)
|
||||
|
||||
|
|
|
|||
|
|
@ -142,6 +142,10 @@ if g.proto.base_proto == 'Bitcoin':
|
|||
|
||||
if not opt.yes:
|
||||
tx.add_comment() # edits an existing comment
|
||||
|
||||
from mmgen.tw import TwUnspentOutputs
|
||||
tx.twuo = TwUnspentOutputs(minconf=opt.minconf)
|
||||
|
||||
tx.create_raw() # creates tx.hex, tx.txid
|
||||
tx.add_timestamp()
|
||||
tx.add_blockcount()
|
||||
|
|
@ -152,6 +156,8 @@ if not silent:
|
|||
msg(green('\nREPLACEMENT TRANSACTION:'))
|
||||
msg_r(tx.format_view(terse=True))
|
||||
|
||||
del tx.twuo.wallet
|
||||
|
||||
if seed_files or kl or kal:
|
||||
if txsign(tx,seed_files,kl,kal):
|
||||
tx.write_to_file(ask_write=False)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ opts_data = {
|
|||
-v, --verbose Produce more verbose output
|
||||
-V, --vsize-adj= f Adjust transaction's estimated vsize by factor 'f'
|
||||
-y, --yes Answer 'yes' to prompts, suppress non-essential output
|
||||
-X, --cached-balances Use cached balances (Ethereum only)
|
||||
""",
|
||||
'notes': '\n{}{}',
|
||||
},
|
||||
|
|
@ -71,6 +72,8 @@ opts_data = {
|
|||
|
||||
cmd_args = opts.init(opts_data)
|
||||
|
||||
g.use_cached_balances = opt.cached_balances
|
||||
|
||||
rpc_init()
|
||||
|
||||
from mmgen.tx import MMGenTX
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ opts_data = {
|
|||
wallet is scanned for subseeds.
|
||||
-v, --verbose Produce more verbose output
|
||||
-V, --vsize-adj= f Adjust transaction's estimated vsize by factor 'f'
|
||||
-X, --cached-balances Use cached balances (Ethereum only)
|
||||
-y, --yes Answer 'yes' to prompts, suppress non-essential output
|
||||
-z, --show-hash-presets Show information on available hash presets
|
||||
""",
|
||||
|
|
@ -108,6 +109,8 @@ column below:
|
|||
|
||||
cmd_args = opts.init(opts_data)
|
||||
|
||||
g.use_cached_balances = opt.cached_balances
|
||||
|
||||
rpc_init()
|
||||
|
||||
from mmgen.tx import *
|
||||
|
|
@ -127,7 +130,6 @@ if txsign(tx,seed_files,kl,kal):
|
|||
tx.write_to_file(ask_write=False)
|
||||
tx.send(exit_on_fail=True)
|
||||
tx.write_to_file(ask_overwrite=False,ask_write=False)
|
||||
if hasattr(tx,'token_addr'):
|
||||
msg('Contract address: {}'.format(tx.token_addr.hl()))
|
||||
tx.print_contract_addr()
|
||||
else:
|
||||
die(2,'Transaction could not be signed')
|
||||
|
|
|
|||
|
|
@ -68,6 +68,4 @@ if not opt.yes:
|
|||
|
||||
tx.send(exit_on_fail=True)
|
||||
tx.write_to_file(ask_overwrite=False,ask_write=False)
|
||||
|
||||
if hasattr(tx,'token_addr'):
|
||||
msg('Contract address: {}'.format(tx.token_addr.hl()))
|
||||
tx.print_contract_addr()
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ 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_str = ' #{}'.format(tx_num)
|
||||
tx = MMGenTX(tx_file)
|
||||
tx = MMGenTX(tx_file,offline=True)
|
||||
|
||||
if tx.marked_signed():
|
||||
msg('Transaction is already signed!'); continue
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ def opt_postproc_initializations():
|
|||
init_color(num_colors=('auto',256)[bool(g.force_256_color)])
|
||||
|
||||
g.coin = g.coin.upper() # allow user to use lowercase
|
||||
g.dcoin = g.coin
|
||||
g.dcoin = g.coin # the display coin; for ERC20 tokens, g.dcoin is set to the token symbol
|
||||
|
||||
def set_data_dir_root():
|
||||
g.data_dir_root = os.path.normpath(os.path.expanduser(opt.data_dir)) if opt.data_dir else \
|
||||
|
|
@ -338,7 +338,6 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
|
|||
opt.verbose,opt.quiet = (True,None)
|
||||
if g.debug_opts: opt_postproc_debug()
|
||||
|
||||
g.altcoin_data_dir = os.path.join(g.data_dir_root,'altcoins')
|
||||
warn_altcoins(altcoin_trust_level)
|
||||
|
||||
# We don't need this data anymore
|
||||
|
|
@ -348,7 +347,7 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
|
|||
|
||||
def opt_is_tx_fee(val,desc):
|
||||
from mmgen.tx import MMGenTX
|
||||
tx = MMGenTX()
|
||||
tx = MMGenTX(offline=True)
|
||||
# TODO: size is just a guess; do this check after parsing tx file
|
||||
ret = tx.process_fee_spec(val,224,on_fail='return')
|
||||
# Non-standard startgas: disable fee checking
|
||||
|
|
|
|||
|
|
@ -21,11 +21,9 @@ rpc.py: Cryptocoin RPC library for the MMGen suite
|
|||
"""
|
||||
|
||||
import http.client,base64,json
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from mmgen.common import *
|
||||
from mmgen.obj import MMGenObject
|
||||
|
||||
def dmsg_rpc(s):
|
||||
if g.debug_rpc: msg(s)
|
||||
|
|
@ -82,6 +80,9 @@ class CoinDaemonRPCConnection(MMGenObject):
|
|||
# With on_fail='return', returns 'rpcfail',(resp_object,(die_args))
|
||||
def request(self,cmd,*args,**kwargs):
|
||||
|
||||
if g.debug:
|
||||
print_stack_trace('RPC REQUEST {}\n args: {!r}\n kwargs: {!r}'.format(cmd,args,kwargs))
|
||||
|
||||
if g.rpc_fail_on_command == cmd:
|
||||
cmd = 'badcommand_' + cmd
|
||||
|
||||
|
|
|
|||
|
|
@ -554,7 +554,7 @@ class MMGenToolCmdFile(MMGenToolCmdBase):
|
|||
|
||||
sep = '—'*77+'\n'
|
||||
return sep.join(
|
||||
[MMGenTX(fn).format_view(terse=terse,sort=tx_sort) for fn in flist.names()]
|
||||
[MMGenTX(fn,offline=True).format_view(terse=terse,sort=tx_sort) for fn in flist.names()]
|
||||
).rstrip()
|
||||
|
||||
class MMGenToolCmdFileCrypt(MMGenToolCmdBase):
|
||||
|
|
@ -772,6 +772,7 @@ class MMGenToolCmdRPC(MMGenToolCmdBase):
|
|||
die(1,m.format(mmgen_addrs))
|
||||
usr_addr_list = [MMGenID('{}:{}'.format(a[0],i)) for i in AddrIdxList(a[1])]
|
||||
|
||||
rpc_init()
|
||||
from mmgen.tw import TwAddrList
|
||||
al = TwAddrList(usr_addr_list,minconf,showempty,showbtcaddrs,all_labels)
|
||||
if not al:
|
||||
|
|
@ -793,7 +794,9 @@ class MMGenToolCmdRPC(MMGenToolCmdBase):
|
|||
twuo.do_sort(sort,reverse=reverse)
|
||||
twuo.age_fmt = age_fmt
|
||||
twuo.show_mmid = show_mmid
|
||||
return twuo.format_for_printing(color=True) if wide else twuo.format_for_display()
|
||||
ret = twuo.format_for_printing(color=True) if wide else twuo.format_for_display()
|
||||
del twuo.wallet
|
||||
return ret
|
||||
|
||||
def add_label(self,mmgen_or_coin_addr:str,label:str):
|
||||
"add descriptive label for address in tracking wallet"
|
||||
|
|
|
|||
203
mmgen/tw.py
203
mmgen/tw.py
|
|
@ -20,6 +20,8 @@
|
|||
tw: Tracking wallet methods for the MMGen suite
|
||||
"""
|
||||
|
||||
import json
|
||||
from mmgen.exception import *
|
||||
from mmgen.common import *
|
||||
from mmgen.obj import *
|
||||
from mmgen.tx import is_mmgen_id
|
||||
|
|
@ -79,7 +81,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
|
|||
""".strip().format(g.proj_name.lower())
|
||||
}
|
||||
|
||||
def __init__(self,minconf=1):
|
||||
def __init__(self,minconf=1,addrs=[]):
|
||||
self.unspent = self.MMGenTwOutputList()
|
||||
self.fmt_display = ''
|
||||
self.fmt_print = ''
|
||||
|
|
@ -88,12 +90,16 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
|
|||
self.group = False
|
||||
self.show_mmid = True
|
||||
self.minconf = minconf
|
||||
self.get_unspent_data()
|
||||
self.addrs = addrs
|
||||
self.age_fmt = 'days'
|
||||
self.sort_key = 'age'
|
||||
self.do_sort()
|
||||
self.disp_prec = self.get_display_precision()
|
||||
|
||||
self.wallet = TrackingWallet('w')
|
||||
self.get_unspent_data()
|
||||
self.do_sort()
|
||||
|
||||
|
||||
@property
|
||||
def age_fmt(self):
|
||||
return self._age_fmt
|
||||
|
|
@ -121,7 +127,9 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
|
|||
# 4. include_unsafe (boolean, optional, default=true) Include outputs that are not safe to spend
|
||||
# 5. query_options (json object, optional) JSON with query options
|
||||
|
||||
return g.rpch.listunspent(self.minconf)
|
||||
# for now, self.addrs is just an empty list for Bitcoin and friends
|
||||
add_args = (9999999,self.addrs) if self.addrs else ()
|
||||
return g.rpch.listunspent(self.minconf,*add_args)
|
||||
|
||||
def get_unspent_data(self):
|
||||
if g.bogus_wallet_data: # for debugging purposes only
|
||||
|
|
@ -326,6 +334,8 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
|
|||
else:
|
||||
if action == 'a_addr_delete':
|
||||
fs = "Removing {} #{} from tracking wallet. Is this what you want?"
|
||||
elif action == 'a_balance_refresh':
|
||||
fs = "Refreshing tracking wallet {} #{}. Is this what you want?"
|
||||
if keypress_confirm(fs.format(self.item_desc,n)):
|
||||
return n
|
||||
|
||||
|
|
@ -356,11 +366,19 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
|
|||
elif action == 'd_redraw': pass
|
||||
elif action == 'd_reverse': self.unspent.reverse(); self.reverse = not self.reverse
|
||||
elif action == 'a_quit': msg(''); return self.unspent
|
||||
elif action == 'a_balance_refresh':
|
||||
idx = self.get_idx_from_user(action)
|
||||
if idx:
|
||||
e = self.unspent[idx-1]
|
||||
bal = self.wallet.get_balance(e.addr,force_rpc=True)
|
||||
self.get_unspent_data()
|
||||
self.do_sort()
|
||||
oneshot_msg = yellow('{} balance for account #{} refreshed\n\n'.format(g.dcoin,idx))
|
||||
elif action == 'a_lbl_add':
|
||||
idx,lbl = self.get_idx_from_user(action)
|
||||
if idx:
|
||||
e = self.unspent[idx-1]
|
||||
if TrackingWallet(mode='w').add_label(e.twmmid,lbl,addr=e.addr):
|
||||
if self.wallet.add_label(e.twmmid,lbl,addr=e.addr):
|
||||
self.get_unspent_data()
|
||||
self.do_sort()
|
||||
a = 'added to' if lbl else 'removed from'
|
||||
|
|
@ -371,7 +389,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
|
|||
idx = self.get_idx_from_user(action)
|
||||
if idx:
|
||||
e = self.unspent[idx-1]
|
||||
if TrackingWallet(mode='w').remove_address(e.addr):
|
||||
if self.wallet.remove_address(e.addr):
|
||||
self.get_unspent_data()
|
||||
self.do_sort()
|
||||
oneshot_msg = yellow("{} #{} removed\n\n".format(capfirst(self.item_desc),idx))
|
||||
|
|
@ -398,7 +416,7 @@ class TwAddrList(MMGenDict):
|
|||
def __new__(cls,*args,**kwargs):
|
||||
return MMGenDict.__new__(altcoin_subclass(cls,'tw','TwAddrList'),*args,**kwargs)
|
||||
|
||||
def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels):
|
||||
def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None):
|
||||
|
||||
def check_dup_mmid(acct_labels):
|
||||
mmid_prev,err = None,False
|
||||
|
|
@ -530,16 +548,146 @@ class TwAddrList(MMGenDict):
|
|||
|
||||
class TrackingWallet(MMGenObject):
|
||||
|
||||
caps = ('rescan','batch')
|
||||
data_key = 'addresses'
|
||||
use_tw_file = False
|
||||
aggressive_sync = False
|
||||
|
||||
def __new__(cls,*args,**kwargs):
|
||||
return MMGenObject.__new__(altcoin_subclass(cls,'tw','TrackingWallet'))
|
||||
|
||||
mode = 'r'
|
||||
caps = ('rescan','batch')
|
||||
def __init__(self,mode='r',no_rpc=False):
|
||||
|
||||
def __init__(self,mode='r'):
|
||||
m = "'{}': invalid 'mode' parameter for {} constructor"
|
||||
assert mode in ('r','w'),m.format(mode,type(self).__name__)
|
||||
if g.debug:
|
||||
print_stack_trace('TW INIT {!r} {!r}'.format(mode,self))
|
||||
|
||||
assert mode in ('r','w'),"{!r}: wallet mode must be 'r' or 'w'".format(self)
|
||||
self.mode = mode
|
||||
self.desc = self.base_desc = '{} tracking wallet'.format(capfirst(g.proto.name))
|
||||
|
||||
if self.use_tw_file:
|
||||
self.init_from_wallet_file()
|
||||
else:
|
||||
self.init_empty()
|
||||
|
||||
if self.data['coin'] != g.coin:
|
||||
m = 'Tracking wallet coin ({}) does not match current coin ({})!'
|
||||
raise WalletFileError(m.format(self.data['coin'],g.coin))
|
||||
|
||||
self.conv_types(self.data[self.data_key])
|
||||
self.rpc_init()
|
||||
self.cur_balances = {} # cache balances to prevent repeated lookups per program invocation
|
||||
|
||||
def init_empty(self):
|
||||
self.data = { 'coin': g.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_root,'altcoins',g.coin.lower(),g.proto.data_subdir) )
|
||||
self.tw_fn = os.path.join(tw_dir,'tracking-wallet.json')
|
||||
|
||||
check_or_create_dir(tw_dir)
|
||||
|
||||
try:
|
||||
self.orig_data = get_data_from_file(self.tw_fn,quiet=True)
|
||||
self.data = json.loads(self.orig_data)
|
||||
except:
|
||||
try: os.stat(self.tw_fn)
|
||||
except:
|
||||
self.orig_data = ''
|
||||
self.init_empty()
|
||||
self.force_write()
|
||||
else:
|
||||
m = "File '{}' exists but does not contain valid json data"
|
||||
raise WalletFileError(m.format(self.tw_fn))
|
||||
else:
|
||||
self.upgrade_wallet_maybe()
|
||||
|
||||
# ensure that wallet file is written when user exits via KeyboardInterrupt:
|
||||
if self.mode == 'w':
|
||||
import atexit
|
||||
def del_tw(tw):
|
||||
dmsg('Running exit handler del_tw() for {!r}'.format(tw))
|
||||
del tw
|
||||
atexit.register(del_tw,self)
|
||||
|
||||
# TrackingWallet instances must be explicitly destroyed with 'del tw', 'del twuo.wallet'
|
||||
# and the like to ensure the instance is deleted and wallet is written before global
|
||||
# vars are destroyed by interpreter at shutdown.
|
||||
# This is especially important, as exceptions are ignored within __del__():
|
||||
# /usr/share/doc/python3.6-doc/html/reference/datamodel.html#object.__del__
|
||||
# This code can only be debugged by examining the program output. Since no exceptions
|
||||
# are raised, errors will not be caught by the test suite.
|
||||
def __del__(self):
|
||||
if g.debug:
|
||||
print_stack_trace('TW DEL {!r}'.format(self))
|
||||
|
||||
if self.mode == 'w':
|
||||
self.write()
|
||||
elif g.debug:
|
||||
msg('read-only wallet, doing nothing')
|
||||
|
||||
def upgrade_wallet_maybe(self): pass
|
||||
|
||||
@staticmethod
|
||||
def conv_types(ad):
|
||||
for k,v in ad.items():
|
||||
if k in ('params','coin'): continue
|
||||
v['mmid'] = TwMMGenID(v['mmid'],on_fail='raise')
|
||||
v['comment'] = TwComment(v['comment'],on_fail='raise')
|
||||
|
||||
def rpc_init(self):
|
||||
rpc_init()
|
||||
|
||||
@property
|
||||
def data_root(self):
|
||||
return self.data[self.data_key]
|
||||
|
||||
@property
|
||||
def data_root_desc(self):
|
||||
return self.data_key
|
||||
|
||||
def cache_balance(self,addr,bal,session_cache,data_root,force=False):
|
||||
if force or addr not in session_cache:
|
||||
session_cache[addr] = str(bal)
|
||||
if addr in data_root:
|
||||
data_root[addr]['balance'] = str(bal)
|
||||
if self.aggressive_sync:
|
||||
self.write()
|
||||
|
||||
def get_cached_balance(self,addr,session_cache,data_root):
|
||||
if addr in session_cache:
|
||||
return g.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'])
|
||||
|
||||
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)
|
||||
if ret == None:
|
||||
ret = self.rpc_get_balance(addr)
|
||||
self.cache_balance(addr,ret,self.cur_balances,self.data_root)
|
||||
return ret
|
||||
|
||||
def rpc_get_balance(self,addr):
|
||||
raise NotImplementedError('not implemented')
|
||||
|
||||
@property
|
||||
def sorted_list(self):
|
||||
return sorted(
|
||||
[ { 'addr':x[0],
|
||||
'mmid':x[1]['mmid'],
|
||||
'comment':x[1]['comment'] }
|
||||
for x in self.data_root.items() if x[0] not in ('params','coin') ],
|
||||
key=lambda x: x['mmid'].sort_key+x['addr'] )
|
||||
|
||||
@property
|
||||
def mmid_ordered_dict(self):
|
||||
from collections import OrderedDict
|
||||
return OrderedDict([(x['mmid'],{'addr':x['addr'],'comment':x['comment']}) for x in self.sorted_list])
|
||||
|
||||
@write_mode
|
||||
def import_address(self,addr,label,rescan):
|
||||
|
|
@ -549,11 +697,38 @@ class TrackingWallet(MMGenObject):
|
|||
def batch_import_address(self,arg_list):
|
||||
return g.rpch.importaddress(arg_list,batch=True)
|
||||
|
||||
def force_write(self):
|
||||
mode_save = self.mode
|
||||
self.mode = 'w'
|
||||
self.write()
|
||||
self.mode = mode_save
|
||||
|
||||
@write_mode
|
||||
def write(self): pass
|
||||
def write_changed(self,data):
|
||||
write_data_to_file(
|
||||
self.tw_fn,data,
|
||||
desc='{} data'.format(self.base_desc),
|
||||
ask_overwrite=False,ignore_opt_outdir=True,quiet=True,
|
||||
check_data=True,cmp_data=self.orig_data)
|
||||
self.orig_data = data
|
||||
|
||||
def write(self): # use 'check_data' to check wallet hasn't been altered by another program
|
||||
if not self.use_tw_file:
|
||||
dmsg("'use_tw_file' is False, doing nothing")
|
||||
return
|
||||
dmsg('write(): checking if {} data has changed'.format(self.desc))
|
||||
wdata = json.dumps(self.data)
|
||||
|
||||
if self.orig_data != wdata:
|
||||
if g.debug:
|
||||
print_stack_trace('TW DATA CHANGED {!r}'.format(self))
|
||||
print_diff(self.orig_data,wdata,from_json=True)
|
||||
self.write_changed(wdata)
|
||||
elif g.debug:
|
||||
msg('Data is unchanged\n')
|
||||
|
||||
def is_in_wallet(self,addr):
|
||||
return addr in TwAddrList([],0,True,True,True).coinaddr_list()
|
||||
return addr in TwAddrList([],0,True,True,True,wallet=self).coinaddr_list()
|
||||
|
||||
@write_mode
|
||||
def set_label(self,coinaddr,lbl):
|
||||
|
|
|
|||
58
mmgen/tx.py
58
mmgen/tx.py
|
|
@ -82,6 +82,7 @@ def mmaddr2coinaddr(mmaddr,ad_w,ad_f):
|
|||
return CoinAddr(coin_addr)
|
||||
|
||||
def segwit_is_active(exit_on_error=False):
|
||||
rpc_init()
|
||||
d = g.rpch.getblockchaininfo()
|
||||
if d['chain'] == 'regtest':
|
||||
return True
|
||||
|
|
@ -306,6 +307,7 @@ class MMGenTX(MMGenObject):
|
|||
view_sort_orders = ('addr','raw')
|
||||
dfl_view_sort_order = 'addr'
|
||||
|
||||
msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)'
|
||||
msg_low_coin = 'Selected outputs insufficient to fund this transaction ({} {} needed)'
|
||||
msg_no_change_output = """
|
||||
ERROR: No change address specified. If you wish to create a transaction with
|
||||
|
|
@ -318,7 +320,7 @@ inputs must be supplied to '{pnl}-txsign' in a file with the '--keys-from-file'
|
|||
option.
|
||||
Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_name.lower())
|
||||
|
||||
def __init__(self,filename=None,metadata_only=False,caller=None,quiet_open=False):
|
||||
def __init__(self,filename=None,metadata_only=False,caller=None,quiet_open=False,offline=False):
|
||||
self.inputs = MMGenTxInputList()
|
||||
self.outputs = MMGenTxOutputList()
|
||||
self.send_amt = g.proto.coin_amt('0') # total amt minus change
|
||||
|
|
@ -416,6 +418,8 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
|
|||
self.hex = HexStr(g.rpch.createrawtransaction(i,o))
|
||||
self.update_txid()
|
||||
|
||||
def print_contract_addr(self): pass
|
||||
|
||||
# returns true if comment added or changed
|
||||
def add_comment(self,infile=None):
|
||||
if infile:
|
||||
|
|
@ -1276,7 +1280,8 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
|
|||
if not self.chain and not self.inputs[0].addr.is_for_chain('testnet'):
|
||||
self.chain = 'mainnet'
|
||||
|
||||
if self.dcoin: self.set_g_token()
|
||||
if self.dcoin:
|
||||
self.resolve_g_token_from_tx_file()
|
||||
|
||||
def process_cmd_arg(self,arg,ad_f,ad_w):
|
||||
|
||||
|
|
@ -1320,7 +1325,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
|
|||
check_infile(a)
|
||||
ad_f.add(AddrList(a))
|
||||
|
||||
ad_w = AddrData(source='tw')
|
||||
ad_w = AddrData(source='tw',wallet=self.twuo.wallet)
|
||||
|
||||
self.process_cmd_args(cmd_args,ad_f,ad_w)
|
||||
|
||||
|
|
@ -1338,9 +1343,13 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
|
|||
return selected
|
||||
msg('Unspent output number must be <= {}'.format(len(unspent)))
|
||||
|
||||
def check_sufficient_funds(self,inputs_sum,foo):
|
||||
if self.send_amt > inputs_sum:
|
||||
msg(self.msg_low_coin.format(self.send_amt-inputs_sum,g.coin))
|
||||
# we don't know fee yet, so perform preliminary check with fee == 0
|
||||
def precheck_sufficient_funds(self,inputs_sum,sel_unspent):
|
||||
if self.twuo.total < self.send_amt:
|
||||
msg(self.msg_wallet_low_coin.format(self.send_amt-inputs_sum,g.dcoin))
|
||||
return False
|
||||
if inputs_sum < self.send_amt:
|
||||
msg(self.msg_low_coin.format(self.send_amt-inputs_sum,g.dcoin))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
@ -1380,17 +1389,21 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
|
|||
|
||||
return set(sel_nums) # silently discard duplicates
|
||||
|
||||
def get_inputs_from_user(self,tw):
|
||||
def get_cmdline_input_addrs(self):
|
||||
# Bitcoin full node, call doesn't go to the network, so just call listunspent with addrs=[]
|
||||
return []
|
||||
|
||||
def get_inputs_from_user(self):
|
||||
|
||||
while True:
|
||||
us_f = ('select_unspent','select_unspent_cmdline')[bool(opt.inputs)]
|
||||
sel_nums = getattr(self,us_f)(tw.unspent)
|
||||
us_f = self.select_unspent_cmdline if opt.inputs else self.select_unspent
|
||||
sel_nums = us_f(self.twuo.unspent)
|
||||
|
||||
msg('Selected output{}: {}'.format(suf(sel_nums,'s'),' '.join(map(str,sel_nums))))
|
||||
sel_unspent = tw.MMGenTwOutputList([tw.unspent[i-1] for i in sel_nums])
|
||||
msg('Selected output{}: {}'.format(suf(sel_nums),' '.join(map(str,sel_nums))))
|
||||
sel_unspent = self.twuo.MMGenTwOutputList([self.twuo.unspent[i-1] for i in sel_nums])
|
||||
|
||||
inputs_sum = sum(s.amt for s in sel_unspent)
|
||||
if not self.check_sufficient_funds(inputs_sum,sel_unspent):
|
||||
if not self.precheck_sufficient_funds(inputs_sum,sel_unspent):
|
||||
continue
|
||||
|
||||
non_mmaddrs = [i for i in sel_unspent if i.twmmid.type == 'non-mmgen']
|
||||
|
|
@ -1406,7 +1419,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
|
|||
|
||||
change_amt = self.get_change_amt()
|
||||
|
||||
if change_amt >= 0: # TODO: show both ETH and token amts remaining
|
||||
if change_amt >= 0:
|
||||
p = self.final_inputs_ok_msg(change_amt)
|
||||
if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
|
||||
if opt.yes: msg(p)
|
||||
|
|
@ -1426,17 +1439,20 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
|
|||
|
||||
if opt.comment_file: self.add_comment(opt.comment_file)
|
||||
|
||||
if not do_info: self.get_outputs_from_cmdline(cmd_args)
|
||||
twuo_addrs = self.get_cmdline_input_addrs()
|
||||
|
||||
from mmgen.tw import TwUnspentOutputs
|
||||
self.twuo = TwUnspentOutputs(minconf=opt.minconf,addrs=twuo_addrs)
|
||||
|
||||
if not do_info:
|
||||
self.get_outputs_from_cmdline(cmd_args)
|
||||
|
||||
do_license_msg()
|
||||
|
||||
from mmgen.tw import TwUnspentOutputs
|
||||
tw = TwUnspentOutputs(minconf=opt.minconf)
|
||||
|
||||
if not opt.inputs:
|
||||
tw.view_and_sort(self)
|
||||
self.twuo.view_and_sort(self)
|
||||
|
||||
tw.display_total()
|
||||
self.twuo.display_total()
|
||||
|
||||
if do_info: sys.exit(0)
|
||||
|
||||
|
|
@ -1446,7 +1462,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
|
|||
('Unknown','{} {}'.format(self.send_amt.hl(),g.dcoin))[bool(self.send_amt)]
|
||||
))
|
||||
|
||||
change_amt = self.get_inputs_from_user(tw)
|
||||
change_amt = self.get_inputs_from_user()
|
||||
|
||||
self.update_change_output(change_amt)
|
||||
self.update_send_amt(change_amt)
|
||||
|
|
@ -1479,6 +1495,8 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
|
|||
if not opt.yes:
|
||||
self.view_with_prompt('View decoded transaction?')
|
||||
|
||||
del self.twuo
|
||||
|
||||
class MMGenBumpTX(MMGenTX):
|
||||
|
||||
def __new__(cls,*args,**kwargs):
|
||||
|
|
|
|||
1
test/ref/ethereum/tracking-wallet-v1.json
Normal file
1
test/ref/ethereum/tracking-wallet-v1.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"coin":"ETH","e704b6cfd9f0edb2e6cfbd0c913438d37ede7b35":{"mmid":"98831F3A:E:1","comment":""},"b7d06382a998817a16ba487a99636bf8cae29d9d":{"mmid":"98831F3A:E:2","comment":""},"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef":{"mmid":"eth:deadbeefdeadbeefdeadbeefdeadbeefdeadbeef","comment":""}}
|
||||
1
test/ref/ethereum/tracking-wallet-v2.json
Normal file
1
test/ref/ethereum/tracking-wallet-v2.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"coin":"ETH","accounts":{"e704b6cfd9f0edb2e6cfbd0c913438d37ede7b35":{"mmid":"98831F3A:E:1","comment":""},"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef":{"mmid":"eth:deadbeefdeadbeefdeadbeefdeadbeefdeadbeef","comment":""}},"tokens":{"d5f051401ca478b34c80d0b5a119e437dc6d9df5":{"6e0fbe42e1343309b3ccb9068dbad6132f86c96f":{"mmid":"98831F3A:E:12","comment":""},"b72268fa55a57fe838a745b27c90f0edb415af61":{"mmid":"98831F3A:E:13","comment":""}},"3dd0864668c36d27b53a98137764c99f9fd5b7b2":{"ca2b705adf43151ce61e01c653424e9790e9eb84":{"mmid":"98831F3A:E:22","comment":""},"7227a8fd728b74208255c6a8a09cb9fb66b1230c":{"mmid":"98831F3A:E:23","comment":""}}}}
|
||||
|
|
@ -53,11 +53,13 @@ except:
|
|||
|
||||
if re.match(r'\b0.5.1\b',solc_ver): # Raspbian Stretch
|
||||
vbal1 = '1.2288337'
|
||||
vbal1a = 'TODO'
|
||||
vbal2 = '99.997085083'
|
||||
vbal3 = '1.23142165'
|
||||
vbal4 = '127.0287837'
|
||||
elif solc_ver == '' or re.match(r'\b0.5.3\b',solc_ver): # Ubuntu Bionic
|
||||
vbal1 = '1.2288487'
|
||||
vbal1a = '1.22627465'
|
||||
vbal2 = '99.997092733'
|
||||
vbal3 = '1.23142915'
|
||||
vbal4 = '127.0287987'
|
||||
|
|
@ -77,32 +79,44 @@ bals = {
|
|||
(burn_addr + '\s+Non-MMGen',amt1)],
|
||||
'8': [ ('98831F3A:E:1','0'),
|
||||
('98831F3A:E:2','23.45495'),
|
||||
('98831F3A:E:11',vbal1,'a'),
|
||||
('98831F3A:E:11',vbal1,'a1'),
|
||||
('98831F3A:E:12','99.99895'),
|
||||
('98831F3A:E:21','2.345'),
|
||||
(burn_addr + '\s+Non-MMGen',amt1)],
|
||||
'9': [ ('98831F3A:E:1','0'),
|
||||
('98831F3A:E:2','23.45495'),
|
||||
('98831F3A:E:11',vbal1,'a'),
|
||||
('98831F3A:E:11',vbal1,'a1'),
|
||||
('98831F3A:E:12',vbal2),
|
||||
('98831F3A:E:21','2.345'),
|
||||
(burn_addr + '\s+Non-MMGen',amt1)],
|
||||
'10': [ ('98831F3A:E:1','0'),
|
||||
('98831F3A:E:2','23.0218'),
|
||||
('98831F3A:E:3','0.4321'),
|
||||
('98831F3A:E:11',vbal1,'a1'),
|
||||
('98831F3A:E:12',vbal2),
|
||||
('98831F3A:E:21','2.345'),
|
||||
(burn_addr + '\s+Non-MMGen',amt1)]
|
||||
}
|
||||
|
||||
token_bals = {
|
||||
'1': [ ('98831F3A:E:11','1000','1.234')],
|
||||
'2': [ ('98831F3A:E:11','998.76544',vbal3,'a'),
|
||||
'2': [ ('98831F3A:E:11','998.76544',vbal3,'a1'),
|
||||
('98831F3A:E:12','1.23456','0')],
|
||||
'3': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
|
||||
'3': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a1'),
|
||||
('98831F3A:E:12','1.23456','0')],
|
||||
'4': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
|
||||
'4': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a1'),
|
||||
('98831F3A:E:12','1.23456','0'),
|
||||
(burn_addr + '\s+Non-MMGen',amt2,amt1)],
|
||||
'5': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
|
||||
'5': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a1'),
|
||||
('98831F3A:E:12','1.23456','99.99895'),
|
||||
(burn_addr + '\s+Non-MMGen',amt2,amt1)],
|
||||
'6': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
|
||||
'6': [ ('98831F3A:E:11','110.654317776666555545',vbal1,'a1'),
|
||||
('98831F3A:E:12','0',vbal2),
|
||||
('98831F3A:E:13','1.23456','0'),
|
||||
(burn_addr + '\s+Non-MMGen',amt2,amt1)],
|
||||
'7': [ ('98831F3A:E:11','67.444317776666555545',vbal1a,'a2'),
|
||||
('98831F3A:E:12','43.21',vbal2),
|
||||
('98831F3A:E:13','1.23456','0'),
|
||||
(burn_addr + '\s+Non-MMGen',amt2,amt1)]
|
||||
}
|
||||
token_bals_getbalance = {
|
||||
|
|
@ -120,12 +134,15 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
tmpdir_nums = [22]
|
||||
cmd_group = (
|
||||
('setup', 'Ethereum Parity dev mode tests for coin {} (start parity)'.format(g.coin)),
|
||||
('wallet_upgrade1', 'upgrading the tracking wallet (v1 -> v2)'),
|
||||
('wallet_upgrade2', 'upgrading the tracking wallet (v2 -> v3)'),
|
||||
('addrgen', 'generating addresses'),
|
||||
('addrimport', 'importing addresses'),
|
||||
('addrimport_dev_addr', "importing Parity dev address 'Ox00a329c..'"),
|
||||
|
||||
('txcreate1', 'creating a transaction (spend from dev address to address :1)'),
|
||||
('txsign1', 'signing the transaction'),
|
||||
('tx_status0', 'getting the transaction status'),
|
||||
('txsign1_ni', 'signing the transaction (non-interactive)'),
|
||||
('txsend1', 'sending the transaction'),
|
||||
('bal1', 'the {} balance'.format(g.coin)),
|
||||
|
|
@ -147,6 +164,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
|
||||
('txsign4', 'signing the transaction'),
|
||||
('txsend4', 'sending the transaction'),
|
||||
('tx_status1a', 'getting the transaction status'),
|
||||
('bal4', 'the {} balance'.format(g.coin)),
|
||||
|
||||
('txcreate5', 'creating a transaction (fund burn address)'),
|
||||
|
|
@ -192,6 +210,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
('token_txcreate1', 'creating a token transaction'),
|
||||
('token_txsign1', 'signing the transaction'),
|
||||
('token_txsend1', 'sending the transaction'),
|
||||
('tx_status3', 'getting the transaction status'),
|
||||
('token_bal2', 'the {} balance and token balance'.format(g.coin)),
|
||||
|
||||
('token_txcreate2', 'creating a token transaction (to burn address)'),
|
||||
|
|
@ -232,6 +251,19 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
('token_listaddresses1','listaddresses --token=mm1'),
|
||||
('token_listaddresses2','listaddresses --token=mm1 showempty=1'),
|
||||
|
||||
('twview_cached_balances','twview (cached balances)'),
|
||||
('token_twview_cached_balances','token twview (cached balances)'),
|
||||
('txcreate_cached_balances','txcreate (cached balances)'),
|
||||
('token_txcreate_cached_balances','token txcreate (cached balances)'),
|
||||
|
||||
('txdo_cached_balances', 'txdo (cached balances)'),
|
||||
('txcreate_refresh_balances','refreshing balances'),
|
||||
('bal10', 'the {} balance'.format(g.coin)),
|
||||
|
||||
('token_txdo_cached_balances', 'token txdo (cached balances)'),
|
||||
('token_txcreate_refresh_balances','refreshing token balances'),
|
||||
('token_bal7', 'the token balance'),
|
||||
|
||||
('twview1','twview'),
|
||||
('twview2','twview wide=1'),
|
||||
('twview3','twview wide=1 sort=age (ignored)'),
|
||||
|
|
@ -247,10 +279,12 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
('edit_label2','adding label to addr #{} in {} tracking wallet (lat+cyr+gr)'.format(del_addrs[1],g.coin)),
|
||||
('edit_label3','removing label from addr #{} in {} tracking wallet'.format(del_addrs[0],g.coin)),
|
||||
|
||||
('token_edit_label1','adding label to addr #{} in {} token tracking wallet'.format(del_addrs[0],g.coin)),
|
||||
|
||||
('remove_addr1','removing addr #{} from {} tracking wallet'.format(del_addrs[0],g.coin)),
|
||||
('remove_addr2','removing addr #{} from {} tracking wallet'.format(del_addrs[1],g.coin)),
|
||||
('remove_token_addr1','removing addr #{} from {} token tracking wallet'.format(del_addrs[0],g.coin)),
|
||||
('remove_token_addr2','removing addr #{} from {} token tracking wallet'.format(del_addrs[1],g.coin)),
|
||||
('token_remove_addr1','removing addr #{} from {} token tracking wallet'.format(del_addrs[0],g.coin)),
|
||||
('token_remove_addr2','removing addr #{} from {} token tracking wallet'.format(del_addrs[1],g.coin)),
|
||||
|
||||
('stop', 'stopping parity'),
|
||||
)
|
||||
|
|
@ -300,6 +334,25 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
die(1,'No parity executable found!')
|
||||
return 'ok'
|
||||
|
||||
def wallet_upgrade(self,src_file):
|
||||
if g.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())
|
||||
w_from = joinpath(src_dir,src_file)
|
||||
w_to = joinpath(dest_dir,'tracking-wallet.json')
|
||||
os.makedirs(dest_dir,mode=0o750,exist_ok=True)
|
||||
dest = shutil.copy2(w_from,w_to)
|
||||
assert dest == w_to, dest
|
||||
t = self.spawn('mmgen-tool', self.eth_args + ['twview'])
|
||||
t.read()
|
||||
os.unlink(w_to)
|
||||
return t
|
||||
|
||||
def wallet_upgrade1(self): return self.wallet_upgrade('tracking-wallet-v1.json')
|
||||
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])
|
||||
|
|
@ -315,7 +368,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
t.read()
|
||||
t.req_exit_val = 2
|
||||
return t
|
||||
if g.debug: t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
|
||||
# if g.debug: t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
|
||||
t.expect('Importing')
|
||||
t.expect(expect)
|
||||
t.read()
|
||||
|
|
@ -332,16 +385,17 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
def addrimport_burn_addr(self):
|
||||
return self.addrimport_one_addr(addr=burn_addr)
|
||||
|
||||
def txcreate(self,args=[],menu=[],acct='1',non_mmgen_inputs=0,
|
||||
def txcreate(self,args=[],menu=[],acct='1',non_mmgen_inputs=0,caller='txcreate',
|
||||
interactive_fee = '50G',
|
||||
eth_fee_res = None,
|
||||
fee_res_fs = '0.00105 {} (50 gas price in Gwei)',
|
||||
fee_desc = 'gas price' ):
|
||||
fee_desc = 'gas price',
|
||||
no_read = False):
|
||||
fee_res = fee_res_fs.format(g.coin)
|
||||
t = self.spawn('mmgen-txcreate', self.eth_args + ['-B'] + args)
|
||||
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')
|
||||
return self.txcreate_ui_common( t, menu=menu,
|
||||
t = self.txcreate_ui_common( t, menu=menu, caller=caller,
|
||||
input_sels_prompt = 'to spend from',
|
||||
inputs = acct,
|
||||
file_desc = 'Ethereum transaction',
|
||||
|
|
@ -352,6 +406,9 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
fee_desc = fee_desc,
|
||||
eth_fee_res = eth_fee_res,
|
||||
add_comment = tx_label_jp )
|
||||
if not no_read:
|
||||
t.read()
|
||||
return t
|
||||
|
||||
def txsign(self,ni=False,ext='{}.rawtx',add_args=[]):
|
||||
ext = ext.format('-α' if g.debug_utf8 else '')
|
||||
|
|
@ -359,7 +416,8 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
write_to_file(keyfile,dfl_privkey+'\n')
|
||||
txfile = self.get_file_with_ext(ext,no_dot=True)
|
||||
t = self.spawn( 'mmgen-txsign',
|
||||
self.eth_args
|
||||
['--outdir={}'.format(self.tmpdir),'--coin='+g.coin,'--quiet']
|
||||
+ ['--rpc-host=bad_host'] # ETH signing must work without RPC
|
||||
+ add_args
|
||||
+ ([],['--yes'])[ni]
|
||||
+ ['-k', keyfile, txfile, dfl_words_file] )
|
||||
|
|
@ -371,16 +429,18 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = ''
|
||||
t = self.spawn('mmgen-txsend', self.eth_args + add_args + [txfile])
|
||||
if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = '1'
|
||||
txid = self.txsend_ui_common(t,quiet=True,bogus_send=bogus_send,has_label=True)
|
||||
txid = self.txsend_ui_common(t,quiet=not g.debug,bogus_send=bogus_send,has_label=True)
|
||||
return t
|
||||
|
||||
def txcreate1(self):
|
||||
# valid_keypresses = EthereumTwUnspentOutputs.key_mappings.keys()
|
||||
menu = ['a','d','r','M','D','e','m','m'] # include one invalid keypress, 'D'
|
||||
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)
|
||||
|
||||
def txsign1(self): return self.txsign(add_args=['--use-internal-keccak-module'])
|
||||
def tx_status0(self):
|
||||
return self.tx_status(ext='{}.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 bal1(self): return self.bal(n='1')
|
||||
|
|
@ -399,17 +459,23 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
def txsend3(self): return self.txsend(ext='2.345,50000]{}.sigtx')
|
||||
def bal3(self): return self.bal(n='3')
|
||||
|
||||
def tx_status(self,ext,expect_str):
|
||||
def tx_status(self,ext,expect_str,expect_str2='',add_args=[],exit_val=0):
|
||||
ext = ext.format('-α' if g.debug_utf8 else '')
|
||||
txfile = self.get_file_with_ext(ext,no_dot=True)
|
||||
t = self.spawn('mmgen-txsend', self.eth_args + ['--status',txfile])
|
||||
t = self.spawn('mmgen-txsend', self.eth_args + add_args + ['--status',txfile])
|
||||
t.expect(expect_str)
|
||||
if expect_str2:
|
||||
t.expect(expect_str2)
|
||||
t.read()
|
||||
t.req_exit_val = exit_val
|
||||
return t
|
||||
|
||||
def tx_status1(self):
|
||||
return self.tx_status(ext='2.345,50000]{}.sigtx',expect_str='has 1 confirmation')
|
||||
|
||||
def tx_status1a(self):
|
||||
return self.tx_status(ext='2.345,50000]{}.sigtx',expect_str='has 2 confirmations')
|
||||
|
||||
def txcreate4(self):
|
||||
args = ['98831F3A:E:2,23.45495']
|
||||
interactive_fee='40G'
|
||||
|
|
@ -445,7 +511,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
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) + self.bal_corr)
|
||||
if adj and g.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()
|
||||
|
|
@ -455,7 +521,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': _amt2 = str(Decimal(_amt2) + self.bal_corr)
|
||||
if adj and g.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)
|
||||
t.read()
|
||||
|
|
@ -510,7 +576,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
imsg("Compiling solidity token contract '{}' with 'solc'".format(token_data['symbol']))
|
||||
try: os.mkdir(odir)
|
||||
except: pass
|
||||
cmd = ['scripts/create-token.py','--coin='+g.coin,'--outdir='+odir] + cmd_args + [dfl_addr_chk]
|
||||
cmd = ['scripts/traceback_run.py','scripts/create-token.py','--coin='+g.coin,'--outdir='+odir] + cmd_args + [dfl_addr_chk]
|
||||
imsg("Executing: {}".format(' '.join(cmd)))
|
||||
subprocess.check_output(cmd,stderr=subprocess.STDOUT)
|
||||
imsg("ERC20 token '{}' compiled".format(token_data['symbol']))
|
||||
|
|
@ -551,7 +617,9 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
t = self.spawn('mmgen-txsend', self.eth_args + [txfile],no_msg=True)
|
||||
|
||||
os.environ['MMGEN_BOGUS_SEND'] = '1'
|
||||
txid = self.txsend_ui_common(t,caller=mmgen_cmd,quiet=True,bogus_send=False)
|
||||
txid = self.txsend_ui_common(t,caller=mmgen_cmd,
|
||||
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 etx.get_exec_status(txid,True) != 0,(
|
||||
|
|
@ -648,12 +716,14 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
|
||||
def token_txcreate(self,args=[],token='',inputs='1',fee='50G'):
|
||||
t = self.spawn('mmgen-txcreate', self.eth_args + ['--token='+token,'-B','--tx-fee='+fee] + args)
|
||||
return self.txcreate_ui_common( t,
|
||||
t = self.txcreate_ui_common( t,
|
||||
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
|
||||
def token_txsign(self,ext='',token=''):
|
||||
return self.txsign(ni=True,ext=ext,add_args=['--token='+token])
|
||||
def token_txsend(self,ext='',token=''):
|
||||
|
|
@ -665,6 +735,14 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
return self.token_txsign(ext='1.23456,50000]{}.rawtx',token='mm1')
|
||||
def token_txsend1(self):
|
||||
return self.token_txsend(ext='1.23456,50000]{}.sigtx',token='mm1')
|
||||
|
||||
def tx_status3(self):
|
||||
return self.tx_status(
|
||||
ext='1.23456,50000]{}.sigtx',
|
||||
add_args=['--token=mm1'],
|
||||
expect_str='successfully executed',
|
||||
expect_str2='has 1 confirmation')
|
||||
|
||||
def token_bal2(self):
|
||||
return self.token_bal(n='2')
|
||||
|
||||
|
|
@ -745,6 +823,62 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
def token_listaddresses2(self):
|
||||
return self.listaddresses(args=['--token=mm1'],tool_args=['showempty=1'])
|
||||
|
||||
def twview_cached_balances(self):
|
||||
return self.twview(args=['--cached-balances'])
|
||||
def token_twview_cached_balances(self):
|
||||
return self.twview(args=['--token=mm1','--cached-balances'])
|
||||
|
||||
def txcreate_cached_balances(self):
|
||||
args = ['--tx-fee=20G','--cached-balances','98831F3A:E:3,0.1276']
|
||||
return self.txcreate(args=args,acct='2')
|
||||
def token_txcreate_cached_balances(self):
|
||||
args=['--cached-balances','--tx-fee=12G','98831F3A:E:12,1.2789']
|
||||
return self.token_txcreate(args=args,token='mm1')
|
||||
|
||||
def txdo_cached_balances(self,
|
||||
acct = '2',
|
||||
fee_res_fs = '0.00105 {} (50 gas price in Gwei)',
|
||||
add_args = ['98831F3A:E:3,0.4321']):
|
||||
args = ['--tx-fee=20G','--cached-balances'] + add_args + [dfl_words_file]
|
||||
os.environ['MMGEN_BOGUS_SEND'] = ''
|
||||
t = self.txcreate(args=args,acct=acct,caller='txdo',fee_res_fs=fee_res_fs,no_read=True)
|
||||
os.environ['MMGEN_BOGUS_SEND'] = '1'
|
||||
self._do_confirm_send(t,quiet=not g.debug,sure=False)
|
||||
t.read()
|
||||
return t
|
||||
|
||||
def txcreate_refresh_balances(self,
|
||||
bals=['2','3'],
|
||||
args=['-B','--cached-balances','-i'],
|
||||
total= '1000126.14829832312345678',adj_total=True,total_coin=g.coin):
|
||||
if g.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:
|
||||
t.expect('[R]efresh balance:\b','R')
|
||||
t.expect(' main menu): ',n)
|
||||
t.expect('Is this what you want? (y/N): ','y')
|
||||
t.expect('[R]efresh balance:\b','q')
|
||||
t.expect('Total unspent: {} {}'.format(total,total_coin))
|
||||
t.read()
|
||||
return t
|
||||
|
||||
def bal10(self): return self.bal(n='10')
|
||||
|
||||
def token_txdo_cached_balances(self):
|
||||
return self.txdo_cached_balances(
|
||||
acct='1',
|
||||
fee_res_fs='0.0026 {} (50 gas price in Gwei)',
|
||||
add_args=['--token=mm1','98831F3A:E:12,43.21'])
|
||||
|
||||
def token_txcreate_refresh_balances(self):
|
||||
return self.txcreate_refresh_balances(
|
||||
bals=['1','2'],
|
||||
args=['--token=mm1','-B','--cached-balances','-i'],
|
||||
total='1000',adj_total=False,total_coin='MM1')
|
||||
|
||||
def token_bal7(self): return self.token_bal(n='7')
|
||||
|
||||
def twview1(self):
|
||||
return self.twview()
|
||||
def twview2(self):
|
||||
|
|
@ -767,11 +901,19 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
|
||||
def edit_label(self,out_num,args=[],action='l',label_text=None):
|
||||
t = self.spawn('mmgen-txcreate', self.eth_args + args + ['-B','-i'])
|
||||
p1,p2 = ('emove address:\b','return to main menu): ')
|
||||
p1,p2 = ('efresh balance:\b','return to main menu): ')
|
||||
p3,r3 = (p2,label_text+'\n') if label_text is not None else ('(y/N): ','y')
|
||||
p4,r4 = (('(y/N): ',),('y',)) if label_text == '' else ((),())
|
||||
for p,r in zip((p1,p1,p2,p3)+p4+(p1,p1),('M',action,out_num+'\n',r3)+r4+('M','q')):
|
||||
for p,r in zip((p1,p1,p2,p3)+p4,('M',action,out_num+'\n',r3)+r4):
|
||||
t.expect(p,r)
|
||||
m = ( 'Account #{} removed' if action == 'D' else
|
||||
'Label added to account #{}' if label_text else
|
||||
'Label removed from account #{}' )
|
||||
t.expect(m.format(out_num))
|
||||
for p,r in zip((p1,p1),('M','q')):
|
||||
t.expect(p,r)
|
||||
t.expect('Total unspent:')
|
||||
t.read()
|
||||
return t
|
||||
|
||||
def edit_label1(self):
|
||||
|
|
@ -781,14 +923,17 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
def edit_label3(self):
|
||||
return self.edit_label(out_num=del_addrs[0],label_text='')
|
||||
|
||||
def token_edit_label1(self):
|
||||
return self.edit_label(out_num='1',label_text='Token label #1',args=['--token=mm1'])
|
||||
|
||||
def remove_addr1(self):
|
||||
return self.edit_label(out_num=del_addrs[0],action='R')
|
||||
return self.edit_label(out_num=del_addrs[0],action='D')
|
||||
def remove_addr2(self):
|
||||
return self.edit_label(out_num=del_addrs[1],action='R')
|
||||
def remove_token_addr1(self):
|
||||
return self.edit_label(out_num=del_addrs[0],args=['--token=mm1'],action='R')
|
||||
def remove_token_addr2(self):
|
||||
return self.edit_label(out_num=del_addrs[1],args=['--token=mm1'],action='R')
|
||||
return self.edit_label(out_num=del_addrs[1],action='D')
|
||||
def token_remove_addr1(self):
|
||||
return self.edit_label(out_num=del_addrs[0],args=['--token=mm1'],action='D')
|
||||
def token_remove_addr2(self):
|
||||
return self.edit_label(out_num=del_addrs[1],args=['--token=mm1'],action='D')
|
||||
|
||||
def stop(self):
|
||||
self.spawn('',msg_only=True)
|
||||
|
|
|
|||
|
|
@ -235,7 +235,8 @@ class TestSuiteShared(object):
|
|||
t.written_to_file('Encrypted secret keys',oo=True)
|
||||
return t
|
||||
|
||||
def _do_confirm_send(self,t,quiet=False,confirm_send=True):
|
||||
t.expect('Are you sure you want to broadcast this')
|
||||
def _do_confirm_send(self,t,quiet=False,confirm_send=True,sure=True):
|
||||
if sure:
|
||||
t.expect('Are you sure you want to broadcast this')
|
||||
m = ('YES, I REALLY WANT TO DO THIS','YES')[quiet]
|
||||
t.expect("'{}' to confirm: ".format(m),('',m)[confirm_send]+'\n')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue