Browse Source

Full Ethereum Classic (ETC) + ERC20 token support

As with ETH transacting support, this feature is in beta phase

All key functionality works, for both ETC and ETC tokens:
- Tracking wallet: getbalance, twview, listaddresses
- TX create, send, sign
- TX bumping
- ERC20 token creation, deployment

For usage details, see https://github.com/mmgen/mmgen/wiki/Altcoin-and-Forkcoin-Support

Differences from ETH:
- Start Parity with --jsonrpc-port=8555 (or --ports-shift=10) and --chain=classic
- Launch MMGen commands with --coin=etc
MMGen 6 years ago
parent
commit
d4eb8f6ac0

+ 1 - 1
mmgen/altcoins/eth/tw.py

@@ -33,7 +33,7 @@ class EthereumTrackingWallet(TrackingWallet):
 	desc = 'Ethereum tracking wallet'
 	caps = ()
 
-	data_dir = os.path.join(g.altcoin_data_dir,'eth',g.proto.data_subdir)
+	data_dir = os.path.join(g.altcoin_data_dir,g.coin.lower(),g.proto.data_subdir)
 	tw_file = os.path.join(data_dir,'tracking-wallet.json')
 
 	def __init__(self,mode='r'):

+ 1 - 1
mmgen/altcoins/eth/tx.py

@@ -321,7 +321,7 @@ class EthereumMMGenTX(MMGenTX):
 
 	def is_in_wallet(self):
 		d = g.rpch.eth_getTransactionReceipt('0x'+self.coin_txid)
-		if d and 'blockNumber' in d:
+		if d and 'blockNumber' in d and d['blockNumber'] is not None:
 			return 1 + int(g.rpch.eth_blockNumber(),16) - int(d['blockNumber'],16)
 		return False
 

+ 12 - 6
mmgen/main_autosign.py

@@ -132,7 +132,11 @@ def check_daemons_running():
 				ydie(1,fs.format(coin,g.proto.rpc_port))
 
 def get_wallet_files():
-	wfs = filter(lambda x: x[-6:] == '.mmdat',os.listdir(wallet_dir))
+	m = "Cannot open wallet directory '{}'. Did you run 'mmgen-autosign setup'?"
+	try: dlist = os.listdir(wallet_dir)
+	except: die(1,m.format(wallet_dir))
+
+	wfs = filter(lambda x: x[-6:] == '.mmdat',dlist)
 	if not wfs:
 		die(1,'No wallet files present!')
 	return [os.path.join(wallet_dir,w) for w in wfs]
@@ -143,8 +147,10 @@ def do_mount():
 			msg('Mounting '+mountpoint)
 	try:
 		ds = os.stat(tx_dir)
-		assert S_ISDIR(ds.st_mode)
-		assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR
+		m1 = "'{}' is not a directory!"
+		m2 = "'{}' is not read/write for this user!"
+		assert S_ISDIR(ds.st_mode),m1.format(tx_dir)
+		assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR,m2.format(tx_dir)
 	except:
 		die(1,'{} missing, or not read/writable by user!'.format(tx_dir))
 
@@ -168,15 +174,15 @@ def sign_tx_file(txfile):
 				init_coin(tmp_tx.coin)
 
 		if hasattr(g.proto,'chain_name'):
-			m = 'Protocol chain name ({}) does not match chain name from TX file ({})'
-			assert tmp_tx.chain == g.proto.chain_name, m.format(tmp_tx.chain,g.proto.chain_name)
+			m = 'Chains do not match! tx file: {}, proto: {}'
+			assert tmp_tx.chain == g.proto.chain_name,m.format(tmp_tx.chain,g.proto.chain_name)
 
 		g.chain = tmp_tx.chain
 		g.token = tmp_tx.dcoin
 		g.dcoin = tmp_tx.dcoin or g.coin
 
 		reload(sys.modules['mmgen.tx'])
-		if g.coin == 'ETH':
+		if g.proto.base_coin == 'ETH':
 			reload(sys.modules['mmgen.altcoins.eth.tx'])
 
 		tx = mmgen.tx.MMGenTX(txfile)

+ 9 - 5
mmgen/protocol.py

@@ -343,14 +343,18 @@ class EthereumProtocol(DummyWIF,BitcoinProtocol):
 
 class EthereumTestnetProtocol(EthereumProtocol):
 	data_subdir = 'testnet'
-	rpc_port    = 8547 # start Parity with --ports-shift=2
+	rpc_port    = 8547 # start Parity with --jsonrpc-port=8547 or --ports-shift=2
 	chain_name  = 'kovan'
 
 class EthereumClassicProtocol(EthereumProtocol):
-	name   = 'ethereum_classic'
-	mmcaps = ('key','addr')
+	name       = 'ethereumClassic'
+	class_pfx  = 'Ethereum'
+	rpc_port   = 8555 # start Parity with --jsonrpc-port=8555 or --ports-shift=10
+	chain_name = 'ethereum_classic' # chain_id 0x3d (61)
 
-class EthereumClassicTestnetProtocol(EthereumClassicProtocol): pass
+class EthereumClassicTestnetProtocol(EthereumClassicProtocol):
+	rpc_port   = 8557 # start Parity with --jsonrpc-port=8557 or --ports-shift=12
+	chain_name = 'classic-testnet' # aka Morden, chain_id 0x3e (62) (UNTESTED)
 
 class ZcashProtocol(BitcoinProtocolAddrgen):
 	name         = 'zcash'
@@ -440,7 +444,7 @@ class CoinProtocol(MMGenObject):
 		'bch': (BitcoinCashProtocol,BitcoinCashTestnetProtocol,None),
 		'ltc': (LitecoinProtocol,LitecoinTestnetProtocol,None),
 		'eth': (EthereumProtocol,EthereumTestnetProtocol,None),
-		'etc': (EthereumClassicProtocol,EthereumClassicTestnetProtocol,2),
+		'etc': (EthereumClassicProtocol,EthereumClassicTestnetProtocol,None),
 		'zec': (ZcashProtocol,ZcashTestnetProtocol,2),
 		'xmr': (MoneroProtocol,MoneroTestnetProtocol,None)
 	}

+ 1 - 1
mmgen/tx.py

@@ -1126,7 +1126,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
 				import re
 				d = literal_eval(re.sub(r"[A-Za-z]+?\(('.+?')\)",r'\1',raw_data))
 			assert type(d) == list,'{} data not a list!'.format(desc)
-			if not (desc == 'outputs' and g.coin == 'ETH'): # ETH txs can have no outputs
+			if not (desc == 'outputs' and g.proto.base_coin == 'ETH'): # ETH txs can have no outputs
 				assert len(d),'no {}!'.format(desc)
 			for e in d: e['amt'] = g.proto.coin_amt(e['amt'])
 			io,io_list = (

+ 6 - 5
mmgen/util.py

@@ -849,7 +849,7 @@ def rpc_init_parity():
 
 	if not g.daemon_version: # First call
 		g.daemon_version = g.rpch.parity_versionInfo()['version'] # fail immediately if daemon is geth
-		g.chain = g.rpch.parity_chain()
+		g.chain = g.rpch.parity_chain().replace(' ','_')
 		if g.token:
 			(g.token,g.dcoin) = resolve_token_arg(g.token)
 
@@ -923,10 +923,11 @@ def format_par(s,indent=0,width=80,as_list=False):
 # module loading magic for tx.py and tw.py
 def altcoin_subclass(cls,mod_id,cls_name):
 	if cls.__name__ != cls_name: return cls
-	pn = capfirst(g.proto.name)
-	tn = 'Token' if g.token else ''
-	e1 = 'from mmgen.altcoins.{}.{} import {}{}{}'.format(g.coin.lower(),mod_id,pn,tn,cls_name)
-	e2 = 'cls = {}{}{}'.format(pn,tn,cls_name)
+	mod_dir = g.proto.base_coin.lower()
+	pname = g.proto.class_pfx if hasattr(g.proto,'class_pfx') else capfirst(g.proto.name)
+	tname = 'Token' if g.token else ''
+	e1 = 'from mmgen.altcoins.{}.{} import {}{}{}'.format(mod_dir,mod_id,pname,tname,cls_name)
+	e2 = 'cls = {}{}{}'.format(pname,tname,cls_name)
 	try: exec e1; exec e2; return cls
 	except ImportError: return cls
 

+ 1 - 1
scripts/create-token.py

@@ -24,8 +24,8 @@ opts_data = lambda: {
 """.format(d=decimals,n=name,s=symbol,t=supply)
 }
 
-g.coin = 'ETH'
 cmd_args = opts.init(opts_data)
+assert g.coin in ('ETH','ETC'),'--coin option must be set to ETH or ETC'
 
 if not len(cmd_args) == 1 or not is_coin_addr(cmd_args[0]):
 	opts.usage()

+ 4 - 2
scripts/test-release.sh

@@ -208,11 +208,13 @@ t_monero=(
 f_monero='Monero tests completed'
 
 i_eth='Ethereum'
-s_eth='Testing transaction and tracking wallet operations for Ethereum'
+s_eth='Testing transaction and tracking wallet operations for Ethereum and Ethereum Classic'
 t_eth=(
 	"$test_py -On --coin=eth ref_tx_chk"
 	"$test_py -On --coin=eth --testnet=1 ref_tx_chk"
-	"$test_py -On ethdev"
+	"$test_py -On --coin=etc ref_tx_chk"
+	"$test_py -On --coin=eth ethdev"
+	"$test_py -On --coin=etc ethdev"
 )
 f_eth='Ethereum tests completed'
 

+ 6 - 0
test/ref/ethereum_classic/ED3848-ETC[1.2345,40000].rawtx

@@ -0,0 +1,6 @@
+aa0148
+ETC ETHEREUM_CLASSIC ED3848 1.2345 20181002_000000 6670000
+{"nonce": "0", "chainId": "61", "from": "1a6acbef8c38f52f20d04ecded2992b04d8608d7", "startGas": "0.000000000000021", "to": "61d7cba023f6131df1ade460880fee75df3987c4", "data": "", "amt": "1.2345", "gasPrice": "0.00000004"}
+[{'confs': 0, 'label': u'', 'mmid': '98831F3A:E:1', 'amt': '123.456', 'addr': '1a6acbef8c38f52f20d04ecded2992b04d8608d7'}]
+[{'mmid': '98831F3A:E:2', 'amt': '1.2345', 'addr': '61d7cba023f6131df1ade460880fee75df3987c4'}]
+TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos

+ 45 - 20
test/test.py

@@ -163,13 +163,12 @@ opt.popen_spawn = True # popen has issues, so use popen_spawn always
 
 if not opt.system: os.environ['PYTHONPATH'] = repo_root
 
-ref_subdir = '' if g.proto.base_coin == 'BTC' else g.proto.name
+ref_subdir = '' if g.proto.base_coin == 'BTC' else 'ethereum_classic' if g.coin == 'ETC' else g.proto.name
 altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin
 tn_ext = ('','.testnet')[g.testnet]
 
 coin_sel = g.coin.lower()
-# if g.coin == 'B2X': coin_sel = 'btc'
-if g.coin == 'ETH': coin_sel = 'btc' # TODO
+if g.coin.lower() in ('eth','etc'): coin_sel = 'btc'
 
 fork       = {'bch':'btc','btc':'btc','ltc':'ltc'}[coin_sel]
 tx_fee     = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[coin_sel]
@@ -585,7 +584,8 @@ cfgs = {
 			'eth': ('88FEFD-ETH[23.45495,40000].rawtx',
 					'B472BD-ETH[23.45495,40000].testnet.rawtx'),
 			'erc20': ('5881D2-MM1[1.23456,50000].rawtx',
-					'6BDB25-MM1[1.23456,50000].testnet.rawtx')
+					'6BDB25-MM1[1.23456,50000].testnet.rawtx'),
+			'etc': ('ED3848-ETC[1.2345,40000].rawtx','')
 		},
 		'ic_wallet':       u'98831F3A-5482381C-18460FB1[256,1].mmincog',
 		'ic_wallet_hex':   u'98831F3A-1630A9F2-870376A9[256,1].mmincox',
@@ -872,7 +872,7 @@ cmd_group['regtest_split'] = (
 )
 
 cmd_group['ethdev'] = (
-	('ethdev_setup',               'Ethereum Parity dev mode tests (start parity)'),
+	('ethdev_setup',               'Ethereum Parity dev mode tests for coin {} (start parity)'.format(g.coin)),
 	('ethdev_addrgen',             'generating addresses'),
 	('ethdev_addrimport',          'importing addresses'),
 	('ethdev_addrimport_dev_addr', "importing Parity dev address 'Ox00a329c..'"),
@@ -934,6 +934,8 @@ cmd_group['ethdev'] = (
 	('ethdev_token_txsign1',       'signing the transaction'),
 	('ethdev_token_txsend1',       'sending the transaction'),
 
+	('ethdev_token_twview1',       'viewing token tracking wallet'),
+
 	('ethdev_token_txcreate2',     'creating a token transaction (to burn address)'),
 	('ethdev_token_txbump',        'bumping the transaction fee'),
 
@@ -942,8 +944,8 @@ cmd_group['ethdev'] = (
 
 	('ethdev_del_dev_addr',        "deleting the dev address"),
 
-	('ethdev_bal2',                'the ETH balance'),
-	('ethdev_bal2_getbalance',     'the ETH balance (getbalance)'),
+	('ethdev_bal2',                'the {} balance'.format(g.coin)),
+	('ethdev_bal2_getbalance',     'the {} balance (getbalance)'.format(g.coin)),
 
 	('ethdev_addrimport_token_burn_addr',"importing the token burn address"),
 
@@ -955,7 +957,7 @@ cmd_group['ethdev'] = (
 	('ethdev_txsend_noamt',       'sending the transaction'),
 
 	('ethdev_token_bal2',          'the token balance'),
-	('ethdev_bal3',                'the ETH balance'),
+	('ethdev_bal3',                'the {} balance'.format(g.coin)),
 
 	('ethdev_token_txcreate_noamt', 'creating a token transaction (full amount send)'),
 	('ethdev_token_txsign_noamt',   'signing the transaction'),
@@ -967,7 +969,7 @@ cmd_group['ethdev'] = (
 )
 
 cmd_group['autosign'] = (
-	('autosign', 'transaction autosigning (BTC,BCH,LTC)'),
+	('autosign', 'transaction autosigning (BTC,BCH,LTC,ETH,ETC)'),
 )
 
 cmd_group['ref_alt'] = (
@@ -2319,14 +2321,20 @@ class MMGenTestSuite(object):
 
 	def autosign(self,name): # tests everything except device detection, mount/unmount
 		if skip_for_win(): return
-		fdata = (('btc',''),('bch',''),('ltc','litecoin'),('eth','ethereum'),('erc20','ethereum'))
+		fdata = (	('btc',''),
+					('bch',''),
+					('ltc','litecoin'),
+					('eth','ethereum'),
+					('erc20','ethereum'),
+					('etc','ethereum_classic'))
 		tfns  = [cfgs['8']['ref_tx_file'][c][1] for c,d in fdata] + \
 				[cfgs['8']['ref_tx_file'][c][0] for c,d in fdata]
 		tfs = [os.path.join(ref_dir,d[1],fn) for d,fn in zip(fdata+fdata,tfns)]
 		try: os.mkdir(os.path.join(cfg['tmpdir'],'tx'))
 		except: pass
 		for f,fn in zip(tfs,tfns):
-			shutil.copyfile(f,os.path.join(cfg['tmpdir'],'tx',fn))
+			if fn: # use empty fn to skip file
+				shutil.copyfile(f,os.path.join(cfg['tmpdir'],'tx',fn))
 		# make a bad tx file
 		with open(os.path.join(cfg['tmpdir'],'tx','bad.rawtx'),'w') as f:
 			f.write('bad tx data')
@@ -2347,7 +2355,7 @@ class MMGenTestSuite(object):
 		t.ok()
 
 		t = MMGenExpect(name,'mmgen-autosign',opts+['wait'],extra_desc='(sign)')
-		t.expect('10 transactions signed')
+		t.expect('11 transactions signed')
 		t.expect('1 transaction failed to sign')
 		t.expect('Waiting.')
 		t.kill(2)
@@ -2596,7 +2604,9 @@ class MMGenTestSuite(object):
 #		self.txcreate_common(name,sources=['8'])
 
 	def ref_tx_chk(self,name):
-		tf = os.path.join(ref_dir,ref_subdir,cfg['ref_tx_file'][g.coin.lower()][bool(tn_ext)])
+		fn = cfg['ref_tx_file'][g.coin.lower()][bool(tn_ext)]
+		if not fn: return
+		tf = os.path.join(ref_dir,ref_subdir,fn)
 		wf = dfl_words
 		write_to_tmpfile(cfg,pwfile,cfg['wpasswd'])
 		pf = get_tmpfile_fn(cfg,pwfile)
@@ -3213,7 +3223,7 @@ class MMGenTestSuite(object):
 
 	def ethdev_txcreate(self,name,args=[],menu=[],acct='1',non_mmgen_inputs=0,
 						interactive_fee='50G',
-						fee_res='0.00105 ETH (50 gas price in Gwei)',
+						fee_res='0.00105 {} (50 gas price in Gwei)'.format(g.coin),
 						fee_desc = 'gas price'):
 		t = MMGenExpect(name,'mmgen-txcreate', eth_args() + ['-B'] + args)
 		t.expect(r"'q'=quit view, .*?:.",'p', regex=True)
@@ -3274,7 +3284,7 @@ class MMGenTestSuite(object):
 	def ethdev_txcreate4(self,name):
 		args = ['98831F3A:E:2,23.45495']
 		interactive_fee='40G'
-		fee_res='0.00084 ETH (40 gas price in Gwei)'
+		fee_res='0.00084 {} (40 gas price in Gwei)'.format(g.coin)
 		return self.ethdev_txcreate(name,args=args,acct='1',non_mmgen_inputs=0,
 					interactive_fee=interactive_fee,fee_res=fee_res)
 
@@ -3329,7 +3339,7 @@ class MMGenTestSuite(object):
 
 	def init_ethdev_common(self):
 		g.testnet = True
-		init_coin('eth')
+		init_coin(g.coin)
 		g.proto.rpc_port = 8549
 		rpc_init()
 
@@ -3338,7 +3348,7 @@ class MMGenTestSuite(object):
 		cmd_args = ['--{}={}'.format(k,v) for k,v in token_data.items()]
 		silence()
 		imsg("Compiling solidity token contract '{}' with 'solc'".format(token_data['symbol']))
-		cmd = ['scripts/create-token.py','--outdir='+cfg['tmpdir']] + cmd_args + [eth_addr]
+		cmd = ['scripts/create-token.py','--coin='+g.coin,'--outdir='+cfg['tmpdir']] + cmd_args + [eth_addr]
 		imsg("Executing: {}".format(' '.join(cmd)))
 		subprocess.check_output(cmd)
 		imsg("ERC20 token '{}' compiled".format(token_data['symbol']))
@@ -3386,7 +3396,7 @@ class MMGenTestSuite(object):
 	def ethdev_token_deploy1c(self,name): self.ethdev_token_deploy(name,num=1,key='Token',gas=1100000,tx_fee='7G')
 
 	def ethdev_tx_status2(self,name):
-		self.ethdev_tx_status(name,ext='ETH[0,7000].sigtx',expect_str='successfully executed')
+		self.ethdev_tx_status(name,ext=g.coin+'[0,7000].sigtx',expect_str='successfully executed')
 
 	def ethdev_token_deploy2a(self,name): self.ethdev_token_deploy(name,num=2,key='SafeMath',gas=200000)
 	def ethdev_token_deploy2b(self,name): self.ethdev_token_deploy(name,num=2,key='Owned',gas=250000)
@@ -3398,7 +3408,7 @@ class MMGenTestSuite(object):
 	def ethdev_token_transfer_funds(self,name):
 		MMGenExpect(name,'',msg_only=True)
 		sid = cfgs['8']['seed_id']
-		cmd = lambda i: ['mmgen-tool','--coin=eth','gen_addr','{}:E:{}'.format(sid,i),'wallet='+dfl_words]
+		cmd = lambda i: ['mmgen-tool','--coin='+g.coin,'gen_addr','{}:E:{}'.format(sid,i),'wallet='+dfl_words]
 		silence()
 		usr_addrs = [subprocess.check_output(cmd(i),stderr=sys.stderr).strip() for i in 11,21]
 		self.init_ethdev_common()
@@ -3443,6 +3453,19 @@ class MMGenTestSuite(object):
 	def ethdev_token_txsend1(self,name):
 		self.ethdev_token_txsend(name,ext='1.23456,50000].sigtx',token='mm1')
 
+	def ethdev_twview(self,name,args,expect_str):
+		t = MMGenExpect(name,'mmgen-tool', eth_args() + args + ['twview'])
+		t.expect(expect_str,regex=True)
+		t.read()
+		t.ok()
+
+	bal_corr = Decimal('0.0000032') # gas use varies for token sends!
+	def ethdev_token_twview1(self,name):
+		ebal = Decimal('1.2314236')
+		if g.coin == 'ETC': ebal += self.bal_corr
+		s = '98831F3A:E:11\s+998.76544\s+' + str(ebal)
+		return self.ethdev_twview(name,args=['--token=mm1'],expect_str=s)
+
 	def ethdev_token_txcreate2(self,name):
 		return self.ethdev_token_txcreate(name,args=[eth_burn_addr+','+eth_amt2],token='mm1')
 
@@ -3466,7 +3489,9 @@ class MMGenTestSuite(object):
 		self.ethdev_bal(name,expect_str=r'deadbeef.* 999999.12345689012345678')
 
 	def ethdev_bal2_getbalance(self,name,t_non_mmgen='',t_mmgen=''):
-		self.ethdev_bal_getbalance(name,t_non_mmgen='999999.12345689012345678',t_mmgen='127.0287876')
+		ebal = Decimal('127.0287876')
+		if g.coin == 'ETC': ebal += self.bal_corr
+		self.ethdev_bal_getbalance(name,t_non_mmgen='999999.12345689012345678',t_mmgen=str(ebal))
 
 	def ethdev_token_bal(self,name,expect_str):
 		t = MMGenExpect(name,'mmgen-tool', eth_args() + ['--token=mm1','twview','wide=1'])