Browse Source

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

The MMGen Project 3 years ago
parent
commit
4742255ad2
4 changed files with 163 additions and 100 deletions
  1. 14 6
      mmgen/altcoins/eth/tx.py
  2. 8 0
      mmgen/obj.py
  3. 93 53
      scripts/create-token.py
  4. 48 41
      test/test_py_d/ts_ethdev.py

+ 14 - 6
mmgen/altcoins/eth/tx.py

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

+ 8 - 0
mmgen/obj.py

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

+ 93 - 53
scripts/create-token.py

@@ -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__':
+
+	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')
+	msg('Contract successfully compiled')

+ 48 - 41
test/test_py_d/ts_ethdev.py

@@ -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
-
-	if cp.returncode:
-		return None
-
-	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'
+tested_solc_ver = '0.5.3'
+
+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
+
+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):