ERC20 token support (create/deploy, TX create/sign/send)

This feature is EXPERIMENTAL.  Until v0.9.9 is released, mainnet use is
strictly at your own risk!

To test on dev chain, run 'test/test.py -e ethdev'

To test on Kovan, add '--testnet=1' option to all commands below

Transaction example:

    Generate some ETH addresses with your default wallet:
    $ mmgen-addrgen --coin=eth 1-5

    Create an EOS token tracking wallet and import the addresses into it:
    $ mmgen-addrimport --coin=eth --token=86fa049857e0209aa7d9e616f7eb3b3b78ecfdb0 ABCDABCD-ETH[1-5].addrs

    Send 10+ EOS from an exchange or another wallet to address ABCDABCD:E:1

    Create a TX sending 10 EOS to address aabbccdd..., with change to ABCDABCD:E:2:
    $ mmgen-txcreate --coin=eth --token=eos aabbccddaabbccddaabbccddaabbccddaabbccdd,10 ABCDABCD:E:2

    On your offline machine, sign the TX:
    $ mmgen-txsign --coin=eth --token=eos ABC123-EOS[10,50000].rawtx

    On your online machine, send the TX:
    $ mmgen-txsend --coin=eth --token=eos ABC123-EOS[10,50000].sigtx

    View your EOS tracking wallet:
    $ mmgen-tool --coin=eth --token=eos twview

Token creation/deployment example:

    Install the Solidity compiler ('solc') on your system.

    Create a token 'MFT' with default parameters, owned by ddeeff... (ABCDABCD:E:1):
    $ scripts/create-token.py --symbol=MFT --name='My First Token' ddeeffddeeffddeeffddeeffddeeffddeeffddee

    Deploy the token on the ETH blockchain:
    $ mmgen-txdo --coin=eth --tx-gas=200000 --contract-data=SafeMath.bin
    $ mmgen-txdo --coin=eth --tx-gas=250000 --contract-data=Owned.bin
    $ mmgen-txdo --coin=eth --tx-gas=1100000 --contract-data=Token.bin
    ...
    Token address: abcd1234abcd1234abcd1234abcd1234abcd1234

    Create an MFT token tracking wallet and import your ETH addresses into it:
    $ mmgen-addrimport --coin=eth --token=abcd1234abcd1234abcd1234abcd1234abcd1234 ABCDABCD-ETH[1-5].addrs

    View your MFT tracking wallet:
    $ mmgen-tool --coin=eth --token=mft twview
This commit is contained in:
The MMGen Project 2018-07-25 12:57:04 +00:00
commit 881d55993f
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
18 changed files with 718 additions and 4 deletions

View file

@ -11,6 +11,7 @@ include test/ref/monero/*
include scripts/bitcoind-walletunlock.py
include scripts/compute-file-chksum.py
include scripts/create-token.py
include scripts/deinstall.sh
include scripts/tx-old2new.py
include scripts/test-release.sh

134
mmgen/altcoins/eth/contract.py Executable file
View file

@ -0,0 +1,134 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2018 The MMGen Project <mmgen@tuta.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
altcoins.eth.contract: Ethereum contract and token classes for the MMGen suite
"""
from sha3 import keccak_256
from decimal import Decimal
from ethereum.transactions import Transaction
import rlp
from mmgen.globalvars import g
from mmgen.common import *
from mmgen.obj import MMGenObject,TokenAddr,CoinTxID,ETHAmt
from mmgen.util import msg,msg_r,pmsg,pdie
def parse_abi(s):
return [s[:8]] + [s[8+x*64:8+(x+1)*64] for x in range(len(s[8:])/64)]
def create_method_id(sig): return keccak_256(sig).hexdigest()[:8]
class Token(MMGenObject): # ERC20
# Test that token is in the blockchain by calling constructor w/o decimals arg
def __init__(self,addr,decimals=None):
self.addr = TokenAddr(addr)
if decimals is None:
ret_hex = self.do_call('decimals()')
try: decimals = int(ret_hex,16)
except: raise TokenNotInBlockchain,"token '{}' not in blockchain".format(addr)
self.base_unit = Decimal(10) ** -decimals
def transferdata2amt(self,data): # online
return ETHAmt(int(parse_abi(data)[-1],16) * self.base_unit)
def do_call(self,method_sig,method_args='',toUnit=False):
data = create_method_id(method_sig) + method_args
if g.debug:
msg('{}: {}'.format(method_sig,'\n '.join(parse_abi(data))))
ret = g.rpch.eth_call({ 'to': '0x'+self.addr, 'data': '0x'+data })
return int(ret,16) * self.base_unit if toUnit else ret
def balance(self,acct_addr):
return self.do_call('balanceOf(address)',acct_addr.rjust(64,'0'),toUnit=True)
def strip(self,s):
return ''.join(ch for ch in s if 32 <= ord(ch) <= 127).strip()
def total_supply(self): return self.do_call('totalSupply()',toUnit=True)
def decimals(self): return int(self.do_call('decimals()'),16)
def name(self): return self.strip(self.do_call('name()')[2:].decode('hex'))
def symbol(self): return self.strip(self.do_call('symbol()')[2:].decode('hex'))
def info(self):
fs = '{:15}{}\n' * 5
return fs.format('token address:',self.addr,
'token symbol:',self.symbol(),
'token name:',self.name(),
'decimals:',self.decimals(),
'total supply:',self.total_supply())
def code(self):
return g.rpch.eth_getCode('0x'+self.addr)[2:]
def transfer_from(self,from_addr,to_addr,amt,key,start_gas,gasPrice):
raise NotImplementedError,'method not implemented'
return self.transfer( from_addr,to_addr,amt,key,start_gas,gasPrice,
method_sig='transferFrom(address,address,uint256)',
from_addr2=from_addr)
def create_data(self,to_addr,amt,method_sig='transfer(address,uint256)',from_addr=None):
from_arg = from_addr.rjust(64,'0') if from_addr else ''
to_arg = to_addr.rjust(64,'0')
amt_arg = '{:064x}'.format(int(amt/self.base_unit))
return create_method_id(method_sig) + from_arg + to_arg + amt_arg
def txcreate( self,from_addr,to_addr,amt,start_gas,gasPrice,nonce=None,
method_sig='transfer(address,uint256)',from_addr2=None):
if nonce is None:
nonce = int(g.rpch.parity_nextNonce('0x'+from_addr),16)
data = self.create_data(to_addr,amt,method_sig=method_sig,from_addr=from_addr2)
return {'to': self.addr.decode('hex'),
'startgas': start_gas.toWei(),
'gasprice': gasPrice.toWei(),
'value': 0,
'nonce': nonce,
'data': data.decode('hex') }
def txsign(self,tx_in,key,from_addr,chain_id=None):
tx = Transaction(**tx_in)
if chain_id is None:
chain_id = int(g.rpch.parity_chainId(),16)
tx.sign(key,chain_id)
hex_tx = rlp.encode(tx).encode('hex')
coin_txid = CoinTxID(tx.hash.encode('hex'))
if tx.sender.encode('hex') != from_addr:
m = "Sender address '{}' does not match address of key '{}'!"
die(3,m.format(from_addr,tx.sender.encode('hex')))
if g.debug:
msg('{}'.format('\n '.join(parse_abi(data))))
pmsg(tx.to_dict())
return hex_tx,coin_txid
def txsend(self,hex_tx):
return g.rpch.eth_sendRawTransaction('0x'+hex_tx).replace('0x','',1)
def transfer( self,from_addr,to_addr,amt,key,start_gas,gasPrice,
method_sig='transfer(address,uint256)',
from_addr2=None,
return_data=False):
tx_in = self.txcreate( from_addr,to_addr,amt,
start_gas,gasPrice,
nonce=None,
method_sig=method_sig,
from_addr2=from_addr2)
(hex_tx,coin_txid) = self.txsign(tx_in,key,from_addr)
return self.txsend(hex_tx)

View file

@ -26,6 +26,7 @@ from mmgen.common import *
from mmgen.obj import ETHAmt,TwMMGenID,TwComment,TwLabel
from mmgen.tw import TrackingWallet,TwAddrList,TwUnspentOutputs
from mmgen.addr import AddrData
from mmgen.altcoins.eth.contract import Token
class EthereumTrackingWallet(TrackingWallet):
@ -140,6 +141,31 @@ class EthereumTrackingWallet(TrackingWallet):
m = "Address '{}' not found in '{}' section of tracking wallet"
return ('rpcfail',(None,2,m.format(coinaddr,self.data_root_desc())))
class EthereumTokenTrackingWallet(EthereumTrackingWallet):
def token_is_in_wallet(self,addr):
return addr in self.data['tokens']
def data_root_desc(self):
return 'token ' + Token(g.token).symbol()
@write_mode
def add_token(self,token):
msg("Adding token '{}' to tracking wallet.".format(token))
self.data['tokens'][token] = {}
def data_root(self): # create the token data root if necessary
if g.token not in self.data['tokens']:
self.add_token(g.token)
return self.data['tokens'][g.token]
def sym2addr(self,sym): # online
for addr in self.data['tokens']:
if Token(addr).symbol().upper() == sym.upper():
return addr
return None
# No unspent outputs with Ethereum, but naming must be consistent
class EthereumTwUnspentOutputs(TwUnspentOutputs):
disp_type = 'eth'
@ -168,6 +194,21 @@ Display options: show [D]ays, show [m]mgen addr, r[e]draw screen
'confirmations': 0, # TODO
}, TrackingWallet().sorted_list())
class EthereumTokenTwUnspentOutputs(EthereumTwUnspentOutputs):
disp_type = 'token'
prompt_fs = 'Total to spend: {} {}\n\n'
def get_display_precision(self): return 10
def get_addr_bal(self,addr):
return Token(g.token).balance(addr)
def get_unspent_data(self):
super(type(self),self).get_unspent_data()
for e in self.unspent:
e.amt2 = ETHAmt(int(g.rpch.eth_getBalance('0x'+e.addr),16),'wei')
class EthereumTwAddrList(TwAddrList):
def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels):
@ -197,6 +238,11 @@ class EthereumTwAddrList(TwAddrList):
def get_addr_balance(self,addr):
return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
class EthereumTokenTwAddrList(EthereumTwAddrList):
def get_addr_balance(self,addr):
return self.token.balance(addr)
from mmgen.tw import TwGetBalance
class EthereumTwGetBalance(TwGetBalance):
@ -221,6 +267,11 @@ class EthereumTwGetBalance(TwGetBalance):
def get_addr_balance(self,addr):
return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
class EthereumTokenTwGetBalance(EthereumTwGetBalance):
def get_addr_balance(self,addr):
return Token(g.token).balance(addr)
class EthereumAddrData(AddrData):
@classmethod
@ -229,3 +280,5 @@ class EthereumAddrData(AddrData):
tw = TrackingWallet().mmid_ordered_dict()
# emulate the output of RPC 'listaccounts' and 'getaddressesbyaccount'
return [(mmid+' '+d['comment'],[d['addr']]) for mmid,d in tw.items()]
class EthereumTokenAddrData(EthereumAddrData): pass

View file

@ -26,6 +26,8 @@ from mmgen.common import *
from mmgen.obj import *
from mmgen.tx import MMGenTX,MMGenBumpTX,MMGenSplitTX,DeserializedTX,mmaddr2coinaddr
from mmgen.altcoins.eth.contract import Token
class EthereumMMGenTX(MMGenTX):
desc = 'Ethereum transaction'
tx_gas = ETHAmt(21000,'wei') # an approximate number, used for fee estimation purposes
@ -346,6 +348,88 @@ class EthereumMMGenTX(MMGenTX):
self.add_blockcount()
return True
class EthereumTokenMMGenTX(EthereumMMGenTX):
desc = 'Ethereum token transaction'
tx_gas = ETHAmt(52000,'wei')
start_gas = ETHAmt(60000,'wei')
fee_is_approximate = True
def check_sufficient_funds(self,inputs_sum,sel_unspent):
eth_bal = ETHAmt(int(g.rpch.eth_getBalance('0x'+sel_unspent[0].addr),16),'wei')
if eth_bal == 0: # we don't know the fee yet
msg('This account has no ether to pay for the transaction fee!')
return False
if self.send_amt > inputs_sum:
msg(self.msg_low_coin.format(self.send_amt-inputs_sum,g.dcoin))
return False
return True
def final_inputs_ok_msg(self,change_amt):
m = u"Transaction leaves ≈{} {} and {} {} in the sender's account"
tbal = g.proto.coin_amt(Token(g.token).balance(self.inputs[0].addr) - self.outputs[0].amt)
chg = g.proto.coin_amt(change_amt)
return m.format(chg.hl(),g.coin,tbal.hl(),g.dcoin)
def get_change_amt(self): # here we know the fee
eth_bal = ETHAmt(int(g.rpch.eth_getBalance('0x'+self.inputs[0].addr),16),'wei')
return Decimal(eth_bal) - self.fee
def set_g_token(self):
g.dcoin = self.dcoin
if is_hex_str(self.hex): return # for txsend we can leave g.token uninitialized
d = json.loads(self.hex)
if g.token.upper() == self.dcoin:
g.token = d['token_addr']
elif g.token != d['token_addr']:
m1 = "'{p}': invalid --token parameter for {t} Ethereum token transaction file\n"
m2 = "Please use '--token={t}'"
die(1,(m1+m2).format(p=g.token,t=self.dcoin))
def make_txobj(self):
super(EthereumTokenMMGenTX,self).make_txobj()
t = Token(g.token)
o = t.txcreate( self.inputs[0].addr,
self.outputs[0].addr,
self.outputs[0].amt,
self.start_gas,
self.usr_rel_fee or self.fee_abs2rel(self.fee,to_unit='eth'))
self.txobj['token_addr'] = self.token_addr = t.addr
self.txobj['decimals'] = t.decimals()
def check_txfile_hex_data(self):
d = super(EthereumTokenMMGenTX,self).check_txfile_hex_data()
o = self.txobj
if self.check_sigs(): # online, from rlp
rpc_init()
o['token_addr'] = TokenAddr(o['to'])
o['amt'] = Token(o['token_addr']).transferdata2amt(o['data'])
else: # offline, from json
o['token_addr'] = TokenAddr(d['token_addr'])
o['decimals'] = Int(d['decimals'])
t = Token(o['token_addr'],o['decimals'])
self.data = o['data'] = t.create_data(o['to'],o['amt'])
def format_view_body(self,*args,**kwargs):
return 'Token: {d} {c}\n{r}'.format(
d=self.txobj['token_addr'].hl(),
c=blue('(' + g.dcoin + ')'),
r=super(EthereumTokenMMGenTX,self).format_view_body(*args,**kwargs))
def do_sign(self,d,wif,tx_num_str):
d = self.txobj
msg_r('Signing transaction{}...'.format(tx_num_str))
try:
t = Token(d['token_addr'],decimals=d['decimals'])
tx_in = t.txcreate(d['from'],d['to'],d['amt'],self.start_gas,d['gasPrice'],nonce=d['nonce'])
(self.hex,self.coin_txid) = t.txsign(tx_in,wif,d['from'],chain_id=d['chainId'])
msg('OK')
except Exception as e:
m = "{!r}: transaction signing failed!"
msg(m.format(e[0]))
return False
return self.check_sigs()
class EthereumMMGenBumpTX(EthereumMMGenTX,MMGenBumpTX):
def choose_output(self): pass
@ -356,5 +440,7 @@ class EthereumMMGenBumpTX(EthereumMMGenTX,MMGenBumpTX):
def update_fee(self,foo,fee):
self.fee = fee
class EthereumTokenMMGenBumpTX(EthereumTokenMMGenTX,EthereumMMGenBumpTX): pass
class EthereumMMGenSplitTX(MMGenSplitTX): pass
class EthereumDeserializedTX(DeserializedTX): pass

View file

@ -22,6 +22,7 @@ common.py: Common imports for all MMGen scripts
"""
import sys,os
from mmgen.exception import *
from mmgen.globalvars import g
import mmgen.opts as opts
from mmgen.opts import opt

25
mmgen/exception.py Executable file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2018 The MMGen Project <mmgen@tuta.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
mmgen.exception: Exception classes for the MMGen suite
"""
class UnrecognizedTokenSymbol(Exception): pass
class TokenNotInBlockchain(Exception): pass

View file

@ -35,6 +35,7 @@ opts_data = lambda: {
-c, --comment-file=f Source the transaction's comment from file 'f'
-C, --tx-confs= c Desired number of confirmations (default: {g.tx_confs})
-d, --outdir= d Specify an alternate directory 'd' for output
-D, --contract-data=D Path to hex-encoded contract data (ETH only)
-f, --tx-fee= f Transaction fee, as a decimal {cu} amount or as
{fu} (an integer followed by {fl}).
See FEE SPECIFICATION below. If omitted, fee will be

View file

@ -36,6 +36,7 @@ opts_data = lambda: {
-c, --comment-file= f Source the transaction's comment from file 'f'
-C, --tx-confs= c Desired number of confirmations (default: {g.tx_confs})
-d, --outdir= d Specify an alternate directory 'd' for output
-D, --contract-data= D Path to hex-encoded contract data (ETH only)
-e, --echo-passphrase Print passphrase to screen when typing it
-f, --tx-fee= f Transaction fee, as a decimal {cu} amount or as
{fu} (an integer followed by {fl}).
@ -108,3 +109,6 @@ tx.write_to_file(ask_write=False)
tx.send(exit_on_fail=True)
tx.write_to_file(ask_overwrite=False,ask_write=False)
if hasattr(tx,'token_addr'):
msg('Token address: {}'.format(tx.token_addr.hl()))

View file

@ -66,3 +66,6 @@ if not opt.yes:
tx.send(exit_on_fail=True)
tx.write_to_file(ask_overwrite=False,ask_write=False)
if hasattr(tx,'token_addr'):
msg('Token address: {}'.format(tx.token_addr.hl()))

View file

@ -444,6 +444,9 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject):
else:
return pfx_ok(vn[self.addr_fmt][1])
class TokenAddr(CoinAddr):
color = 'blue'
class ViewKey(object):
def __new__(cls,s,on_fail='die'):
from mmgen.globalvars import g

View file

@ -199,6 +199,7 @@ def init(opts_f,add_opts=[],opt_filter=None):
common_opts_data = """
--, --accept-defaults Accept defaults at all prompts
--, --coin=c Choose coin unit. Default: {cu_dfl}. Options: {cu_all}
--, --token=t Specify an ERC20 token by address or symbol
--, --color=0|1 Disable or enable color output
--, --force-256-color Force 256-color output when color is enabled
--, --daemon-data-dir=d Specify coin daemon data directory location 'd'
@ -344,7 +345,9 @@ def init(opts_f,add_opts=[],opt_filter=None):
def opt_is_tx_fee(val,desc):
from mmgen.tx import MMGenTX
ret = MMGenTX().process_fee_spec(val,224,on_fail='return')
if opt.contract_data or opt.tx_gas: ret = None # Non-standard startgas: disable fee checking
# Non-standard startgas: disable fee checking
if hasattr(opt,'contract_data') and opt.contract_data: ret = None
if hasattr(opt,'tx_gas') and opt.tx_gas: ret = None
if ret == False:
msg("'{}': invalid {}\n(not a {} amount or {} specification)".format(
val,desc,g.coin.upper(),MMGenTX().rel_fee_desc))
@ -511,6 +514,17 @@ def check_opts(usr_opts): # Returns false if any check fails
elif key == 'locktime':
if not opt_is_int(val,desc): return False
if not opt_compares(val,'>',0,desc): return False
elif key == 'token':
if not 'token' in g.proto.caps:
msg("Coin '{}' does not support the --token option".format(g.coin))
return False
elif len(val) == 40 and is_hex_str(val):
pass
elif len(val) > 20 or not all(s.isalnum() for s in val):
msg("u'{}: invalid parameter for --token option".format(val))
return False
elif key == 'contract_data':
check_infile(val)
else:
if g.debug: Msg("check_opts(): No test for opt '{}'".format(key))

View file

@ -309,6 +309,7 @@ class EthereumProtocol(DummyWIF,BitcoinProtocolAddrgen):
max_tx_fee = ETHAmt('0.005')
chain_name = 'foundation'
sign_mode = 'standalone'
caps = ('token',)
@classmethod
def verify_addr(cls,addr,hex_width,return_dict=False):

View file

@ -186,7 +186,8 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
out = [self.hdr_fmt.format(' '.join(self.sort_info()),g.dcoin,self.total.hl())]
if g.chain != 'mainnet': out += ['Chain: '+green(g.chain.upper())]
fs = { 'btc': u' {n:%s} {t:%s} {v:2} {a} {A} {c:<}' % (col1_w,tx_w),
'eth': u' {n:%s} {a} {A}' % col1_w }[self.disp_type]
'eth': u' {n:%s} {a} {A}' % col1_w,
'token': u' {n:%s} {a} {A} {A2}' % col1_w }[self.disp_type]
out += [fs.format( n='Num',
t='TXid'.ljust(tx_w - 5) + ' Vout',
v='',
@ -231,7 +232,8 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in self.unspent) or 12 # DEADBEEF:S:1
amt_w = g.proto.coin_amt.max_prec + 4
fs = { 'btc': u' {n:4} {t:%s} {a} {m} {A:%s} {c:<8} {g:<6} {l}' % (self.txid_w+3,amt_w),
'eth': u' {n:4} {a} {m} {A:%s} {c:<8} {g:<6} {l}' % amt_w
'eth': u' {n:4} {a} {m} {A:%s} {c:<8} {g:<6} {l}' % amt_w,
'token': u' {n:4} {a} {m} {A:%s} {A2:%s} {c:<8} {g:<6} {l}' % (amt_w,amt_w)
}[self.disp_type]
out = [fs.format( n='Num',
t='Tx ID,Vout',

View file

@ -1306,7 +1306,6 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
sel_nums = getattr(self,us_f)(tw.unspent)
msg('Selected output{}: {}'.format(suf(sel_nums,'s'),' '.join(map(str,sel_nums))))
sel_unspent = tw.MMGenTwOutputList([tw.unspent[i-1] for i in sel_nums])
inputs_sum = sum(s.amt for s in sel_unspent)

View file

@ -25,6 +25,7 @@ from hashlib import sha256
from binascii import hexlify,unhexlify
from string import hexdigits
from mmgen.color import *
from mmgen.exception import *
def msg(s): sys.stderr.write(s.encode('utf8') + '\n')
def msg_r(s): sys.stderr.write(s.encode('utf8'))
@ -824,6 +825,23 @@ def get_coin_daemon_auth_cookie():
def rpc_init_parity():
def resolve_token_arg(token_arg):
from mmgen.tw import TrackingWallet
from mmgen.obj import CoinAddr
from mmgen.altcoins.eth.contract import Token
try: addr = CoinAddr(token_arg,on_fail='raise')
except: addr = TrackingWallet().sym2addr(token_arg)
else: Token(addr) # test for presence in blockchain
if not addr:
m = "'{}': unrecognized token symbol"
raise UnrecognizedTokenSymbol,m.format(token_arg)
sym = Token(addr).symbol().upper()
vmsg('ERC20 token resolved: {} ({})'.format(addr,sym))
return addr,sym
from mmgen.rpc import EthereumRPCConnection
g.rpch = EthereumRPCConnection(
g.rpc_host or 'localhost',
@ -832,6 +850,8 @@ 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()
if g.token:
(g.token,g.dcoin) = resolve_token_arg(g.token)
return g.rpch

186
scripts/create-token.py Executable file
View file

@ -0,0 +1,186 @@
#!/usr/bin/env python
import sys,os,json
from subprocess import Popen,PIPE
from mmgen.common import *
from mmgen.obj import CoinAddr,is_coin_addr
decimals = 18
supply = 10**26
name = 'MMGen Token'
symbol = 'MMT'
opts_data = lambda: {
'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
""".format(d=decimals,n=name,s=symbol,t=supply)
}
g.coin = 'ETH'
cmd_args = opts.init(opts_data)
if not len(cmd_args) == 1 or not is_coin_addr(cmd_args[0]):
opts.usage()
owner_addr = '0x' + CoinAddr(cmd_args[0])
# ERC Token Standard #20 Interface
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md
code_in = """
pragma solidity ^0.4.18;
contract SafeMath {
function safeAdd(uint a, uint b) public pure returns (uint c) {
c = a + b;
require(c >= a);
}
function safeSub(uint a, uint b) public pure returns (uint c) {
require(b <= a);
c = a - b;
}
function safeMul(uint a, uint b) public pure returns (uint c) {
c = a * b;
require(a == 0 || c / a == b);
}
function safeDiv(uint a, uint b) public pure returns (uint c) {
require(b > 0);
c = a / b;
}
}
contract ERC20Interface {
function totalSupply() public constant returns (uint);
function balanceOf(address tokenOwner) public constant returns (uint balance);
function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
// Contract function to receive approval and execute function in one call
contract ApproveAndCallFallBack {
function receiveApproval(address from, uint256 tokens, address token, bytes data) public;
}
contract Owned {
address public owner;
address public newOwner;
event OwnershipTransferred(address indexed _from, address indexed _to);
constructor() public {
owner = msg.sender;
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function transferOwnership(address _newOwner) public onlyOwner {
newOwner = _newOwner;
}
function acceptOwnership() public {
require(msg.sender == newOwner);
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
newOwner = address(0);
}
}
// ----------------------------------------------------------------------------
// ERC20 Token, with the addition of symbol, name and decimals and assisted
// token transfers
// ----------------------------------------------------------------------------
contract Token is ERC20Interface, Owned, SafeMath {
string public symbol;
string public name;
uint8 public decimals;
uint public _totalSupply;
mapping(address => uint) balances;
mapping(address => mapping(address => uint)) allowed;
constructor() public {
symbol = "<SYMBOL>";
name = "<NAME>";
decimals = <DECIMALS>;
_totalSupply = <SUPPLY>;
balances[<OWNER_ADDR>] = _totalSupply;
emit Transfer(address(0), <OWNER_ADDR>, _totalSupply);
}
function totalSupply() public constant returns (uint) {
return _totalSupply - balances[address(0)];
}
function balanceOf(address tokenOwner) public constant returns (uint balance) {
return balances[tokenOwner];
}
function transfer(address to, uint tokens) public returns (bool success) {
balances[msg.sender] = safeSub(balances[msg.sender], tokens);
balances[to] = safeAdd(balances[to], tokens);
emit Transfer(msg.sender, to, tokens);
return true;
}
function approve(address spender, uint tokens) public returns (bool success) {
allowed[msg.sender][spender] = tokens;
emit Approval(msg.sender, spender, tokens);
return true;
}
function transferFrom(address from, address to, uint tokens) public returns (bool success) {
balances[from] = safeSub(balances[from], tokens);
allowed[from][msg.sender] = safeSub(allowed[from][msg.sender], tokens);
balances[to] = safeAdd(balances[to], tokens);
emit Transfer(from, to, tokens);
return true;
}
function allowance(address tokenOwner, address spender) public constant returns (uint remaining) {
return allowed[tokenOwner][spender];
}
function approveAndCall(address spender, uint tokens, bytes data) public returns (bool success) {
allowed[msg.sender][spender] = tokens;
emit Approval(msg.sender, spender, tokens);
ApproveAndCallFallBack(spender).receiveApproval(msg.sender, tokens, this, data);
return true;
}
// Don't accept ETH
function () public payable {
revert();
}
// Owner can transfer out any accidentally sent ERC20 tokens
function transferAnyERC20Token(address tokenAddress, uint tokens) public onlyOwner returns (bool success) {
return ERC20Interface(tokenAddress).transfer(owner, tokens);
}
}
"""
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]))
return code
def compile_code(code):
cmd = ['solc','--optimize','--bin','--overwrite']
if not opt.stdout: cmd += ['--output-dir', opt.outdir or '.']
p = Popen(cmd,stdin=PIPE,stdout=PIPE,stderr=PIPE)
res = p.communicate(code)
o = res[0].replace('\r','').split('\n')
dmsg(res[1])
if opt.stdout:
return dict((k,o[i+2]) for k in ('SafeMath','Owned','Token') for i in range(len(o)) if k in o[i])
src = create_src(code_in)
out = compile_code(src)
if opt.stdout:
print(json.dumps(out))

View file

@ -115,6 +115,7 @@ setup(
'mmgen.common',
'mmgen.crypto',
'mmgen.ed25519',
'mmgen.exception',
'mmgen.filename',
'mmgen.globalvars',
'mmgen.license',
@ -137,6 +138,7 @@ setup(
'mmgen.altcoins.__init__',
'mmgen.altcoins.eth.__init__',
'mmgen.altcoins.eth.contract',
'mmgen.altcoins.eth.obj',
'mmgen.altcoins.eth.tx',
'mmgen.altcoins.eth.tw',

View file

@ -897,6 +897,44 @@ cmd_group['ethdev'] = (
('ethdev_chk_label', 'the label'),
('ethdev_remove_label', 'removing the label'),
('ethdev_token_compile1', 'compiling ERC20 token #1'),
('ethdev_token_deploy1a', 'deploying ERC20 token #1 (SafeMath)'),
('ethdev_token_deploy1b', 'deploying ERC20 token #1 (Owned)'),
('ethdev_token_deploy1c', 'deploying ERC20 token #1 (Token)'),
('ethdev_token_compile2', 'compiling ERC20 token #2'),
('ethdev_token_deploy2a', 'deploying ERC20 token #2 (SafeMath)'),
('ethdev_token_deploy2b', 'deploying ERC20 token #2 (Owned)'),
('ethdev_token_deploy2c', 'deploying ERC20 token #2 (Token)'),
('ethdev_contract_deploy', 'deploying contract (create,sign,send)'),
('ethdev_token_transfer_funds','transferring token funds from dev to user'),
('ethdev_token_addrgen', 'generating token addresses'),
('ethdev_token_addrimport', 'importing token addresses'),
('ethdev_token_txcreate1', 'creating a token transaction'),
('ethdev_token_txsign1', 'signing the transaction'),
('ethdev_token_txsend1', 'sending the transaction'),
('ethdev_token_txcreate2', 'creating a token transaction (to burn address)'),
('ethdev_token_txbump', 'bumping the transaction fee'),
('ethdev_token_txsign2', 'signing the transaction'),
('ethdev_token_txsend2', 'sending the transaction'),
('ethdev_del_dev_addr', "deleting the dev address"),
('ethdev_bal2', 'the ETH balance'),
('ethdev_bal2_getbalance', 'the ETH balance (getbalance)'),
('ethdev_addrimport_token_burn_addr',"importing the token burn address"),
('ethdev_token_bal', 'the token balance'),
('ethdev_token_bal_getbalance','the token balance (getbalance)'),
('ethdev_stop', 'stopping parity'),
)
@ -3251,6 +3289,147 @@ class MMGenTestSuite(object):
g.proto.rpc_port = 8549
rpc_init()
def ethdev_token_compile(self,name,token_data={}):
MMGenExpect(name,'',msg_only=True)
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]
imsg("Executing: {}".format(' '.join(cmd)))
subprocess.check_output(cmd)
imsg("ERC20 token '{}' compiled".format(token_data['symbol']))
end_silence()
ok()
def ethdev_token_compile1(self,name):
token_data = { 'name':'MMGen Token 1', 'symbol':'MM1', 'supply':10**26, 'decimals':18 }
self.ethdev_token_compile(name,token_data)
def ethdev_token_compile2(self,name):
token_data = { 'name':'MMGen Token 2', 'symbol':'MM2', 'supply':10**18, 'decimals':10 }
self.ethdev_token_compile(name,token_data)
def ethdev_token_deploy(self,name,num,key,gas,mmgen_cmd='txdo'):
self.init_ethdev_common()
key_fn = get_tmpfile_fn(cfg,cfg['parity_keyfile'])
fn = os.path.join(cfg['tmpdir'],key+'.bin')
os.environ['MMGEN_BOGUS_SEND'] = ''
args = ['-B','--tx-fee=8G','--tx-gas={}'.format(gas),'--contract-data='+fn,'--inputs='+eth_addr,'--yes']
if mmgen_cmd == 'txdo': args += ['-k',key_fn]
t = MMGenExpect(name,'mmgen-'+mmgen_cmd, eth_args + args)
if mmgen_cmd == 'txcreate':
t.written_to_file('Ethereum transaction')
tx_fn = get_file_with_ext('[0,8000].rawtx',cfg['tmpdir'],no_dot=True)
t = MMGenExpect(name,'mmgen-txsign', eth_args + ['--yes','-k',key_fn,tx_fn],no_msg=True)
self.txsign_ui_common(t,name,ni=True,no_ok=True)
tx_fn = tx_fn.replace('.rawtx','.sigtx')
t = MMGenExpect(name,'mmgen-txsend', eth_args + [tx_fn],no_msg=True)
os.environ['MMGEN_BOGUS_SEND'] = '1'
txid = self.txsend_ui_common(t,mmgen_cmd,quiet=True,bogus_send=False,no_ok=True)
addr = t.expect_getend('Token address: ')
from mmgen.altcoins.eth.tx import EthereumMMGenTX as etx
assert etx.get_exec_status(txid) != 0,"Contract '{}:{}' failed to execute. Aborting".format(num,key)
if key == 'Token':
write_to_tmpfile(cfg,'token_addr{}'.format(num),addr+'\n')
silence()
imsg('\nToken #{} ({}) deployed!'.format(num,addr))
end_silence()
t.ok()
def ethdev_token_deploy1a(self,name): self.ethdev_token_deploy(name,num=1,key='SafeMath',gas=200000)
def ethdev_token_deploy1b(self,name): self.ethdev_token_deploy(name,num=1,key='Owned',gas=250000)
def ethdev_token_deploy1c(self,name): self.ethdev_token_deploy(name,num=1,key='Token',gas=1100000)
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)
def ethdev_token_deploy2c(self,name): self.ethdev_token_deploy(name,num=2,key='Token',gas=1100000)
def ethdev_contract_deploy(self,name): # test create,sign,send
self.ethdev_token_deploy(name,num=2,key='SafeMath',gas=1100000,mmgen_cmd='txcreate')
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]
silence()
usr_addrs = [subprocess.check_output(cmd(i),stderr=sys.stderr).strip() for i in 11,21]
self.init_ethdev_common()
from mmgen.altcoins.eth.contract import Token
from mmgen.altcoins.eth.tx import EthereumMMGenTX as etx
for i in range(2):
tk = Token(read_from_tmpfile(cfg,'token_addr{}'.format(i+1)).strip())
imsg('\n'+tk.info())
txid = tk.transfer(eth_addr,usr_addrs[i],1000,eth_key,
start_gas=ETHAmt(60000,'wei'),gasPrice=ETHAmt(8,'Gwei'))
assert etx.get_exec_status(txid) != 0,'Transfer of token funds failed. Aborting'
imsg('dev token balance: {}'.format(tk.balance(eth_addr)))
imsg('usr{} token balance: {}'.format(i+1,tk.balance(usr_addrs[i])))
end_silence()
ok()
def ethdev_token_addrgen(self,name):
self.ethdev_addrgen(name,addrs='11-13')
self.ethdev_addrgen(name,addrs='21-23')
def ethdev_token_addrimport(self,name):
for n,r in ('1','11-13'),('2','21-23'):
tk_addr = read_from_tmpfile(cfg,'token_addr'+n).strip()
self.ethdev_addrimport(name,ext='['+r+'].addrs',expect='3/3',add_args=['--token='+tk_addr])
def ethdev_token_txcreate(self,name,args=[],token='',inputs='1'):
t = MMGenExpect(name,'mmgen-txcreate', eth_args + ['--token='+token,'-B','--tx-fee=50G'] + args)
self.txcreate_ui_common(t,name,menu=[],
input_sels_prompt='to spend from',
inputs=inputs,file_desc='Ethereum token transaction',
add_comment=ref_tx_label_lat_cyr_gr)
return t
def ethdev_token_txsign(self,name,ext='',token=''):
self.ethdev_txsign(name,ni=True,ext=ext,add_args=['--token='+token])
def ethdev_token_txsend(self,name,ext='',token=''):
self.ethdev_txsend(name,ext=ext,add_args=['--token=mm1'])
def ethdev_token_txcreate1(self,name):
return self.ethdev_token_txcreate(name,args=['98831F3A:E:12,1.23456'],token='mm1')
def ethdev_token_txsign1(self,name):
self.ethdev_token_txsign(name,ext='1.23456,50000].rawtx',token='mm1')
def ethdev_token_txsend1(self,name):
self.ethdev_token_txsend(name,ext='1.23456,50000].sigtx',token='mm1')
def ethdev_token_txcreate2(self,name):
return self.ethdev_token_txcreate(name,args=[eth_burn_addr+','+eth_amt2],token='mm1')
def ethdev_token_txbump(self,name):
self.ethdev_txbump(name,ext=eth_amt2+',50000].rawtx',fee='56G',add_args=['--token=mm1'])
def ethdev_token_txsign2(self,name):
self.ethdev_token_txsign(name,ext=eth_amt2+',50000].rawtx',token='mm1')
def ethdev_token_txsend2(self,name):
self.ethdev_token_txsend(name,ext=eth_amt2+',50000].sigtx',token='mm1')
def ethdev_del_dev_addr(self,name):
t = MMGenExpect(name,'mmgen-tool', eth_args + ['remove_address',eth_addr])
t.read() # TODO
t.ok()
def ethdev_addrimport_token_burn_addr(self,name):
self.ethdev_addrimport_one_addr(name,addr=eth_burn_addr,extra_args=['--token=mm1'])
def ethdev_bal2(self,name,expect_str=''):
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')
def ethdev_token_bal(self,name):
t = MMGenExpect(name,'mmgen-tool', eth_args + ['--token=mm1','twview','wide=1'])
t.expect(r'deadbeef.* '+eth_amt2,regex=True)
t.read()
t.ok()
def ethdev_token_bal_getbalance(self,name):
self.ethdev_bal_getbalance(name,
t_non_mmgen='888.111122223333444455',t_mmgen='111.888877776666555545',extra_args=['--token=mm1'])
def ethdev_stop(self,name):
MMGenExpect(name,'',msg_only=True)
pid = read_from_tmpfile(cfg,cfg['parity_pidfile'])