test.py ethdev, scripts/create-token.py: cleanups

This commit is contained in:
The MMGen Project 2021-09-03 20:17:25 +00:00
commit 4742255ad2
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
4 changed files with 163 additions and 100 deletions

View file

@ -60,12 +60,20 @@ class EthereumMMGenTX:
def is_replaceable(self):
return True
async def get_exec_status(self,txid,silent=False):
d = await self.rpc.call('eth_getTransactionReceipt','0x'+txid)
if not silent:
if 'contractAddress' in d and d['contractAddress']:
msg('Contract address: {}'.format(d['contractAddress'].replace('0x','')))
return int(d['status'],16)
async def get_receipt(self,txid,silent=False):
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 = ETHAmt(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,
)
class New(Base,MMGenTX.New):
hexdata_type = 'hex'

View file

@ -520,6 +520,14 @@ class CoinAmt(Decimal,Hilite,InitErrors): # abstract class
def hl(self,color=True):
return self.colorize(self.__str__(),color=color)
def hl2(self,color=True,encl=''): # display with coin symbol
return (
encl[:-1]
+ self.colorize(self.__str__(),color=color)
+ ' ' + type(self).__name__[:-3]
+ encl[1:]
)
def __str__(self): # format simply, with no exponential notation
return str(int(self)) if int(self) == self else self.normalize().__format__('f')

View file

@ -19,56 +19,46 @@
import sys,os,json,re
from subprocess import run,PIPE
from mmgen.common import *
from mmgen.obj import CoinAddr,is_coin_addr
decimals = 18
supply = 10**26
name = 'MMGen Token'
symbol = 'MMT'
solc_version_pat = r'0.5.[123]'
class TokenData:
attrs = ('decimals','supply','name','symbol','owner_addr')
decimals = 18
supply = 10**26
name = 'MMGen Token'
symbol = 'MMT'
owner_addr = None
token_data = TokenData()
req_solc_ver_pat = '^0.5.2'
opts_data = {
'text': {
'desc': 'Create an ERC20 token contract',
'usage':'[opts] <owner address>',
'options': """
-h, --help Print this help message
-o, --outdir= d Specify output directory for *.bin files
-d, --decimals=d Number of decimals for the token (default: {d})
-n, --name=n Token name (default: {n})
-t, --supply= t Total supply of the token (default: {t})
-s, --symbol= s Token symbol (default: {s})
-S, --stdout Output data in JSON format to stdout instead of files
-v, --verbose Produce more verbose output
'options': f"""
-h, --help Print this help message
-o, --outdir=D Specify output directory for *.bin files
-d, --decimals=D Number of decimals for the token (default: {token_data.decimals})
-n, --name=N Token name (default: {token_data.name!r})
-t, --supply=T Total supply of the token (default: {token_data.supply})
-s, --symbol=S Token symbol (default: {token_data.symbol!r})
-S, --stdout Output JSON data to stdout instead of files
-v, --verbose Produce more verbose output
-c, --check-solc-version Check the installed solc version
""",
'notes': """
The owner address must be in checksummed format
"""
},
'code': {
'options': lambda s: s.format(
d=decimals,
n=name,
s=symbol,
t=supply)
}
}
cmd_args = opts.init(opts_data)
from mmgen.protocol import init_proto_from_opts
proto = init_proto_from_opts()
assert proto.coin in ('ETH','ETC'),'--coin option must be set to ETH or ETC'
if not len(cmd_args) == 1 or not is_coin_addr(proto,cmd_args[0].lower()):
opts.usage()
owner_addr = '0x' + cmd_args[0]
# ERC Token Standard #20 Interface
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md
code_in = """
solidity_code_template = """
pragma solidity >0.5.0 <0.5.4;
pragma solidity %s;
contract SafeMath {
function safeAdd(uint a, uint b) public pure returns (uint c) {
@ -180,23 +170,49 @@ contract Token is ERC20Interface, Owned, SafeMath {
return ERC20Interface(tokenAddress).transfer(owner, tokens);
}
}
"""
""" % req_solc_ver_pat
def create_src(code):
for k in ('decimals','supply','name','symbol','owner_addr'):
if hasattr(opt,k) and getattr(opt,k): globals()[k] = getattr(opt,k)
code = code.replace('<{}>'.format(k.upper()),str(globals()[k]))
def create_src(code,token_data,owner_addr):
token_data.owner_addr = '0x' + owner_addr
for k in token_data.attrs:
if getattr(opt,k,None):
setattr( token_data, k, getattr(opt,k) )
code = code.replace( f'<{k.upper()}>', str(getattr(token_data,k)) )
return code
def check_version():
res = run(['solc','--version'],stdout=PIPE).stdout.decode()
ver = re.search(r'Version:\s*(.*)',res).group(1)
msg(f'Installed solc version: {ver}')
if not re.search(r'{}\b'.format(solc_version_pat),ver):
ydie(1,f'Incorrect Solidity compiler version (need version {solc_version_pat})')
def check_solc_version():
"""
The output is used by other programs, so write to stdout only
"""
try:
cp = run(['solc','--version'],check=True,stdout=PIPE)
except:
msg('solc missing or could not be executed') # this must go to stderr
return False
if cp.returncode != 0:
Msg('solc exited with error')
return False
line = cp.stdout.decode().splitlines()[1]
version_str = re.sub(r'Version:\s*','',line)
m = re.match(r'(\d+)\.(\d+)\.(\d+)',version_str)
if not m:
Msg(f'Unrecognized solc version string: {version_str}')
return False
from semantic_version import Version,NpmSpec
version = Version('{}.{}.{}'.format(*m.groups()))
if version in NpmSpec(req_solc_ver_pat):
Msg(str(version))
return True
else:
Msg(f'solc version ({version_str}) does not match requirement ({req_solc_ver_pat})')
return False
def compile_code(code):
check_version()
cmd = ['solc','--optimize','--bin','--overwrite']
if not opt.stdout:
cmd += ['--output-dir', opt.outdir or '.']
@ -218,9 +234,33 @@ def compile_code(code):
else:
vmsg(out)
src = create_src(code_in)
out = compile_code(src)
if opt.stdout:
print(json.dumps(out))
if __name__ == '__main__':
msg('Contract successfully compiled')
cmd_args = opts.init(opts_data)
if opt.check_solc_version:
sys.exit(0 if check_solc_version() else 1)
from mmgen.protocol import init_proto_from_opts
proto = init_proto_from_opts()
if not proto.coin in ('ETH','ETC'):
die(1,'--coin option must be ETH or ETC')
if not len(cmd_args) == 1:
opts.usage()
owner_addr = cmd_args[0]
from mmgen.obj import is_coin_addr
if not is_coin_addr( proto, owner_addr.lower() ):
die(1,f'{owner_addr}: not a valid {proto.coin} coin address')
out = compile_code(
create_src( solidity_code_template, token_data, owner_addr )
)
if opt.stdout:
print(json.dumps(out))
msg('Contract successfully compiled')

View file

@ -45,32 +45,30 @@ amt2 = '888.111122223333444455'
openethereum_key_fn = 'openethereum.devkey'
# Token sends require varying amounts of gas, depending on compiler version
def get_solc_ver():
try: cp = run(['solc','--version'],stdout=PIPE)
except: return None
tested_solc_ver = '0.5.3'
if cp.returncode:
return None
def check_solc_ver():
cmd = 'scripts/create-token.py --check-solc-version'
try:
cp = run(cmd.split(),check=False,stdout=PIPE)
except Exception as e:
rdie(2,f'Unable to execute {cmd!r}: {e}')
res = cp.stdout.decode().strip()
if cp.returncode == 0:
omsg(
orange(f'Found supported solc version {res}') if res == tested_solc_ver else
yellow(f'WARNING: solc version ({res}) does not match tested version ({tested_solc_ver})')
)
return True
else:
omsg(yellow(res + '\nUsing precompiled contract data'))
return False
line = cp.stdout.decode().splitlines()[1]
m = re.search(r'Version:\s*(\d+)\.(\d+)\.(\d+)',line)
return '.'.join(m.groups()) if m else None
solc_ver = get_solc_ver()
if solc_ver == '0.5.1':
vbal1 = '1.2288337'
vbal1a = 'TODO'
vbal2 = '99.997085083'
vbal3 = '1.23142165'
vbal4 = '127.0287837'
else: # 0.5.3 or precompiled 0.5.3
vbal1 = '1.2288487'
vbal1a = '1.22627465'
vbal2 = '99.997092733'
vbal3 = '1.23142915'
vbal4 = '127.0287987'
vbal1 = '1.2288487'
vbal9 = '1.22627465'
vbal2 = '99.997092733'
vbal3 = '1.23142915'
vbal4 = '127.0287987'
bals = {
'1': [ ('98831F3A:E:1','123.456')],
@ -122,7 +120,7 @@ token_bals = {
('98831F3A:E:12','0',vbal2),
('98831F3A:E:13','1.23456','0'),
(burn_addr + '\s+Non-MMGen',amt2,amt1)],
'7': [ ('98831F3A:E:11','67.444317776666555545',vbal1a,'a2'),
'7': [ ('98831F3A:E:11','67.444317776666555545',vbal9,'a2'),
('98831F3A:E:12','43.21',vbal2),
('98831F3A:E:13','1.23456','0'),
(burn_addr + '\s+Non-MMGen',amt2,amt1)]
@ -143,7 +141,6 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
passthru_opts = ('coin','daemon_id','http_timeout')
extra_spawn_args = ['--regtest=1']
tmpdir_nums = [22]
solc_vers = ('0.5.1','0.5.3') # 0.5.1: Raspbian Stretch, 0.5.3: Ubuntu Bionic
color = True
cmd_group = (
('setup', 'dev mode tests for coin {} (start daemon)'.format(coin)),
@ -311,10 +308,13 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
def __init__(self,trunner,cfgs,spawn):
TestSuiteBase.__init__(self,trunner,cfgs,spawn)
if trunner == None:
return
from mmgen.protocol import init_proto
self.proto = init_proto(g.coin,network='regtest')
from mmgen.daemon import CoinDaemon
self.rpc_port = CoinDaemon(proto=self.proto,test_suite=True).rpc_port
self.using_solc = check_solc_ver()
@property
def eth_args(self):
@ -322,16 +322,13 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
async def setup(self):
self.spawn('',msg_only=True)
if solc_ver in self.solc_vers:
imsg('Found solc version {}'.format(solc_ver))
else:
imsg('Solc compiler {}. Using precompiled contract data'.format(
'version {} not supported by test suite'.format(solc_ver)
if solc_ver else 'not found' ))
if not self.using_solc:
srcdir = os.path.join(self.tr.repo_root,'test','ref','ethereum','bin')
from shutil import copytree
for d in ('mm1','mm2'):
copytree(os.path.join(srcdir,d),os.path.join(self.tmpdir,d))
if not opt.no_daemon_autostart:
if g.daemon_id == 'geth':
self.geth_setup()
@ -342,6 +339,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
from mmgen.rpc import rpc_init
rpc = await rpc_init(self.proto)
imsg('Daemon: {} v{}'.format(rpc.daemon.coind_name,rpc.daemon_version_str))
return 'ok'
def geth_setup(self):
@ -662,8 +660,8 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
def token_compile(self,token_data={}):
odir = joinpath(self.tmpdir,token_data['symbol'].lower())
if not solc_ver:
imsg('Using precompiled contract data in {}'.format(odir))
if not self.using_solc:
imsg(f'Using precompiled contract data in {odir}')
return 'skip' if os.path.exists(odir) else False
self.spawn('',msg_only=True)
cmd_args = ['--{}={}'.format(k,v) for k,v in list(token_data.items())]
@ -679,7 +677,8 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
imsg("Executing: {}".format(' '.join(cmd)))
cp = run(cmd,stdout=DEVNULL,stderr=PIPE)
if cp.returncode != 0:
rdie(2,'solc failed with the following output: {}'.format(cp.stderr))
rmsg('solc failed with the following output:')
ydie(2,cp.stderr.decode())
imsg("ERC20 token '{}' compiled".format(token_data['symbol']))
return 'ok'
@ -691,12 +690,18 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
token_data = { 'name':'MMGen Token 2', 'symbol':'MM2', 'supply':10**18, 'decimals':10 }
return self.token_compile(token_data)
async def get_exec_status(self,txid):
async def get_tx_receipt(self,txid):
from mmgen.tx import MMGenTX
tx = MMGenTX.New(proto=self.proto)
from mmgen.rpc import rpc_init
tx.rpc = await rpc_init(self.proto)
return await tx.get_exec_status(txid,True)
res = await tx.get_receipt(txid)
imsg(f'Gas sent: {res.gas_sent.hl():<9} {(res.gas_sent*res.gas_price).hl2(encl="()")}')
imsg(f'Gas used: {res.gas_used.hl():<9} {(res.gas_used*res.gas_price).hl2(encl="()")}')
imsg(f'Gas price: {res.gas_price.hl2()}')
if res.gas_used == res.gas_sent:
omsg(yellow(f'Warning: all gas was used!'))
return res
async def token_deploy(self,num,key,gas,mmgen_cmd='txdo',tx_fee='8G'):
keyfile = joinpath(self.tmpdir,openethereum_key_fn)
@ -724,7 +729,8 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
quiet = mmgen_cmd == 'txdo' or not g.debug,
bogus_send=False)
addr = strip_ansi_escapes(t.expect_getend('Contract address: '))
assert (await self.get_exec_status(txid)) != 0, f'Contract {num}:{key} failed to execute. Aborting'
if (await self.get_tx_receipt(txid)).status == 0:
die(2,f'Contract {num}:{key} failed to execute. Aborting')
if key == 'Token':
self.write_to_tmpfile( f'token_addr{num}', addr+'\n' )
imsg(f'\nToken MM{num} deployed!')
@ -771,7 +777,8 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
dfl_privkey,
start_gas = ETHAmt(60000,'wei'),
gasPrice = ETHAmt(8,'Gwei') )
assert (await self.get_exec_status(txid)) != 0,'Transfer of token funds failed. Aborting'
if (await self.get_tx_receipt(txid)).status == 0:
die(2,'Transfer of token funds failed. Aborting')
async def show_bals(rpc):
for i in range(2):