From 4742255ad2c8826ad53e45f0caea9a555cf25368 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Fri, 3 Sep 2021 20:17:25 +0000 Subject: [PATCH] test.py ethdev, scripts/create-token.py: cleanups --- mmgen/altcoins/eth/tx.py | 20 +++-- mmgen/obj.py | 8 ++ scripts/create-token.py | 146 +++++++++++++++++++++++------------- test/test_py_d/ts_ethdev.py | 85 +++++++++++---------- 4 files changed, 161 insertions(+), 98 deletions(-) diff --git a/mmgen/altcoins/eth/tx.py b/mmgen/altcoins/eth/tx.py index be7032cb..2d33077a 100755 --- a/mmgen/altcoins/eth/tx.py +++ b/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' diff --git a/mmgen/obj.py b/mmgen/obj.py index 6155a234..c89649d8 100755 --- a/mmgen/obj.py +++ b/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') diff --git a/scripts/create-token.py b/scripts/create-token.py index e885b475..42ca75dd 100755 --- a/scripts/create-token.py +++ b/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] ', - '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') diff --git a/test/test_py_d/ts_ethdev.py b/test/test_py_d/ts_ethdev.py index e2111d46..2c68d822 100755 --- a/test/test_py_d/ts_ethdev.py +++ b/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 +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):