From d4eb8f6ac0c1992d0b2cc0a270b171e77bfb121b Mon Sep 17 00:00:00 2001 From: MMGen Date: Tue, 2 Oct 2018 18:09:48 +0000 Subject: [PATCH] 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/altcoins/eth/tw.py | 2 +- mmgen/altcoins/eth/tx.py | 2 +- mmgen/main_autosign.py | 18 +++-- mmgen/protocol.py | 14 ++-- mmgen/tx.py | 2 +- mmgen/util.py | 11 ++-- scripts/create-token.py | 2 +- scripts/test-release.sh | 6 +- .../ED3848-ETC[1.2345,40000].rawtx | 6 ++ test/test.py | 65 +++++++++++++------ 10 files changed, 86 insertions(+), 42 deletions(-) create mode 100644 test/ref/ethereum_classic/ED3848-ETC[1.2345,40000].rawtx diff --git a/mmgen/altcoins/eth/tw.py b/mmgen/altcoins/eth/tw.py index 0255bd30..1656680d 100755 --- a/mmgen/altcoins/eth/tw.py +++ b/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'): diff --git a/mmgen/altcoins/eth/tx.py b/mmgen/altcoins/eth/tx.py index 75f683a2..66248742 100755 --- a/mmgen/altcoins/eth/tx.py +++ b/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 diff --git a/mmgen/main_autosign.py b/mmgen/main_autosign.py index a4c3b0b0..b4627ac0 100755 --- a/mmgen/main_autosign.py +++ b/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) diff --git a/mmgen/protocol.py b/mmgen/protocol.py index 51b15026..2aa66cd1 100755 --- a/mmgen/protocol.py +++ b/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) } diff --git a/mmgen/tx.py b/mmgen/tx.py index c5f48e5f..b5bcdff0 100755 --- a/mmgen/tx.py +++ b/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 = ( diff --git a/mmgen/util.py b/mmgen/util.py index 47a7e323..c9df9534 100755 --- a/mmgen/util.py +++ b/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 diff --git a/scripts/create-token.py b/scripts/create-token.py index 66da9a84..20b37642 100755 --- a/scripts/create-token.py +++ b/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() diff --git a/scripts/test-release.sh b/scripts/test-release.sh index 9fc28c57..08a02336 100755 --- a/scripts/test-release.sh +++ b/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' diff --git a/test/ref/ethereum_classic/ED3848-ETC[1.2345,40000].rawtx b/test/ref/ethereum_classic/ED3848-ETC[1.2345,40000].rawtx new file mode 100644 index 00000000..7371ad9e --- /dev/null +++ b/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 diff --git a/test/test.py b/test/test.py index 4b964fd6..0e43c7df 100755 --- a/test/test.py +++ b/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'])