whitespace: proto.eth (plus cleanup)
This commit is contained in:
parent
4a0a199e85
commit
6346c1d11a
22 changed files with 321 additions and 318 deletions
|
|
@ -31,12 +31,12 @@ class EthereumTwAddrData(TwAddrData):
|
|||
"""
|
||||
}
|
||||
|
||||
async def get_tw_data(self,twctl=None):
|
||||
async def get_tw_data(self, twctl=None):
|
||||
from ...tw.ctl import TwCtl
|
||||
self.cfg._util.vmsg('Getting address data from tracking wallet')
|
||||
twctl = (twctl or await TwCtl(self.cfg,self.proto)).mmid_ordered_dict
|
||||
twctl = (twctl or await TwCtl(self.cfg, self.proto)).mmid_ordered_dict
|
||||
# emulate the output of RPC 'listaccounts' and 'getaddressesbyaccount'
|
||||
return [(mmid+' '+d['comment'],[d['addr']]) for mmid,d in list(twctl.items())]
|
||||
return [(mmid+' '+d['comment'], [d['addr']]) for mmid, d in list(twctl.items())]
|
||||
|
||||
class EthereumTokenTwAddrData(EthereumTwAddrData):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -24,46 +24,46 @@ from decimal import Decimal
|
|||
from . import rlp
|
||||
|
||||
from . import erigon_sleep
|
||||
from ...util import msg,pp_msg,die
|
||||
from ...util import msg, pp_msg, die
|
||||
from ...base_obj import AsyncInit
|
||||
from ...obj import MMGenObject,CoinTxID
|
||||
from ...addr import CoinAddr,TokenAddr
|
||||
from ...obj import MMGenObject, CoinTxID
|
||||
from ...addr import CoinAddr, TokenAddr
|
||||
|
||||
def parse_abi(s):
|
||||
return [s[:8]] + [s[8+x*64:8+(x+1)*64] for x in range(len(s[8:])//64)]
|
||||
|
||||
class TokenCommon(MMGenObject):
|
||||
|
||||
def create_method_id(self,sig):
|
||||
def create_method_id(self, sig):
|
||||
return self.keccak_256(sig.encode()).hexdigest()[:8]
|
||||
|
||||
def transferdata2sendaddr(self,data): # online
|
||||
return CoinAddr(self.proto,parse_abi(data)[1][-40:])
|
||||
def transferdata2sendaddr(self, data): # online
|
||||
return CoinAddr(self.proto, parse_abi(data)[1][-40:])
|
||||
|
||||
def transferdata2amt(self,data): # online
|
||||
def transferdata2amt(self, data): # online
|
||||
return self.proto.coin_amt(
|
||||
int(parse_abi(data)[-1], 16) * self.base_unit,
|
||||
from_decimal = True)
|
||||
|
||||
async def do_call(self,method_sig,method_args='',toUnit=False):
|
||||
async def do_call(self, method_sig, method_args='', toUnit=False):
|
||||
data = self.create_method_id(method_sig) + method_args
|
||||
if self.cfg.debug:
|
||||
msg('ETH_CALL {}: {}'.format(
|
||||
method_sig,
|
||||
'\n '.join(parse_abi(data)) ))
|
||||
ret = await self.rpc.call('eth_call',{ 'to': '0x'+self.addr, 'data': '0x'+data },'pending')
|
||||
'\n '.join(parse_abi(data))))
|
||||
ret = await self.rpc.call('eth_call', {'to': '0x'+self.addr, 'data': '0x'+data}, 'pending')
|
||||
await erigon_sleep(self)
|
||||
if toUnit:
|
||||
return int(ret,16) * self.base_unit
|
||||
return int(ret, 16) * self.base_unit
|
||||
else:
|
||||
return ret
|
||||
|
||||
async def get_balance(self,acct_addr):
|
||||
async def get_balance(self, acct_addr):
|
||||
return self.proto.coin_amt(
|
||||
await self.do_call('balanceOf(address)', acct_addr.rjust(64, '0'), toUnit=True),
|
||||
from_decimal = True)
|
||||
|
||||
def strip(self,s):
|
||||
def strip(self, s):
|
||||
return ''.join([chr(b) for b in s if 32 <= b <= 127]).strip()
|
||||
|
||||
async def get_name(self):
|
||||
|
|
@ -76,13 +76,13 @@ class TokenCommon(MMGenObject):
|
|||
ret = await self.do_call('decimals()')
|
||||
try:
|
||||
assert ret[:2] == '0x'
|
||||
return int(ret,16)
|
||||
return int(ret, 16)
|
||||
except:
|
||||
msg(f'RPC call to decimals() failed (returned {ret!r})')
|
||||
return None
|
||||
|
||||
async def get_total_supply(self):
|
||||
return await self.do_call('totalSupply()',toUnit=True)
|
||||
return await self.do_call('totalSupply()', toUnit=True)
|
||||
|
||||
async def info(self):
|
||||
return ('{:15}{}\n' * 5).format(
|
||||
|
|
@ -90,10 +90,10 @@ class TokenCommon(MMGenObject):
|
|||
'token symbol:', await self.get_symbol(),
|
||||
'token name:', await self.get_name(),
|
||||
'decimals:', self.decimals,
|
||||
'total supply:', await self.get_total_supply() )
|
||||
'total supply:', await self.get_total_supply())
|
||||
|
||||
async def code(self):
|
||||
return (await self.rpc.call('eth_getCode','0x'+self.addr))[2:]
|
||||
return (await self.rpc.call('eth_getCode', '0x'+self.addr))[2:]
|
||||
|
||||
def create_data(
|
||||
self,
|
||||
|
|
@ -101,8 +101,8 @@ class TokenCommon(MMGenObject):
|
|||
amt,
|
||||
method_sig = 'transfer(address,uint256)'):
|
||||
from_arg = ''
|
||||
to_arg = to_addr.rjust(64,'0')
|
||||
amt_arg = '{:064x}'.format( int(amt / self.base_unit) )
|
||||
to_arg = to_addr.rjust(64, '0')
|
||||
amt_arg = '{:064x}'.format(int(amt / self.base_unit))
|
||||
return self.create_method_id(method_sig) + from_arg + to_arg + amt_arg
|
||||
|
||||
def make_tx_in(
|
||||
|
|
@ -125,24 +125,24 @@ class TokenCommon(MMGenObject):
|
|||
'nonce': nonce,
|
||||
'data': bytes.fromhex(data)}
|
||||
|
||||
async def txsign(self,tx_in,key,from_addr,chain_id=None):
|
||||
async def txsign(self, tx_in, key, from_addr, chain_id=None):
|
||||
|
||||
from .pyethereum.transactions import Transaction
|
||||
|
||||
if chain_id is None:
|
||||
res = await self.rpc.call('eth_chainId')
|
||||
chain_id = None if res is None else int(res,16)
|
||||
chain_id = None if res is None else int(res, 16)
|
||||
|
||||
tx = Transaction(**tx_in).sign(key,chain_id)
|
||||
tx = Transaction(**tx_in).sign(key, chain_id)
|
||||
|
||||
if tx.sender.hex() != from_addr:
|
||||
die(3,f'Sender address {from_addr!r} does not match address of key {tx.sender.hex()!r}!')
|
||||
die(3, f'Sender address {from_addr!r} does not match address of key {tx.sender.hex()!r}!')
|
||||
|
||||
if self.cfg.debug:
|
||||
msg('TOKEN DATA:')
|
||||
pp_msg(tx.to_dict())
|
||||
msg('PARSED ABI DATA:\n {}'.format(
|
||||
'\n '.join(parse_abi(tx.data.hex())) ))
|
||||
'\n '.join(parse_abi(tx.data.hex()))))
|
||||
|
||||
return (
|
||||
rlp.encode(tx).hex(),
|
||||
|
|
@ -151,8 +151,8 @@ class TokenCommon(MMGenObject):
|
|||
|
||||
# The following are used for token deployment only:
|
||||
|
||||
async def txsend(self,txhex):
|
||||
return (await self.rpc.call('eth_sendRawTransaction','0x'+txhex)).replace('0x','',1)
|
||||
async def txsend(self, txhex):
|
||||
return (await self.rpc.call('eth_sendRawTransaction', '0x'+txhex)).replace('0x', '', 1)
|
||||
|
||||
async def transfer(
|
||||
self,
|
||||
|
|
@ -168,35 +168,35 @@ class TokenCommon(MMGenObject):
|
|||
amt,
|
||||
start_gas,
|
||||
gasPrice,
|
||||
nonce = int(await self.rpc.call('eth_getTransactionCount','0x'+from_addr,'pending'),16),
|
||||
nonce = int(await self.rpc.call('eth_getTransactionCount', '0x'+from_addr, 'pending'), 16),
|
||||
method_sig = method_sig)
|
||||
txhex,_ = await self.txsign(tx_in,key,from_addr)
|
||||
txhex, _ = await self.txsign(tx_in, key, from_addr)
|
||||
return await self.txsend(txhex)
|
||||
|
||||
class Token(TokenCommon):
|
||||
|
||||
def __init__(self,cfg,proto,addr,decimals,rpc=None):
|
||||
def __init__(self, cfg, proto, addr, decimals, rpc=None):
|
||||
if type(self).__name__ == 'Token':
|
||||
from ...util2 import get_keccak
|
||||
self.keccak_256 = get_keccak(cfg)
|
||||
self.cfg = cfg
|
||||
self.proto = proto
|
||||
self.addr = TokenAddr(proto,addr)
|
||||
assert isinstance(decimals,int),f'decimals param must be int instance, not {type(decimals)}'
|
||||
self.addr = TokenAddr(proto, addr)
|
||||
assert isinstance(decimals, int), f'decimals param must be int instance, not {type(decimals)}'
|
||||
self.decimals = decimals
|
||||
self.base_unit = Decimal('10') ** -self.decimals
|
||||
self.rpc = rpc
|
||||
|
||||
class ResolvedToken(TokenCommon,metaclass=AsyncInit):
|
||||
class ResolvedToken(TokenCommon, metaclass=AsyncInit):
|
||||
|
||||
async def __init__(self,cfg,proto,rpc,addr):
|
||||
async def __init__(self, cfg, proto, rpc, addr):
|
||||
from ...util2 import get_keccak
|
||||
self.keccak_256 = get_keccak(cfg)
|
||||
self.cfg = cfg
|
||||
self.proto = proto
|
||||
self.rpc = rpc
|
||||
self.addr = TokenAddr(proto,addr)
|
||||
self.addr = TokenAddr(proto, addr)
|
||||
decimals = await self.get_decimals() # requires self.addr!
|
||||
if not decimals:
|
||||
die( 'TokenNotInBlockchain', f'Token {addr!r} not in blockchain' )
|
||||
Token.__init__(self,cfg,proto,addr,decimals,rpc)
|
||||
die('TokenNotInBlockchain', f'Token {addr!r} not in blockchain')
|
||||
Token.__init__(self, cfg, proto, addr, decimals, rpc)
|
||||
|
|
|
|||
|
|
@ -15,29 +15,29 @@ proto.eth.daemon: Ethereum base protocol daemon classes
|
|||
import os
|
||||
|
||||
from ...cfg import gc
|
||||
from ...util import list_gen,get_subclasses
|
||||
from ...daemon import CoinDaemon,RPCDaemon,_nw,_dd
|
||||
from ...util import list_gen, get_subclasses
|
||||
from ...daemon import CoinDaemon, RPCDaemon, _nw, _dd
|
||||
|
||||
class ethereum_daemon(CoinDaemon):
|
||||
chain_subdirs = _nw('ethereum','goerli','DevelopmentChain')
|
||||
chain_subdirs = _nw('ethereum', 'goerli', 'DevelopmentChain')
|
||||
base_rpc_port = 8545 # same for all networks!
|
||||
base_authrpc_port = 8551 # same for all networks!
|
||||
base_p2p_port = 30303 # same for all networks!
|
||||
daemon_port_offset = 100
|
||||
network_port_offsets = _nw(0,10,20)
|
||||
network_port_offsets = _nw(0, 10, 20)
|
||||
|
||||
def __init__(self,*args,test_suite=False,**kwargs):
|
||||
def __init__(self, *args, test_suite=False, **kwargs):
|
||||
|
||||
if not hasattr(self,'all_daemons'):
|
||||
ethereum_daemon.all_daemons = get_subclasses(ethereum_daemon,names=True)
|
||||
if not hasattr(self, 'all_daemons'):
|
||||
ethereum_daemon.all_daemons = get_subclasses(ethereum_daemon, names=True)
|
||||
|
||||
daemon_idx_offset = (
|
||||
self.all_daemons.index(self.id+'_daemon') * self.daemon_port_offset
|
||||
if test_suite else 0 )
|
||||
if test_suite else 0)
|
||||
|
||||
self.port_offset = daemon_idx_offset + getattr(self.network_port_offsets,self.network)
|
||||
self.port_offset = daemon_idx_offset + getattr(self.network_port_offsets, self.network)
|
||||
|
||||
super().__init__( *args, test_suite=test_suite, **kwargs )
|
||||
super().__init__(*args, test_suite=test_suite, **kwargs)
|
||||
|
||||
def get_rpc_port(self):
|
||||
return self.base_rpc_port + self.port_offset
|
||||
|
|
@ -54,7 +54,7 @@ class ethereum_daemon(CoinDaemon):
|
|||
return os.path.join(
|
||||
self.logdir,
|
||||
self.id,
|
||||
getattr(self.chain_subdirs,self.network) )
|
||||
getattr(self.chain_subdirs, self.network))
|
||||
|
||||
class openethereum_daemon(ethereum_daemon):
|
||||
daemon_data = _dd('OpenEthereum', 3003005, '3.3.5')
|
||||
|
|
@ -62,9 +62,9 @@ class openethereum_daemon(ethereum_daemon):
|
|||
exec_fn = 'openethereum'
|
||||
cfg_file = 'parity.conf'
|
||||
datadirs = {
|
||||
'linux': [gc.home_dir,'.local','share','io.parity.ethereum'],
|
||||
'linux': [gc.home_dir, '.local', 'share', 'io.parity.ethereum'],
|
||||
'darwin': [gc.home_dir, 'Library', 'Application Support', 'io.parity.ethereum'],
|
||||
'win32': [os.getenv('LOCALAPPDATA'),'Parity','Ethereum']
|
||||
'win32': [os.getenv('LOCALAPPDATA'), 'Parity', 'Ethereum']
|
||||
}
|
||||
|
||||
def init_subclass(self):
|
||||
|
|
@ -102,20 +102,20 @@ class geth_daemon(ethereum_daemon):
|
|||
exec_fn = 'geth'
|
||||
use_pidfile = False
|
||||
use_threads = True
|
||||
avail_opts = ('no_daemonize','online')
|
||||
avail_opts = ('no_daemonize', 'online')
|
||||
version_info_arg = 'version'
|
||||
datadirs = {
|
||||
'linux': [gc.home_dir,'.ethereum','geth'],
|
||||
'linux': [gc.home_dir, '.ethereum', 'geth'],
|
||||
'darwin': [gc.home_dir, 'Library', 'Ethereum', 'geth'],
|
||||
'win32': [os.getenv('LOCALAPPDATA'),'Geth'] # FIXME
|
||||
'win32': [os.getenv('LOCALAPPDATA'), 'Geth'] # FIXME
|
||||
}
|
||||
|
||||
def init_subclass(self):
|
||||
|
||||
def have_authrpc():
|
||||
from subprocess import run,PIPE
|
||||
from subprocess import run, PIPE
|
||||
try:
|
||||
return b'authrpc' in run(['geth','help'],check=True,stdout=PIPE).stdout
|
||||
return b'authrpc' in run(['geth', 'help'], check=True, stdout=PIPE).stdout
|
||||
except:
|
||||
return False
|
||||
|
||||
|
|
@ -138,12 +138,12 @@ class erigon_daemon(geth_daemon):
|
|||
daemon_data = _dd('Erigon', 2022099099, '2022.99.99')
|
||||
version_pat = r'erigon/(\d+)\.(\d+)\.(\d+)'
|
||||
exec_fn = 'erigon'
|
||||
private_ports = _nw(9090,9091,9092) # testnet and regtest are non-standard
|
||||
torrent_ports = _nw(42069,42070,None) # testnet is non-standard
|
||||
private_ports = _nw(9090, 9091, 9092) # testnet and regtest are non-standard
|
||||
torrent_ports = _nw(42069, 42070, None) # testnet is non-standard
|
||||
version_info_arg = '--version'
|
||||
datadirs = {
|
||||
'linux': [gc.home_dir,'.local','share','erigon'],
|
||||
'win32': [os.getenv('LOCALAPPDATA'),'Erigon'] # FIXME
|
||||
'linux': [gc.home_dir, '.local', 'share', 'erigon'],
|
||||
'win32': [os.getenv('LOCALAPPDATA'), 'Erigon'] # FIXME
|
||||
}
|
||||
|
||||
def init_subclass(self):
|
||||
|
|
@ -169,21 +169,21 @@ class erigon_daemon(geth_daemon):
|
|||
rpc_port = self.rpc_port,
|
||||
private_port = self.private_port,
|
||||
test_suite = self.test_suite,
|
||||
datadir = self.datadir )
|
||||
datadir = self.datadir)
|
||||
|
||||
def start(self,quiet=False,silent=False):
|
||||
super().start(quiet=quiet,silent=silent)
|
||||
def start(self, quiet=False, silent=False):
|
||||
super().start(quiet=quiet, silent=silent)
|
||||
self.rpc_d.debug = self.debug
|
||||
return self.rpc_d.start(quiet=quiet,silent=silent)
|
||||
return self.rpc_d.start(quiet=quiet, silent=silent)
|
||||
|
||||
def stop(self,quiet=False,silent=False):
|
||||
def stop(self, quiet=False, silent=False):
|
||||
self.rpc_d.debug = self.debug
|
||||
self.rpc_d.stop(quiet=quiet,silent=silent)
|
||||
return super().stop(quiet=quiet,silent=silent)
|
||||
self.rpc_d.stop(quiet=quiet, silent=silent)
|
||||
return super().stop(quiet=quiet, silent=silent)
|
||||
|
||||
@property
|
||||
def start_cmds(self):
|
||||
return [self.start_cmd,self.rpc_d.start_cmd]
|
||||
return [self.start_cmd, self.rpc_d.start_cmd]
|
||||
|
||||
class erigon_rpcdaemon(RPCDaemon):
|
||||
|
||||
|
|
@ -193,7 +193,7 @@ class erigon_rpcdaemon(RPCDaemon):
|
|||
use_pidfile = False
|
||||
use_threads = True
|
||||
|
||||
def __init__(self,cfg,proto,rpc_port,private_port,test_suite,datadir):
|
||||
def __init__(self, cfg, proto, rpc_port, private_port, test_suite, datadir):
|
||||
|
||||
self.proto = proto
|
||||
self.test_suite = test_suite
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ proto.eth.misc: miscellaneous utilities for Ethereum base protocol
|
|||
|
||||
from ...util2 import get_keccak
|
||||
|
||||
def decrypt_geth_keystore(cfg,wallet_fn,passwd,check_addr=True):
|
||||
def decrypt_geth_keystore(cfg, wallet_fn, passwd, check_addr=True):
|
||||
"""
|
||||
Decrypt the encrypted private key in a Geth keystore wallet, returning the decrypted key
|
||||
"""
|
||||
|
|
@ -33,32 +33,32 @@ def decrypt_geth_keystore(cfg,wallet_fn,passwd,check_addr=True):
|
|||
if check_addr:
|
||||
from ...tool.coin import tool_cmd
|
||||
from ...protocol import init_proto
|
||||
t = tool_cmd( cfg=cfg, proto=init_proto(cfg,'eth') )
|
||||
t = tool_cmd(cfg=cfg, proto=init_proto(cfg, 'eth'))
|
||||
addr = t.wif2addr(key.hex())
|
||||
addr_chk = wallet_data['address']
|
||||
assert addr == addr_chk, f'incorrect address: ({addr} != {addr_chk})'
|
||||
|
||||
return key
|
||||
|
||||
def hash_message(cfg,message,msghash_type):
|
||||
def hash_message(cfg, message, msghash_type):
|
||||
return get_keccak(cfg)(
|
||||
{
|
||||
'raw': message,
|
||||
'eth_sign': '\x19Ethereum Signed Message:\n{}{}'.format( len(message), message ),
|
||||
'eth_sign': '\x19Ethereum Signed Message:\n{}{}'.format(len(message), message),
|
||||
}[msghash_type].encode()
|
||||
).digest()
|
||||
|
||||
def ec_sign_message_with_privkey(cfg,message,key,msghash_type):
|
||||
def ec_sign_message_with_privkey(cfg, message, key, msghash_type):
|
||||
"""
|
||||
Sign an arbitrary string with an Ethereum private key, returning the signature
|
||||
|
||||
Conforms to the standard defined by the Geth `eth_sign` JSON-RPC call
|
||||
"""
|
||||
from py_ecc.secp256k1 import ecdsa_raw_sign
|
||||
v,r,s = ecdsa_raw_sign( hash_message(cfg,message,msghash_type), key )
|
||||
return '{:064x}{:064x}{:02x}'.format(r,s,v)
|
||||
v, r, s = ecdsa_raw_sign(hash_message(cfg, message, msghash_type), key)
|
||||
return '{:064x}{:064x}{:02x}'.format(r, s, v)
|
||||
|
||||
def ec_recover_pubkey(cfg,message,sig,msghash_type):
|
||||
def ec_recover_pubkey(cfg, message, sig, msghash_type):
|
||||
"""
|
||||
Given a message and signature, recover the public key associated with the private key
|
||||
used to make the signature
|
||||
|
|
@ -66,7 +66,8 @@ def ec_recover_pubkey(cfg,message,sig,msghash_type):
|
|||
Conforms to the standard defined by the Geth `eth_sign` JSON-RPC call
|
||||
"""
|
||||
from py_ecc.secp256k1 import ecdsa_raw_recover
|
||||
r,s,v = ( sig[:64], sig[64:128], sig[128:] )
|
||||
r, s, v = (sig[:64], sig[64:128], sig[128:])
|
||||
return '{:064x}{:064x}'.format(
|
||||
*ecdsa_raw_recover( hash_message(cfg,message,msghash_type), tuple(int(hexstr,16) for hexstr in (v,r,s)) )
|
||||
*ecdsa_raw_recover(
|
||||
hash_message(cfg, message, msghash_type), tuple(int(hexstr, 16) for hexstr in (v, r, s)))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,23 +18,23 @@ class coin_msg(coin_msg):
|
|||
|
||||
include_pubhash = False
|
||||
sigdata_pfx = '0x'
|
||||
msghash_types = ('eth_sign','raw') # first-listed is the default
|
||||
msghash_types = ('eth_sign', 'raw') # first-listed is the default
|
||||
|
||||
class unsigned(coin_msg.unsigned):
|
||||
|
||||
async def do_sign(self,wif,message,msghash_type):
|
||||
async def do_sign(self, wif, message, msghash_type):
|
||||
from .misc import ec_sign_message_with_privkey
|
||||
return ec_sign_message_with_privkey( self.cfg, message, bytes.fromhex(wif), msghash_type )
|
||||
return ec_sign_message_with_privkey(self.cfg, message, bytes.fromhex(wif), msghash_type)
|
||||
|
||||
class signed_online(coin_msg.signed_online):
|
||||
|
||||
async def do_verify(self,addr,sig,message,msghash_type):
|
||||
async def do_verify(self, addr, sig, message, msghash_type):
|
||||
from ...tool.coin import tool_cmd
|
||||
from .misc import ec_recover_pubkey
|
||||
return tool_cmd(
|
||||
self.cfg,
|
||||
proto = self.proto).pubhex2addr(
|
||||
ec_recover_pubkey( self.cfg, message, sig, msghash_type )) == addr
|
||||
ec_recover_pubkey(self.cfg, message, sig, msghash_type)) == addr
|
||||
|
||||
class exported_sigs(coin_msg.exported_sigs,signed_online):
|
||||
class exported_sigs(coin_msg.exported_sigs, signed_online):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@
|
|||
proto.eth.params: Ethereum protocol
|
||||
"""
|
||||
|
||||
from ...protocol import CoinProtocol,_nw,decoded_addr
|
||||
from ...protocol import CoinProtocol, _nw, decoded_addr
|
||||
from ...addr import CoinAddr
|
||||
from ...util import is_hex_str_lc,Msg
|
||||
from ...util import is_hex_str_lc, Msg
|
||||
|
||||
class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1):
|
||||
class mainnet(CoinProtocol.DummyWIF, CoinProtocol.Secp256k1):
|
||||
|
||||
network_names = _nw('mainnet','testnet','devnet')
|
||||
network_names = _nw('mainnet', 'testnet', 'devnet')
|
||||
addr_len = 20
|
||||
mmtypes = ('E',)
|
||||
dfl_mmtype = 'E'
|
||||
|
|
@ -27,7 +27,7 @@ class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1):
|
|||
|
||||
coin_amt = 'ETHAmt'
|
||||
max_tx_fee = '0.005'
|
||||
chain_names = ['ethereum','foundation']
|
||||
chain_names = ['ethereum', 'foundation']
|
||||
sign_mode = 'standalone'
|
||||
caps = ('token',)
|
||||
mmcaps = ('rpc', 'rpc_init', 'tw', 'msg')
|
||||
|
|
@ -63,9 +63,9 @@ class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1):
|
|||
Msg(f'Invalid address: {addr}')
|
||||
return False
|
||||
|
||||
def checksummed_addr(self,addr):
|
||||
def checksummed_addr(self, addr):
|
||||
h = self.keccak_256(addr.encode()).digest().hex()
|
||||
return ''.join(addr[i].upper() if int(h[i],16) > 7 else addr[i] for i in range(len(addr)))
|
||||
return ''.join(addr[i].upper() if int(h[i], 16) > 7 else addr[i] for i in range(len(addr)))
|
||||
|
||||
def pubhash2addr(self, pubhash, addr_type):
|
||||
assert len(pubhash) == 20, f'{len(pubhash)}: invalid length for {self.name} pubkey hash'
|
||||
|
|
@ -74,7 +74,7 @@ class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1):
|
|||
return CoinAddr(self, pubhash.hex())
|
||||
|
||||
class testnet(mainnet):
|
||||
chain_names = ['kovan','goerli','rinkeby']
|
||||
chain_names = ['kovan', 'goerli', 'rinkeby']
|
||||
|
||||
class regtest(testnet):
|
||||
chain_names = ['developmentchain']
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import re
|
|||
|
||||
from ...base_obj import AsyncInit
|
||||
from ...obj import Int
|
||||
from ...util import die,fmt,oneshot_warning_group
|
||||
from ...util import die, fmt, oneshot_warning_group
|
||||
from ...rpc import RPCClient
|
||||
|
||||
class daemon_warning(oneshot_warning_group):
|
||||
|
|
@ -32,7 +32,7 @@ class daemon_warning(oneshot_warning_group):
|
|||
class CallSigs:
|
||||
pass
|
||||
|
||||
class EthereumRPCClient(RPCClient,metaclass=AsyncInit):
|
||||
class EthereumRPCClient(RPCClient, metaclass=AsyncInit):
|
||||
|
||||
async def __init__(
|
||||
self,
|
||||
|
|
@ -44,50 +44,50 @@ class EthereumRPCClient(RPCClient,metaclass=AsyncInit):
|
|||
|
||||
self.proto = proto
|
||||
self.daemon = daemon
|
||||
self.call_sigs = getattr(CallSigs,daemon.id,None)
|
||||
self.call_sigs = getattr(CallSigs, daemon.id, None)
|
||||
|
||||
super().__init__(
|
||||
cfg = cfg,
|
||||
host = 'localhost' if cfg.test_suite else (cfg.rpc_host or 'localhost'),
|
||||
port = daemon.rpc_port )
|
||||
port = daemon.rpc_port)
|
||||
|
||||
await self.set_backend_async(backend)
|
||||
|
||||
vi,bh,ci = await self.gathered_call(None, (
|
||||
('web3_clientVersion',()),
|
||||
('eth_getBlockByNumber',('latest',False)),
|
||||
('eth_chainId',()),
|
||||
vi, bh, ci = await self.gathered_call(None, (
|
||||
('web3_clientVersion', ()),
|
||||
('eth_getBlockByNumber', ('latest', False)),
|
||||
('eth_chainId', ()),
|
||||
))
|
||||
|
||||
vip = re.match(self.daemon.version_pat,vi,re.ASCII)
|
||||
vip = re.match(self.daemon.version_pat, vi, re.ASCII)
|
||||
if not vip:
|
||||
die(2,fmt(f"""
|
||||
die(2, fmt(f"""
|
||||
Aborting on daemon mismatch:
|
||||
Requested daemon: {self.daemon.id}
|
||||
Running daemon: {vi}
|
||||
""",strip_char='\t').rstrip())
|
||||
""", strip_char='\t').rstrip())
|
||||
|
||||
self.daemon_version = int('{:d}{:03d}{:03d}'.format(*[int(e) for e in vip.groups()]))
|
||||
self.daemon_version_str = '{}.{}.{}'.format(*vip.groups())
|
||||
self.daemon_version_info = vi
|
||||
|
||||
self.blockcount = int(bh['number'],16)
|
||||
self.cur_date = int(bh['timestamp'],16)
|
||||
self.blockcount = int(bh['number'], 16)
|
||||
self.cur_date = int(bh['timestamp'], 16)
|
||||
|
||||
self.caps = ()
|
||||
if self.daemon.id in ('parity','openethereum'):
|
||||
if self.daemon.id in ('parity', 'openethereum'):
|
||||
if (await self.call('parity_nodeKind'))['capability'] == 'full':
|
||||
self.caps += ('full_node',)
|
||||
self.chainID = None if ci is None else Int(ci,16) # parity/oe return chainID only for dev chain
|
||||
self.chain = (await self.call('parity_chain')).replace(' ','_').replace('_testnet','')
|
||||
elif self.daemon.id in ('geth','erigon'):
|
||||
self.chainID = None if ci is None else Int(ci, 16) # parity/oe return chainID only for dev chain
|
||||
self.chain = (await self.call('parity_chain')).replace(' ', '_').replace('_testnet', '')
|
||||
elif self.daemon.id in ('geth', 'erigon'):
|
||||
if self.daemon.network == 'mainnet':
|
||||
daemon_warning(self.daemon.id)
|
||||
self.caps += ('full_node',)
|
||||
self.chainID = Int(ci,16)
|
||||
self.chainID = Int(ci, 16)
|
||||
self.chain = self.proto.chain_ids[self.chainID]
|
||||
|
||||
def make_host_path(self,wallet):
|
||||
def make_host_path(self, wallet):
|
||||
return ''
|
||||
|
||||
rpcmethods = (
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from ....tw.addresses import TwAddresses
|
|||
from .view import EthereumTwView
|
||||
from .rpc import EthereumTwRPC
|
||||
|
||||
class EthereumTwAddresses(TwAddresses,EthereumTwView,EthereumTwRPC):
|
||||
class EthereumTwAddresses(TwAddresses, EthereumTwView, EthereumTwRPC):
|
||||
|
||||
has_age = False
|
||||
prompt_fs_in = [
|
||||
|
|
@ -35,13 +35,13 @@ class EthereumTwAddresses(TwAddresses,EthereumTwView,EthereumTwRPC):
|
|||
'D':'i_addr_delete',
|
||||
'v':'a_view',
|
||||
'w':'a_view_detail',
|
||||
'p':'a_print_detail' }
|
||||
'p':'a_print_detail'}
|
||||
|
||||
def get_column_widths(self,data,wide,interactive):
|
||||
def get_column_widths(self, data, wide, interactive):
|
||||
|
||||
return self.compute_column_widths(
|
||||
widths = { # fixed cols
|
||||
'num': max(2,len(str(len(data)))+1),
|
||||
'num': max(2, len(str(len(data)))+1),
|
||||
'mmid': max(len(d.twmmid.disp) for d in data),
|
||||
'used': 0,
|
||||
'amt': self.amt_widths['amt'],
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@ from ....tw.bal import TwGetBalance
|
|||
|
||||
class EthereumTwGetBalance(TwGetBalance):
|
||||
|
||||
start_labels = ('TOTAL','Non-MMGen')
|
||||
start_labels = ('TOTAL', 'Non-MMGen')
|
||||
conf_cols = {
|
||||
'ge_minconf': 'Balance',
|
||||
}
|
||||
|
||||
async def __init__(self,cfg,proto,*args,**kwargs):
|
||||
self.twctl = await TwCtl(cfg,proto,mode='w')
|
||||
await super().__init__(cfg,proto,*args,**kwargs)
|
||||
async def __init__(self, cfg, proto, *args, **kwargs):
|
||||
self.twctl = await TwCtl(cfg, proto, mode='w')
|
||||
await super().__init__(cfg, proto, *args, **kwargs)
|
||||
|
||||
async def create_data(self):
|
||||
in_data = self.twctl.mmid_ordered_dict
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@
|
|||
proto.eth.tw.ctl: Ethereum tracking wallet control class
|
||||
"""
|
||||
|
||||
from ....util import msg,ymsg,die
|
||||
from ....util import msg, ymsg, die
|
||||
from ....tw.ctl import TwCtl, write_mode, label_addr_pair
|
||||
from ....tw.shared import TwLabel
|
||||
from ....addr import is_coin_addr,is_mmgen_id,CoinAddr
|
||||
from ..contract import Token,ResolvedToken
|
||||
from ....addr import is_coin_addr, is_mmgen_id, CoinAddr
|
||||
from ..contract import Token, ResolvedToken
|
||||
|
||||
class EthereumTwCtl(TwCtl):
|
||||
|
||||
|
|
@ -81,43 +81,43 @@ class EthereumTwCtl(TwCtl):
|
|||
self.force_write()
|
||||
msg(f'{self.desc} upgraded successfully!')
|
||||
|
||||
async def rpc_get_balance(self,addr):
|
||||
async def rpc_get_balance(self, addr):
|
||||
return self.proto.coin_amt(
|
||||
int(await self.rpc.call('eth_getBalance', '0x' + addr, 'latest'), 16),
|
||||
from_unit = 'wei')
|
||||
|
||||
@write_mode
|
||||
async def batch_import_address(self,args_list):
|
||||
async def batch_import_address(self, args_list):
|
||||
return [await self.import_address(*a) for a in args_list]
|
||||
|
||||
async def rescan_addresses(self,coin_addrs):
|
||||
async def rescan_addresses(self, coin_addrs):
|
||||
pass
|
||||
|
||||
@write_mode
|
||||
async def import_address(self,addr,label,rescan=False):
|
||||
async def import_address(self, addr, label, rescan=False):
|
||||
r = self.data_root
|
||||
if addr in r:
|
||||
if not r[addr]['mmid'] and label.mmid:
|
||||
msg(f'Warning: MMGen ID {label.mmid!r} was missing in tracking wallet!')
|
||||
elif r[addr]['mmid'] != label.mmid:
|
||||
die(3,'MMGen ID {label.mmid!r} does not match tracking wallet!')
|
||||
r[addr] = { 'mmid': label.mmid, 'comment': label.comment }
|
||||
die(3, 'MMGen ID {label.mmid!r} does not match tracking wallet!')
|
||||
r[addr] = {'mmid': label.mmid, 'comment': label.comment}
|
||||
|
||||
@write_mode
|
||||
async def remove_address(self,addr):
|
||||
async def remove_address(self, addr):
|
||||
r = self.data_root
|
||||
|
||||
if is_coin_addr(self.proto,addr):
|
||||
if is_coin_addr(self.proto, addr):
|
||||
have_match = lambda k: k == addr
|
||||
elif is_mmgen_id(self.proto,addr):
|
||||
elif is_mmgen_id(self.proto, addr):
|
||||
have_match = lambda k: r[k]['mmid'] == addr
|
||||
else:
|
||||
die(1,f'{addr!r} is not an Ethereum address or MMGen ID')
|
||||
die(1, f'{addr!r} is not an Ethereum address or MMGen ID')
|
||||
|
||||
for k in r:
|
||||
if have_match(k):
|
||||
# return the addr resolved to mmid if possible
|
||||
ret = r[k]['mmid'] if is_mmgen_id(self.proto,r[k]['mmid']) else addr
|
||||
ret = r[k]['mmid'] if is_mmgen_id(self.proto, r[k]['mmid']) else addr
|
||||
del r[k]
|
||||
self.write()
|
||||
return ret
|
||||
|
|
@ -125,8 +125,8 @@ class EthereumTwCtl(TwCtl):
|
|||
return None
|
||||
|
||||
@write_mode
|
||||
async def set_label(self,coinaddr,lbl):
|
||||
for addr,d in list(self.data_root.items()):
|
||||
async def set_label(self, coinaddr, lbl):
|
||||
for addr, d in list(self.data_root.items()):
|
||||
if addr == coinaddr:
|
||||
d['comment'] = lbl.comment
|
||||
self.write()
|
||||
|
|
@ -134,32 +134,32 @@ class EthereumTwCtl(TwCtl):
|
|||
msg(f'Address {coinaddr!r} not found in {self.data_root_desc!r} section of tracking wallet')
|
||||
return False
|
||||
|
||||
async def addr2sym(self,req_addr):
|
||||
async def addr2sym(self, req_addr):
|
||||
for addr in self.data['tokens']:
|
||||
if addr == req_addr:
|
||||
return self.data['tokens'][addr]['params']['symbol']
|
||||
|
||||
async def sym2addr(self,sym):
|
||||
async def sym2addr(self, sym):
|
||||
for addr in self.data['tokens']:
|
||||
if self.data['tokens'][addr]['params']['symbol'] == sym.upper():
|
||||
return addr
|
||||
|
||||
def get_token_param(self,token,param):
|
||||
def get_token_param(self, token, param):
|
||||
if token in self.data['tokens']:
|
||||
return self.data['tokens'][token]['params'].get(param)
|
||||
|
||||
@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'] )
|
||||
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):
|
||||
return dict((x['mmid'],{'addr':x['addr'],'comment':x['comment']}) for x in self.sorted_list)
|
||||
return dict((x['mmid'], {'addr': x['addr'], 'comment': x['comment']}) for x in self.sorted_list)
|
||||
|
||||
async def get_label_addr_pairs(self):
|
||||
return [label_addr_pair(
|
||||
|
|
@ -182,22 +182,22 @@ class EthereumTokenTwCtl(EthereumTwCtl):
|
|||
self.conv_types(v)
|
||||
|
||||
if self.importing and token_addr:
|
||||
if not is_coin_addr(proto,token_addr):
|
||||
die( 'InvalidTokenAddress', f'{token_addr!r}: invalid token address' )
|
||||
if not is_coin_addr(proto, token_addr):
|
||||
die('InvalidTokenAddress', f'{token_addr!r}: invalid token address')
|
||||
else:
|
||||
assert token_addr is None,'EthereumTokenTwCtl_chk1'
|
||||
assert token_addr is None, 'EthereumTokenTwCtl_chk1'
|
||||
token_addr = await self.sym2addr(proto.tokensym) # returns None on failure
|
||||
if not is_coin_addr(proto,token_addr):
|
||||
die( 'UnrecognizedTokenSymbol', f'Specified token {proto.tokensym!r} could not be resolved!' )
|
||||
if not is_coin_addr(proto, token_addr):
|
||||
die('UnrecognizedTokenSymbol', f'Specified token {proto.tokensym!r} could not be resolved!')
|
||||
|
||||
from ....addr import TokenAddr
|
||||
self.token = TokenAddr(proto,token_addr)
|
||||
self.token = TokenAddr(proto, token_addr)
|
||||
|
||||
if self.token not in self.data['tokens']:
|
||||
if self.importing:
|
||||
await self.import_token(self.token)
|
||||
else:
|
||||
die( 'TokenNotInWallet', f'Specified token {self.token!r} not in wallet!' )
|
||||
die('TokenNotInWallet', f'Specified token {self.token!r} not in wallet!')
|
||||
|
||||
self.decimals = self.get_param('decimals')
|
||||
self.symbol = self.get_param('symbol')
|
||||
|
|
@ -212,29 +212,29 @@ class EthereumTokenTwCtl(EthereumTwCtl):
|
|||
def data_root_desc(self):
|
||||
return 'token ' + self.get_param('symbol')
|
||||
|
||||
async def rpc_get_balance(self,addr):
|
||||
return await Token(self.cfg,self.proto,self.token,self.decimals,self.rpc).get_balance(addr)
|
||||
async def rpc_get_balance(self, addr):
|
||||
return await Token(self.cfg, self.proto, self.token, self.decimals, self.rpc).get_balance(addr)
|
||||
|
||||
async def get_eth_balance(self,addr,force_rpc=False):
|
||||
async def get_eth_balance(self, addr, force_rpc=False):
|
||||
cache = self.cur_eth_balances
|
||||
r = self.data['accounts']
|
||||
ret = None if force_rpc else self.get_cached_balance(addr,cache,r)
|
||||
ret = None if force_rpc else self.get_cached_balance(addr, cache, r)
|
||||
if ret is None:
|
||||
ret = await super().rpc_get_balance(addr)
|
||||
self.cache_balance(addr,ret,cache,r)
|
||||
self.cache_balance(addr, ret, cache, r)
|
||||
return ret
|
||||
|
||||
def get_param(self,param):
|
||||
def get_param(self, param):
|
||||
return self.data['tokens'][self.token]['params'][param]
|
||||
|
||||
@write_mode
|
||||
async def import_token(self,tokenaddr):
|
||||
async def import_token(self, tokenaddr):
|
||||
"""
|
||||
Token 'symbol' and 'decimals' values are resolved from the network by the system just
|
||||
once, upon token import. Thereafter, token address, symbol and decimals are resolved
|
||||
either from the tracking wallet (online operations) or transaction file (when signing).
|
||||
"""
|
||||
t = await ResolvedToken(self.cfg,self.proto,self.rpc,tokenaddr)
|
||||
t = await ResolvedToken(self.cfg, self.proto, self.rpc, tokenaddr)
|
||||
self.data['tokens'][tokenaddr] = {
|
||||
'params': {
|
||||
'symbol': await t.get_symbol(),
|
||||
|
|
|
|||
|
|
@ -20,30 +20,30 @@ class EthereumTwJSON(TwJSON):
|
|||
|
||||
class Base(TwJSON.Base):
|
||||
|
||||
def __init__(self,proto,*args,**kwargs):
|
||||
def __init__(self, proto, *args, **kwargs):
|
||||
|
||||
self.params_keys = ['symbol','decimals']
|
||||
self.params_tuple = namedtuple('params_tuple',self.params_keys)
|
||||
self.params_keys = ['symbol', 'decimals']
|
||||
self.params_tuple = namedtuple('params_tuple', self.params_keys)
|
||||
|
||||
super().__init__(proto,*args,**kwargs)
|
||||
super().__init__(proto, *args, **kwargs)
|
||||
|
||||
@property
|
||||
def mappings_json(self):
|
||||
|
||||
def gen_mappings(data):
|
||||
for d in data:
|
||||
yield (d.mmgen_id,d.address) if hasattr(d,'mmgen_id') else d
|
||||
yield (d.mmgen_id, d.address) if hasattr(d, 'mmgen_id') else d
|
||||
|
||||
return self.json_dump({
|
||||
'accounts': list(gen_mappings(self.entries['accounts'])),
|
||||
'tokens': {k:list(gen_mappings(v)) for k,v in self.entries['tokens'].items()}
|
||||
'tokens': {k:list(gen_mappings(v)) for k, v in self.entries['tokens'].items()}
|
||||
})
|
||||
|
||||
@property
|
||||
def num_entries(self):
|
||||
return len(self.entries['accounts']) + len(self.entries['tokens'])
|
||||
|
||||
class Import(TwJSON.Import,Base):
|
||||
class Import(TwJSON.Import, Base):
|
||||
|
||||
info_msg = """
|
||||
This utility will recreate a new tracking wallet from the supplied JSON dump.
|
||||
|
|
@ -68,72 +68,72 @@ class EthereumTwJSON(TwJSON):
|
|||
else:
|
||||
e = self.entry_tuple_in(*d)
|
||||
yield self.entry_tuple(
|
||||
TwMMGenID(self.proto,e.mmgen_id),
|
||||
TwMMGenID(self.proto, e.mmgen_id),
|
||||
e.address,
|
||||
getattr(e,'amount','0'),
|
||||
e.comment )
|
||||
getattr(e, 'amount', '0'),
|
||||
e.comment)
|
||||
|
||||
def gen_token_entries():
|
||||
for token_addr,token_data in edata['tokens'].items():
|
||||
for token_addr, token_data in edata['tokens'].items():
|
||||
yield (
|
||||
token_addr,
|
||||
list(gen_entries(token_data)),
|
||||
)
|
||||
|
||||
return {
|
||||
'accounts': list(gen_entries( edata['accounts'] )),
|
||||
'accounts': list(gen_entries(edata['accounts'])),
|
||||
'tokens': dict(list(gen_token_entries()))
|
||||
}
|
||||
|
||||
async def do_import(self,batch):
|
||||
async def do_import(self, batch):
|
||||
|
||||
from ....obj import TwComment
|
||||
|
||||
def gen_data(data):
|
||||
for d in data:
|
||||
if hasattr(d,'address'):
|
||||
if d.amount is None:
|
||||
yield (d.address, {'mmid':d.mmgen_id,'comment':TwComment(d.comment)})
|
||||
else:
|
||||
yield (d.address, {'mmid':d.mmgen_id,'comment':TwComment(d.comment),'balance':d.amount})
|
||||
if hasattr(d, 'address'):
|
||||
yield (
|
||||
d.address,
|
||||
{'mmid': d.mmgen_id, 'comment': TwComment(d.comment)}
|
||||
| ({} if d.amount is None else {'balance': d.amount}))
|
||||
else:
|
||||
yield ('params', {'symbol':d.symbol,'decimals':d.decimals})
|
||||
yield ('params', {'symbol': d.symbol, 'decimals': d.decimals})
|
||||
|
||||
self.twctl.data = { # keys must be in correct order
|
||||
'coin': self.coin.upper(),
|
||||
'network': self.network.upper(),
|
||||
'accounts': dict(gen_data(self.entries['accounts'])),
|
||||
'tokens': {k:dict(gen_data(v)) for k,v in self.entries['tokens'].items()},
|
||||
'tokens': {k:dict(gen_data(v)) for k, v in self.entries['tokens'].items()},
|
||||
}
|
||||
self.twctl.write(quiet=False)
|
||||
|
||||
class Export(TwJSON.Export,Base):
|
||||
class Export(TwJSON.Export, Base):
|
||||
|
||||
async def get_entries(self,include_amts=True):
|
||||
async def get_entries(self, include_amts=True):
|
||||
|
||||
def gen_data(data):
|
||||
for k,v in data.items():
|
||||
for k, v in data.items():
|
||||
if k == 'params':
|
||||
yield self.params_tuple(**v)
|
||||
elif include_amts:
|
||||
yield self.entry_tuple(TwMMGenID(self.proto,v['mmid']), k, v.get('balance'), v['comment'])
|
||||
yield self.entry_tuple(TwMMGenID(self.proto, v['mmid']), k, v.get('balance'), v['comment'])
|
||||
else:
|
||||
yield self.entry_tuple_in(TwMMGenID(self.proto,v['mmid']), k, v['comment'])
|
||||
yield self.entry_tuple_in(TwMMGenID(self.proto, v['mmid']), k, v['comment'])
|
||||
|
||||
def gen_token_data():
|
||||
for token_addr,token_data in self.twctl.data['tokens'].items():
|
||||
for token_addr, token_data in self.twctl.data['tokens'].items():
|
||||
yield (
|
||||
token_addr,
|
||||
sorted(
|
||||
gen_data(token_data),
|
||||
key = lambda x: x.mmgen_id.sort_key if hasattr(x,'mmgen_id') else '+'
|
||||
key = lambda x: x.mmgen_id.sort_key if hasattr(x, 'mmgen_id') else '+'
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
'accounts': sorted(
|
||||
gen_data(self.twctl.data['accounts']),
|
||||
key = lambda x: x.mmgen_id.sort_key ),
|
||||
key = lambda x: x.mmgen_id.sort_key),
|
||||
'tokens': dict(sorted(gen_token_data()))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,18 +25,18 @@ from ....tw.unspent import TwUnspentOutputs
|
|||
from .view import EthereumTwView
|
||||
|
||||
# No unspent outputs with Ethereum, but naming must be consistent
|
||||
class EthereumTwUnspentOutputs(EthereumTwView,TwUnspentOutputs):
|
||||
class EthereumTwUnspentOutputs(EthereumTwView, TwUnspentOutputs):
|
||||
|
||||
class display_type(TwUnspentOutputs.display_type):
|
||||
|
||||
class squeezed(TwUnspentOutputs.display_type.squeezed):
|
||||
cols = ('num','addr','mmid','comment','amt','amt2')
|
||||
cols = ('num', 'addr', 'mmid', 'comment', 'amt', 'amt2')
|
||||
|
||||
class detail(TwUnspentOutputs.display_type.detail):
|
||||
cols = ('num','addr','mmid','amt','amt2','comment')
|
||||
cols = ('num', 'addr', 'mmid', 'amt', 'amt2', 'comment')
|
||||
|
||||
class MMGenTwUnspentOutput(TwUnspentOutputs.MMGenTwUnspentOutput):
|
||||
valid_attrs = {'txid','vout','amt','amt2','comment','twmmid','addr','confs','skip'}
|
||||
valid_attrs = {'txid', 'vout', 'amt', 'amt2', 'comment', 'twmmid', 'addr', 'confs', 'skip'}
|
||||
invalid_attrs = {'proto'}
|
||||
|
||||
has_age = False
|
||||
|
|
@ -62,19 +62,19 @@ class EthereumTwUnspentOutputs(EthereumTwView,TwUnspentOutputs):
|
|||
'w':'a_view_detail',
|
||||
'l':'i_comment_add',
|
||||
'D':'i_addr_delete',
|
||||
'R':'i_balance_refresh' }
|
||||
'R':'i_balance_refresh'}
|
||||
|
||||
no_data_errmsg = 'No accounts in tracking wallet!'
|
||||
|
||||
def get_column_widths(self,data,wide,interactive):
|
||||
def get_column_widths(self, data, wide, interactive):
|
||||
# min screen width: 80 cols
|
||||
# num addr [mmid] [comment] amt [amt2]
|
||||
return self.compute_column_widths(
|
||||
widths = { # fixed cols
|
||||
'num': max(2,len(str(len(data)))+1),
|
||||
'num': max(2, len(str(len(data)))+1),
|
||||
'mmid': max(len(d.twmmid.disp) for d in data) if self.show_mmid else 0,
|
||||
'amt': self.amt_widths['amt'],
|
||||
'amt2': self.amt_widths.get('amt2',0),
|
||||
'amt2': self.amt_widths.get('amt2', 0),
|
||||
'spc': (5 if self.show_mmid else 3) + self.has_amt2, # 5(3) spaces in fs
|
||||
'txid': 0,
|
||||
'vout': 0,
|
||||
|
|
@ -95,17 +95,17 @@ class EthereumTwUnspentOutputs(EthereumTwView,TwUnspentOutputs):
|
|||
interactive = interactive,
|
||||
)
|
||||
|
||||
def do_sort(self,key=None,reverse=False):
|
||||
def do_sort(self, key=None, reverse=False):
|
||||
if key == 'txid':
|
||||
return
|
||||
super().do_sort(key=key,reverse=reverse)
|
||||
super().do_sort(key=key, reverse=reverse)
|
||||
|
||||
async def get_rpc_data(self):
|
||||
wl = self.twctl.sorted_list
|
||||
if self.addrs:
|
||||
wl = [d for d in wl if d['addr'] in self.addrs]
|
||||
return [{
|
||||
'account': TwLabel(self.proto,d['mmid']+' '+d['comment']),
|
||||
'account': TwLabel(self.proto, d['mmid']+' '+d['comment']),
|
||||
'address': d['addr'],
|
||||
'amt': await self.twctl.get_balance(d['addr']),
|
||||
'confirmations': 0, # TODO
|
||||
|
|
@ -115,11 +115,11 @@ class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs):
|
|||
|
||||
has_amt2 = True
|
||||
|
||||
async def __init__(self,proto,*args,**kwargs):
|
||||
await super().__init__(proto,*args,**kwargs)
|
||||
async def __init__(self, proto, *args, **kwargs):
|
||||
await super().__init__(proto, *args, **kwargs)
|
||||
self.proto.tokensym = self.twctl.symbol
|
||||
|
||||
async def get_data(self,*args,**kwargs):
|
||||
await super().get_data(*args,**kwargs)
|
||||
async def get_data(self, *args, **kwargs):
|
||||
await super().get_data(*args, **kwargs)
|
||||
for e in self.data:
|
||||
e.amt2 = await self.twctl.get_eth_balance(e.addr)
|
||||
|
|
|
|||
|
|
@ -24,15 +24,15 @@ class EthereumTwView(TwView):
|
|||
'twmmid': lambda i: i.twmmid.sort_key
|
||||
}
|
||||
|
||||
def age_disp(self,o,age_fmt): # TODO
|
||||
def age_disp(self, o, age_fmt): # TODO
|
||||
pass
|
||||
|
||||
def get_disp_prec(self,wide):
|
||||
def get_disp_prec(self, wide):
|
||||
return self.proto.coin_amt.max_prec if wide else 8
|
||||
|
||||
def gen_subheader(self,cw,color):
|
||||
def gen_subheader(self, cw, color):
|
||||
if self.disp_prec == 8:
|
||||
yield 'Balances truncated to 8 decimal points'
|
||||
if self.cfg.cached_balances:
|
||||
from ....color import nocolor,yellow
|
||||
yield (nocolor,yellow)[color]('WARNING: Using cached balances. These may be out of date!')
|
||||
from ....color import nocolor, yellow
|
||||
yield (nocolor, yellow)[color]('WARNING: Using cached balances. These may be out of date!')
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ proto.eth.tx.base: Ethereum base transaction class
|
|||
from collections import namedtuple
|
||||
|
||||
from ....tx import base as TxBase
|
||||
from ....obj import HexStr,Int
|
||||
from ....obj import HexStr, Int
|
||||
|
||||
class Base(TxBase.Base):
|
||||
|
||||
|
|
@ -52,16 +52,17 @@ class Base(TxBase.Base):
|
|||
def is_replaceable(self):
|
||||
return True
|
||||
|
||||
async def get_receipt(self,txid):
|
||||
rx = await self.rpc.call('eth_getTransactionReceipt','0x'+txid) # -> null if pending
|
||||
async def get_receipt(self, txid):
|
||||
rx = await self.rpc.call('eth_getTransactionReceipt', '0x'+txid) # -> null if pending
|
||||
if not rx:
|
||||
return None
|
||||
tx = await self.rpc.call('eth_getTransactionByHash','0x'+txid)
|
||||
return namedtuple('exec_status',['status','gas_sent','gas_used','gas_price','contract_addr','tx','rx'])(
|
||||
status = Int(rx['status'],16), # zero is failure, non-zero success
|
||||
gas_sent = Int(tx['gas'],16),
|
||||
gas_used = Int(rx['gasUsed'],16),
|
||||
gas_price = self.proto.coin_amt(int(tx['gasPrice'],16),from_unit='wei'),
|
||||
tx = await self.rpc.call('eth_getTransactionByHash', '0x'+txid)
|
||||
return namedtuple('exec_status',
|
||||
['status', 'gas_sent', 'gas_used', 'gas_price', 'contract_addr', 'tx', 'rx'])(
|
||||
status = Int(rx['status'], 16), # zero is failure, non-zero success
|
||||
gas_sent = Int(tx['gas'], 16),
|
||||
gas_used = Int(rx['gasUsed'], 16),
|
||||
gas_price = self.proto.coin_amt(int(tx['gasPrice'], 16), from_unit='wei'),
|
||||
contract_addr = self.proto.coin_addr(rx['contractAddress'][2:]) if rx['contractAddress'] else None,
|
||||
tx = tx,
|
||||
rx = rx,
|
||||
|
|
|
|||
|
|
@ -15,23 +15,23 @@ proto.eth.tx.bump: Ethereum transaction bump class
|
|||
from decimal import Decimal
|
||||
|
||||
from ....tx import bump as TxBase
|
||||
from .completed import Completed,TokenCompleted
|
||||
from .new import New,TokenNew
|
||||
from .completed import Completed, TokenCompleted
|
||||
from .new import New, TokenNew
|
||||
|
||||
class Bump(Completed,New,TxBase.Bump):
|
||||
class Bump(Completed, New, TxBase.Bump):
|
||||
desc = 'fee-bumped transaction'
|
||||
|
||||
@property
|
||||
def min_fee(self):
|
||||
return self.fee * Decimal('1.101')
|
||||
|
||||
def bump_fee(self,idx,fee):
|
||||
def bump_fee(self, idx, fee):
|
||||
self.txobj['gasPrice'] = self.fee_abs2gas(fee)
|
||||
|
||||
async def get_nonce(self):
|
||||
return self.txobj['nonce']
|
||||
|
||||
class TokenBump(TokenCompleted,TokenNew,Bump):
|
||||
class TokenBump(TokenCompleted, TokenNew, Bump):
|
||||
desc = 'fee-bumped transaction'
|
||||
|
||||
class AutomountBump(Bump):
|
||||
|
|
|
|||
|
|
@ -13,16 +13,16 @@ proto.eth.tx.completed: Ethereum completed transaction class
|
|||
"""
|
||||
|
||||
from ....tx import completed as TxBase
|
||||
from .base import Base,TokenBase
|
||||
from .base import Base, TokenBase
|
||||
|
||||
class Completed(Base,TxBase.Completed):
|
||||
class Completed(Base, TxBase.Completed):
|
||||
fn_fee_unit = 'Mwei'
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
self.txobj = {}
|
||||
|
||||
super().__init__(*args,**kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.gas = self.proto.coin_amt(self.dfl_gas, from_unit='wei')
|
||||
self.start_gas = self.proto.coin_amt(self.dfl_start_gas, from_unit='wei')
|
||||
|
|
@ -54,7 +54,7 @@ class Completed(Base,TxBase.Completed):
|
|||
def get_serialized_locktime(self):
|
||||
return None # TODO
|
||||
|
||||
class TokenCompleted(TokenBase,Completed):
|
||||
class TokenCompleted(TokenBase, Completed):
|
||||
|
||||
@property
|
||||
def change(self):
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ class TxInfo(TxInfo):
|
|||
def format_body(self, blockcount, nonmm_str, max_mmwid, enl, terse, sort):
|
||||
tx = self.tx
|
||||
m = {}
|
||||
for k in ('inputs','outputs'):
|
||||
if len(getattr(tx,k)):
|
||||
m[k] = getattr(tx,k)[0].mmid if len(getattr(tx,k)) else ''
|
||||
for k in ('inputs', 'outputs'):
|
||||
if len(getattr(tx, k)):
|
||||
m[k] = getattr(tx, k)[0].mmid if len(getattr(tx, k)) else ''
|
||||
m[k] = ' ' + m[k].hl() if m[k] else ' ' + MMGenID.hlc(nonmm_str)
|
||||
fs = """
|
||||
From: {f}{f_mmid}
|
||||
|
|
@ -43,7 +43,7 @@ class TxInfo(TxInfo):
|
|||
Start gas: {G} Kwei
|
||||
Nonce: {n}
|
||||
Data: {d}
|
||||
""".strip().replace('\t','')
|
||||
""".strip().replace('\t', '')
|
||||
t = tx.txobj
|
||||
td = t['data']
|
||||
to_addr = t[self.to_addr_key]
|
||||
|
|
@ -52,19 +52,19 @@ class TxInfo(TxInfo):
|
|||
t = to_addr.hl(0) if to_addr else blue('None'),
|
||||
a = t['amt'].hl(),
|
||||
n = t['nonce'].hl(),
|
||||
d = '{}... ({} bytes)'.format(td[:40],len(td)//2) if len(td) else blue('None'),
|
||||
d = '{}... ({} bytes)'.format(td[:40], len(td)//2) if len(td) else blue('None'),
|
||||
c = tx.proto.dcoin if len(tx.outputs) else '',
|
||||
g = yellow(tx.pretty_fmt_fee(t['gasPrice'].to_unit('Gwei'))),
|
||||
G = yellow(tx.pretty_fmt_fee(t['startGas'].to_unit('Kwei'))),
|
||||
t_mmid = m['outputs'] if len(tx.outputs) else '',
|
||||
f_mmid = m['inputs']) + '\n\n'
|
||||
|
||||
def format_abs_fee(self,color,iwidth):
|
||||
return self.tx.fee.fmt(color=color,iwidth=iwidth) + (' (max)' if self.tx.txobj['data'] else '')
|
||||
def format_abs_fee(self, color, iwidth):
|
||||
return self.tx.fee.fmt(color=color, iwidth=iwidth) + (' (max)' if self.tx.txobj['data'] else '')
|
||||
|
||||
def format_rel_fee(self):
|
||||
return ' ({} of spend amount)'.format(
|
||||
pink('{:0.6f}%'.format( self.tx.fee / self.tx.send_amt * 100 ))
|
||||
pink('{:0.6f}%'.format(self.tx.fee / self.tx.send_amt * 100))
|
||||
)
|
||||
|
||||
def format_verbose_footer(self):
|
||||
|
|
@ -80,8 +80,8 @@ class TokenTxInfo(TxInfo):
|
|||
def format_rel_fee(self):
|
||||
return ''
|
||||
|
||||
def format_body(self,*args,**kwargs):
|
||||
def format_body(self, *args, **kwargs):
|
||||
return 'Token: {d} {c}\n{r}'.format(
|
||||
d = self.tx.txobj['token_addr'].hl(0),
|
||||
c = blue('(' + self.tx.proto.dcoin + ')'),
|
||||
r = super().format_body(*args,**kwargs ))
|
||||
r = super().format_body(*args, **kwargs))
|
||||
|
|
|
|||
|
|
@ -22,16 +22,16 @@ from ....addr import is_mmgen_id, is_coin_addr
|
|||
from ..contract import Token
|
||||
from .base import Base, TokenBase
|
||||
|
||||
class New(Base,TxBase.New):
|
||||
class New(Base, TxBase.New):
|
||||
desc = 'transaction'
|
||||
fee_fail_fs = 'Network fee estimation failed'
|
||||
no_chg_msg = 'Warning: Transaction leaves account with zero balance'
|
||||
usr_fee_prompt = 'Enter transaction fee or gas price: '
|
||||
msg_insufficient_funds = 'Account balance insufficient to fund this transaction ({} {} needed)'
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args,**kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.cfg.gas:
|
||||
self.gas = self.start_gas = self.proto.coin_amt(int(self.cfg.gas), from_unit='wei')
|
||||
|
|
@ -47,7 +47,8 @@ class New(Base,TxBase.New):
|
|||
self.disable_fee_check = True
|
||||
|
||||
async def get_nonce(self):
|
||||
return ETHNonce(int(await self.rpc.call('eth_getTransactionCount','0x'+self.inputs[0].addr,'pending'),16))
|
||||
return ETHNonce(int(
|
||||
await self.rpc.call('eth_getTransactionCount', '0x'+self.inputs[0].addr, 'pending'), 16))
|
||||
|
||||
async def make_txobj(self): # called by create_serialized()
|
||||
self.txobj = {
|
||||
|
|
@ -64,22 +65,22 @@ class New(Base,TxBase.New):
|
|||
# Instead of serializing tx data as with BTC, just create a JSON dump.
|
||||
# This complicates things but means we avoid using the rlp library to deserialize the data,
|
||||
# thus removing an attack vector
|
||||
async def create_serialized(self,locktime=None,bump=None):
|
||||
assert len(self.inputs) == 1,'Transaction has more than one input!'
|
||||
async def create_serialized(self, locktime=None, bump=None):
|
||||
assert len(self.inputs) == 1, 'Transaction has more than one input!'
|
||||
o_num = len(self.outputs)
|
||||
o_ok = 0 if self.usr_contract_data else 1
|
||||
assert o_num == o_ok, f'Transaction has {o_num} output{suf(o_num)} (should have {o_ok})'
|
||||
await self.make_txobj()
|
||||
odict = {k:v if v is None else str(v) for k,v in self.txobj.items() if k != 'token_to'}
|
||||
odict = {k:v if v is None else str(v) for k, v in self.txobj.items() if k != 'token_to'}
|
||||
self.serialized = json.dumps(odict)
|
||||
self.update_txid()
|
||||
|
||||
def update_txid(self):
|
||||
assert not is_hex_str(self.serialized), (
|
||||
'update_txid() must be called only when self.serialized is not hex data' )
|
||||
'update_txid() must be called only when self.serialized is not hex data')
|
||||
self.txid = MMGenTxID(make_chksum_6(self.serialized).upper())
|
||||
|
||||
async def process_cmd_args(self,cmd_args,ad_f,ad_w):
|
||||
async def process_cmd_args(self, cmd_args, ad_f, ad_w):
|
||||
|
||||
lc = len(cmd_args)
|
||||
|
||||
|
|
@ -96,10 +97,10 @@ class New(Base,TxBase.New):
|
|||
amt = self.proto.coin_amt(arg.amt or '0'),
|
||||
is_chg = not arg.amt)
|
||||
|
||||
def select_unspent(self,unspent):
|
||||
def select_unspent(self, unspent):
|
||||
from ....ui import line_input
|
||||
while True:
|
||||
reply = line_input( self.cfg, 'Enter an account to spend from: ' ).strip()
|
||||
reply = line_input(self.cfg, 'Enter an account to spend from: ').strip()
|
||||
if reply:
|
||||
if not is_int(reply):
|
||||
msg('Account number must be an integer')
|
||||
|
|
@ -116,7 +117,7 @@ class New(Base,TxBase.New):
|
|||
|
||||
# get rel_fee (gas price) from network, return in native wei
|
||||
async def get_rel_fee_from_network(self):
|
||||
return Int(await self.rpc.call('eth_gasPrice'),16), 'eth_gasPrice'
|
||||
return Int(await self.rpc.call('eth_gasPrice'), 16), 'eth_gasPrice'
|
||||
|
||||
def check_fee(self):
|
||||
if not self.disable_fee_check:
|
||||
|
|
@ -127,14 +128,14 @@ class New(Base,TxBase.New):
|
|||
return self.proto.coin_amt(amt_in_units, from_unit=units[unit]) * self.gas.toWei()
|
||||
|
||||
# given fee estimate (gas price) in wei, return absolute fee, adjusting by self.cfg.fee_adjust
|
||||
def fee_est2abs(self,rel_fee,fe_type=None):
|
||||
def fee_est2abs(self, rel_fee, fe_type=None):
|
||||
ret = self.fee_gasPrice2abs(rel_fee) * self.cfg.fee_adjust
|
||||
if self.cfg.verbose:
|
||||
msg(f'Estimated fee: {ret} ETH')
|
||||
return ret
|
||||
|
||||
def convert_and_check_fee(self,fee,desc):
|
||||
abs_fee = self.feespec2abs(fee,None)
|
||||
def convert_and_check_fee(self, fee, desc):
|
||||
abs_fee = self.feespec2abs(fee, None)
|
||||
if abs_fee is False:
|
||||
return False
|
||||
elif not self.disable_fee_check and (abs_fee > self.proto.max_tx_fee):
|
||||
|
|
@ -142,71 +143,71 @@ class New(Base,TxBase.New):
|
|||
abs_fee.hl(),
|
||||
desc,
|
||||
self.proto.max_tx_fee.hl(),
|
||||
c = self.proto.coin ))
|
||||
c = self.proto.coin))
|
||||
return False
|
||||
else:
|
||||
return abs_fee
|
||||
|
||||
def update_change_output(self,funds_left):
|
||||
def update_change_output(self, funds_left):
|
||||
if self.outputs and self.outputs[0].is_chg:
|
||||
self.update_output_amt(0, funds_left)
|
||||
|
||||
async def get_input_addrs_from_cmdline(self):
|
||||
ret = []
|
||||
if self.cfg.inputs:
|
||||
data_root = (await TwCtl(self.cfg,self.proto)).data_root # must create new instance here
|
||||
data_root = (await TwCtl(self.cfg, self.proto)).data_root # must create new instance here
|
||||
errmsg = 'Address {!r} not in tracking wallet'
|
||||
for addr in self.cfg.inputs.split(','):
|
||||
if is_mmgen_id(self.proto,addr):
|
||||
if is_mmgen_id(self.proto, addr):
|
||||
for waddr in data_root:
|
||||
if data_root[waddr]['mmid'] == addr:
|
||||
ret.append(waddr)
|
||||
break
|
||||
else:
|
||||
die( 'UserAddressNotInWallet', errmsg.format(addr) )
|
||||
elif is_coin_addr(self.proto,addr):
|
||||
die('UserAddressNotInWallet', errmsg.format(addr))
|
||||
elif is_coin_addr(self.proto, addr):
|
||||
if not addr in data_root:
|
||||
die( 'UserAddressNotInWallet', errmsg.format(addr) )
|
||||
die('UserAddressNotInWallet', errmsg.format(addr))
|
||||
ret.append(addr)
|
||||
else:
|
||||
die(1,f'{addr!r}: not an MMGen ID or coin address')
|
||||
die(1, f'{addr!r}: not an MMGen ID or coin address')
|
||||
return ret
|
||||
|
||||
def final_inputs_ok_msg(self, funds_left):
|
||||
chg = self.proto.coin_amt('0') if (self.outputs and self.outputs[0].is_chg) else funds_left
|
||||
return 'Transaction leaves {} {} in the sender’s account'.format(chg.hl(), self.proto.coin)
|
||||
|
||||
class TokenNew(TokenBase,New):
|
||||
class TokenNew(TokenBase, New):
|
||||
desc = 'transaction'
|
||||
fee_is_approximate = True
|
||||
|
||||
async def make_txobj(self): # called by create_serialized()
|
||||
await super().make_txobj()
|
||||
t = Token(self.cfg,self.proto,self.twctl.token,self.twctl.decimals)
|
||||
t = Token(self.cfg, self.proto, self.twctl.token, self.twctl.decimals)
|
||||
o = self.txobj
|
||||
o['token_addr'] = t.addr
|
||||
o['decimals'] = t.decimals
|
||||
o['token_to'] = o['to']
|
||||
o['data'] = t.create_data(o['token_to'],o['amt'])
|
||||
o['data'] = t.create_data(o['token_to'], o['amt'])
|
||||
|
||||
def update_change_output(self,funds_left):
|
||||
def update_change_output(self, funds_left):
|
||||
if self.outputs[0].is_chg:
|
||||
self.update_output_amt(0,self.inputs[0].amt)
|
||||
self.update_output_amt(0, self.inputs[0].amt)
|
||||
|
||||
# token transaction, so check both eth and token balances
|
||||
# TODO: add test with insufficient funds
|
||||
async def precheck_sufficient_funds(self,inputs_sum,sel_unspent,outputs_sum):
|
||||
async def precheck_sufficient_funds(self, inputs_sum, sel_unspent, outputs_sum):
|
||||
eth_bal = await self.twctl.get_eth_balance(sel_unspent[0].addr)
|
||||
if eth_bal == 0: # we don't know the fee yet
|
||||
msg('This account has no ether to pay for the transaction fee!')
|
||||
return False
|
||||
return await super().precheck_sufficient_funds(inputs_sum,sel_unspent,outputs_sum)
|
||||
return await super().precheck_sufficient_funds(inputs_sum, sel_unspent, outputs_sum)
|
||||
|
||||
async def get_funds_available(self, fee, outputs_sum):
|
||||
bal = await self.twctl.get_eth_balance(self.inputs[0].addr)
|
||||
return self._funds_available(bal >= fee, bal - fee if bal >= fee else fee - bal)
|
||||
|
||||
def final_inputs_ok_msg(self,funds_left):
|
||||
def final_inputs_ok_msg(self, funds_left):
|
||||
token_bal = (
|
||||
self.proto.coin_amt('0') if self.outputs[0].is_chg
|
||||
else self.inputs[0].amt - self.outputs[0].amt
|
||||
|
|
|
|||
|
|
@ -12,24 +12,24 @@
|
|||
proto.eth.tx.online: Ethereum online signed transaction class
|
||||
"""
|
||||
|
||||
from ....util import msg,die
|
||||
from ....util import msg, die
|
||||
from ....color import orange
|
||||
from ....tx import online as TxBase
|
||||
from .. import erigon_sleep
|
||||
from .signed import Signed,TokenSigned
|
||||
from .signed import Signed, TokenSigned
|
||||
|
||||
class OnlineSigned(Signed,TxBase.OnlineSigned):
|
||||
class OnlineSigned(Signed, TxBase.OnlineSigned):
|
||||
|
||||
async def send(self,prompt_user=True):
|
||||
async def send(self, prompt_user=True):
|
||||
|
||||
self.check_correct_chain()
|
||||
|
||||
if not self.disable_fee_check and (self.fee > self.proto.max_tx_fee):
|
||||
die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
|
||||
die(2, 'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
|
||||
self.fee,
|
||||
self.proto.name,
|
||||
self.proto.max_tx_fee,
|
||||
self.proto.coin ))
|
||||
self.proto.coin))
|
||||
|
||||
await self.status.display()
|
||||
|
||||
|
|
@ -40,12 +40,12 @@ class OnlineSigned(Signed,TxBase.OnlineSigned):
|
|||
m = 'BOGUS transaction NOT sent: {}'
|
||||
else:
|
||||
try:
|
||||
ret = await self.rpc.call('eth_sendRawTransaction','0x'+self.serialized)
|
||||
ret = await self.rpc.call('eth_sendRawTransaction', '0x'+self.serialized)
|
||||
except Exception as e:
|
||||
msg(orange('\n'+str(e)))
|
||||
die(2, f'Send of MMGen transaction {self.txid} failed')
|
||||
m = 'Transaction sent: {}'
|
||||
assert ret == '0x'+self.coin_txid,'txid mismatch (after sending)'
|
||||
assert ret == '0x'+self.coin_txid, 'txid mismatch (after sending)'
|
||||
await erigon_sleep(self)
|
||||
|
||||
msg(m.format(self.coin_txid.hl()))
|
||||
|
|
@ -58,7 +58,7 @@ class OnlineSigned(Signed,TxBase.OnlineSigned):
|
|||
if 'token_addr' in self.txobj:
|
||||
msg('Contract address: {}'.format(self.txobj['token_addr'].hl(0)))
|
||||
|
||||
class TokenOnlineSigned(TokenSigned,OnlineSigned):
|
||||
class TokenOnlineSigned(TokenSigned, OnlineSigned):
|
||||
|
||||
def parse_txfile_serialized_data(self):
|
||||
from ....addr import TokenAddr
|
||||
|
|
@ -66,9 +66,9 @@ class TokenOnlineSigned(TokenSigned,OnlineSigned):
|
|||
OnlineSigned.parse_txfile_serialized_data(self)
|
||||
o = self.txobj
|
||||
assert self.twctl.token == o['to']
|
||||
o['token_addr'] = TokenAddr(self.proto,o['to'])
|
||||
o['token_addr'] = TokenAddr(self.proto, o['to'])
|
||||
o['decimals'] = self.twctl.decimals
|
||||
t = Token(self.cfg,self.proto,o['token_addr'],o['decimals'])
|
||||
t = Token(self.cfg, self.proto, o['token_addr'], o['decimals'])
|
||||
o['amt'] = t.transferdata2amt(o['data'])
|
||||
o['token_to'] = t.transferdata2sendaddr(o['data'])
|
||||
|
||||
|
|
|
|||
|
|
@ -17,22 +17,22 @@ from ....obj import CoinTxID, ETHNonce, HexStr
|
|||
from ....addr import CoinAddr, TokenAddr
|
||||
from .completed import Completed, TokenCompleted
|
||||
|
||||
class Signed(Completed,TxBase.Signed):
|
||||
class Signed(Completed, TxBase.Signed):
|
||||
|
||||
desc = 'signed transaction'
|
||||
|
||||
def parse_txfile_serialized_data(self):
|
||||
from ..pyethereum.transactions import Transaction
|
||||
from .. import rlp
|
||||
etx = rlp.decode(bytes.fromhex(self.serialized),Transaction)
|
||||
etx = rlp.decode(bytes.fromhex(self.serialized), Transaction)
|
||||
d = etx.to_dict() # ==> hex values have '0x' prefix, 0 is '0x'
|
||||
for k in ('sender','to','data'):
|
||||
for k in ('sender', 'to', 'data'):
|
||||
if k in d:
|
||||
d[k] = d[k].replace('0x','',1)
|
||||
d[k] = d[k].replace('0x', '', 1)
|
||||
o = {
|
||||
'from': CoinAddr(self.proto,d['sender']),
|
||||
'from': CoinAddr(self.proto, d['sender']),
|
||||
# NB: for token, 'to' is token address
|
||||
'to': CoinAddr(self.proto,d['to']) if d['to'] else None,
|
||||
'to': CoinAddr(self.proto, d['to']) if d['to'] else None,
|
||||
'amt': self.proto.coin_amt(d['value'], from_unit='wei'),
|
||||
'gasPrice': self.proto.coin_amt(d['gasprice'], from_unit='wei'),
|
||||
'startGas': self.proto.coin_amt(d['startgas'], from_unit='wei'),
|
||||
|
|
@ -40,15 +40,15 @@ class Signed(Completed,TxBase.Signed):
|
|||
'data': HexStr(d['data']) }
|
||||
if o['data'] and not o['to']: # token- or contract-creating transaction
|
||||
# NB: could be a non-token contract address:
|
||||
o['token_addr'] = TokenAddr(self.proto,etx.creates.hex())
|
||||
o['token_addr'] = TokenAddr(self.proto, etx.creates.hex())
|
||||
self.disable_fee_check = True
|
||||
txid = CoinTxID(etx.hash.hex())
|
||||
assert txid == self.coin_txid,"txid in tx.serialized doesn't match value in MMGen transaction file"
|
||||
assert txid == self.coin_txid, "txid in tx.serialized doesn't match value in MMGen transaction file"
|
||||
self.gas = o['startGas'] # approximate, but better than nothing
|
||||
self.txobj = o
|
||||
return d # 'token_addr','decimals' required by Token subclass
|
||||
return d # 'token_addr', 'decimals' required by Token subclass
|
||||
|
||||
class TokenSigned(TokenCompleted,Signed):
|
||||
class TokenSigned(TokenCompleted, Signed):
|
||||
desc = 'signed transaction'
|
||||
|
||||
def parse_txfile_serialized_data(self):
|
||||
|
|
|
|||
|
|
@ -13,38 +13,38 @@ proto.eth.tx.status: Ethereum transaction status class
|
|||
"""
|
||||
|
||||
from ....tx import status as TxBase
|
||||
from ....util import msg,die,suf,capfirst
|
||||
from ....util import msg, die, suf, capfirst
|
||||
|
||||
class Status(TxBase.Status):
|
||||
|
||||
async def display(self,usr_req=False):
|
||||
async def display(self, usr_req=False):
|
||||
|
||||
tx = self.tx
|
||||
|
||||
async def is_in_mempool():
|
||||
if not 'full_node' in tx.rpc.caps:
|
||||
return False
|
||||
if tx.rpc.daemon.id in ('parity','openethereum'):
|
||||
if tx.rpc.daemon.id in ('parity', 'openethereum'):
|
||||
pool = [x['hash'] for x in await tx.rpc.call('parity_pendingTransactions')]
|
||||
elif tx.rpc.daemon.id in ('geth','erigon'):
|
||||
elif tx.rpc.daemon.id in ('geth', 'erigon'):
|
||||
res = await tx.rpc.call('txpool_content')
|
||||
pool = list(res['pending']) + list(res['queued'])
|
||||
return '0x'+tx.coin_txid in pool
|
||||
|
||||
async def is_in_wallet():
|
||||
d = await tx.rpc.call('eth_getTransactionReceipt','0x'+tx.coin_txid)
|
||||
d = await tx.rpc.call('eth_getTransactionReceipt', '0x'+tx.coin_txid)
|
||||
if d and 'blockNumber' in d and d['blockNumber'] is not None:
|
||||
from collections import namedtuple
|
||||
receipt_info = namedtuple('receipt_info',['confs','exec_status'])
|
||||
receipt_info = namedtuple('receipt_info', ['confs', 'exec_status'])
|
||||
return receipt_info(
|
||||
confs = 1 + int(await tx.rpc.call('eth_blockNumber'),16) - int(d['blockNumber'],16),
|
||||
exec_status = int(d['status'],16)
|
||||
confs = 1 + int(await tx.rpc.call('eth_blockNumber'), 16) - int(d['blockNumber'], 16),
|
||||
exec_status = int(d['status'], 16)
|
||||
)
|
||||
|
||||
if await is_in_mempool():
|
||||
msg(
|
||||
'Transaction is in mempool' if usr_req else
|
||||
'Warning: transaction is in mempool!' )
|
||||
'Warning: transaction is in mempool!')
|
||||
return
|
||||
|
||||
if usr_req:
|
||||
|
|
@ -56,8 +56,8 @@ class Status(TxBase.Status):
|
|||
msg(f'{cd} failed to execute!')
|
||||
else:
|
||||
msg(f'{cd} successfully executed with status {ret.exec_status}')
|
||||
die(0,f'Transaction has {ret.confs} confirmation{suf(ret.confs)}')
|
||||
die(1,'Transaction is neither in mempool nor blockchain!')
|
||||
die(0, f'Transaction has {ret.confs} confirmation{suf(ret.confs)}')
|
||||
die(1, 'Transaction is neither in mempool nor blockchain!')
|
||||
|
||||
class TokenStatus(Status):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -21,26 +21,26 @@ from ....addr import CoinAddr, TokenAddr
|
|||
from ..contract import Token
|
||||
from .completed import Completed, TokenCompleted
|
||||
|
||||
class Unsigned(Completed,TxBase.Unsigned):
|
||||
class Unsigned(Completed, TxBase.Unsigned):
|
||||
desc = 'unsigned transaction'
|
||||
|
||||
def parse_txfile_serialized_data(self):
|
||||
d = json.loads(self.serialized)
|
||||
o = {
|
||||
'from': CoinAddr(self.proto,d['from']),
|
||||
'from': CoinAddr(self.proto, d['from']),
|
||||
# NB: for token, 'to' is sendto address
|
||||
'to': CoinAddr(self.proto,d['to']) if d['to'] else None,
|
||||
'to': CoinAddr(self.proto, d['to']) if d['to'] else None,
|
||||
'amt': self.proto.coin_amt(d['amt']),
|
||||
'gasPrice': self.proto.coin_amt(d['gasPrice']),
|
||||
'startGas': self.proto.coin_amt(d['startGas']),
|
||||
'nonce': ETHNonce(d['nonce']),
|
||||
'chainId': None if d['chainId'] == 'None' else Int(d['chainId']),
|
||||
'data': HexStr(d['data']) }
|
||||
'data': HexStr(d['data'])}
|
||||
self.gas = o['startGas'] # approximate, but better than nothing
|
||||
self.txobj = o
|
||||
return d # 'token_addr','decimals' required by Token subclass
|
||||
return d # 'token_addr', 'decimals' required by Token subclass
|
||||
|
||||
async def do_sign(self,wif):
|
||||
async def do_sign(self, wif):
|
||||
o = self.txobj
|
||||
o_conv = {
|
||||
'to': bytes.fromhex(o['to'] or ''),
|
||||
|
|
@ -61,11 +61,11 @@ class Unsigned(Completed,TxBase.Unsigned):
|
|||
|
||||
if o['data']:
|
||||
if o['to']:
|
||||
assert self.txobj['token_addr'] == TokenAddr(self.proto,etx.creates.hex()),'Token address mismatch'
|
||||
assert self.txobj['token_addr'] == TokenAddr(self.proto, etx.creates.hex()), 'Token address mismatch'
|
||||
else: # token- or contract-creating transaction
|
||||
self.txobj['token_addr'] = TokenAddr(self.proto,etx.creates.hex())
|
||||
self.txobj['token_addr'] = TokenAddr(self.proto, etx.creates.hex())
|
||||
|
||||
async def sign(self,tx_num_str,keys): # return TX object or False; don't exit or raise exception
|
||||
async def sign(self, tx_num_str, keys): # return TX object or False; don't exit or raise exception
|
||||
|
||||
from ....exception import TransactionChainMismatch
|
||||
try:
|
||||
|
|
@ -84,28 +84,28 @@ class Unsigned(Completed,TxBase.Unsigned):
|
|||
msg(f'{e}: transaction signing failed!')
|
||||
return False
|
||||
|
||||
class TokenUnsigned(TokenCompleted,Unsigned):
|
||||
class TokenUnsigned(TokenCompleted, Unsigned):
|
||||
desc = 'unsigned transaction'
|
||||
|
||||
def parse_txfile_serialized_data(self):
|
||||
d = Unsigned.parse_txfile_serialized_data(self)
|
||||
o = self.txobj
|
||||
o['token_addr'] = TokenAddr(self.proto,d['token_addr'])
|
||||
o['token_addr'] = TokenAddr(self.proto, d['token_addr'])
|
||||
o['decimals'] = Int(d['decimals'])
|
||||
t = Token(self.cfg,self.proto,o['token_addr'],o['decimals'])
|
||||
o['data'] = t.create_data(o['to'],o['amt'])
|
||||
t = Token(self.cfg, self.proto, o['token_addr'], o['decimals'])
|
||||
o['data'] = t.create_data(o['to'], o['amt'])
|
||||
o['token_to'] = t.transferdata2sendaddr(o['data'])
|
||||
|
||||
async def do_sign(self,wif):
|
||||
async def do_sign(self, wif):
|
||||
o = self.txobj
|
||||
t = Token(self.cfg,self.proto,o['token_addr'],o['decimals'])
|
||||
t = Token(self.cfg, self.proto, o['token_addr'], o['decimals'])
|
||||
tx_in = t.make_tx_in(
|
||||
to_addr = o['to'],
|
||||
amt = o['amt'],
|
||||
start_gas = self.start_gas,
|
||||
gasPrice = o['gasPrice'],
|
||||
nonce = o['nonce'])
|
||||
(self.serialized,self.coin_txid) = await t.txsign(tx_in,wif,o['from'],chain_id=o['chainId'])
|
||||
(self.serialized, self.coin_txid) = await t.txsign(tx_in, wif, o['from'], chain_id=o['chainId'])
|
||||
|
||||
class AutomountUnsigned(TxBase.AutomountUnsigned, Unsigned):
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue