whitespace: proto.eth (plus cleanup)

This commit is contained in:
The MMGen Project 2024-10-18 10:32:09 +00:00
commit 6346c1d11a
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
22 changed files with 321 additions and 318 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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