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
This commit is contained in:
The MMGen Project 2018-10-02 18:09:48 +00:00
commit d4eb8f6ac0
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
10 changed files with 86 additions and 42 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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