tx.process_cmd_args(); ETH: no-change tx support, tx.get_status(), other fixes

This commit is contained in:
The MMGen Project 2018-07-28 13:52:43 +00:00
commit d1970d1473
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
6 changed files with 180 additions and 73 deletions

View file

@ -53,6 +53,8 @@ class EthereumMMGenTX(MMGenTX):
if hasattr(opt,'tx_gas') and opt.tx_gas:
self.tx_gas = self.start_gas = ETHAmt(int(opt.tx_gas),'wei')
if hasattr(opt,'contract_data') and opt.contract_data:
m = "'--contract-data' option may not be used with token transaction"
assert not 'Token' in type(self).__name__, m
self.data = HexStr(open(opt.contract_data).read().strip())
self.disable_fee_check = True
@ -62,7 +64,10 @@ class EthereumMMGenTX(MMGenTX):
@classmethod
def get_exec_status(cls,txid):
return int(g.rpch.eth_getTransactionReceipt('0x'+txid)['status'],16)
d = g.rpch.eth_getTransactionReceipt('0x'+txid)
if 'contractAddress' in d and d['contractAddress']:
msg('Contract address: {}'.format(d['contractAddress'].replace('0x','')))
return int(d['status'],16)
def is_replaceable(self): return True
@ -120,6 +125,9 @@ class EthereumMMGenTX(MMGenTX):
self.txobj = o
return d # 'token_addr','decimals' required by subclass
def get_nonce(self):
return ETHNonce(int(g.rpch.parity_nextNonce('0x'+self.inputs[0].addr),16))
def make_txobj(self): # create_raw
self.txobj = {
'from': self.inputs[0].addr,
@ -127,7 +135,7 @@ class EthereumMMGenTX(MMGenTX):
'amt': self.outputs[0].amt if self.outputs else ETHAmt(0),
'gasPrice': self.usr_rel_fee or self.fee_abs2rel(self.fee,to_unit='eth'),
'startGas': self.start_gas,
'nonce': ETHNonce(int(g.rpch.parity_nextNonce('0x'+self.inputs[0].addr),16)),
'nonce': self.get_nonce(),
'chainId': Int(g.rpch.parity_chainId(),16),
'data': self.data,
}
@ -145,8 +153,6 @@ class EthereumMMGenTX(MMGenTX):
self.update_txid()
def del_output(self,idx): pass
def update_output_amt(self,idx,amt): pass
def get_chg_output_idx(self): return None
def update_txid(self):
assert not is_hex_str(self.hex),'update_txid() must be called only when self.hex is not hex data'
@ -157,26 +163,12 @@ class EthereumMMGenTX(MMGenTX):
def process_cmd_args(self,cmd_args,ad_f,ad_w):
lc = len(cmd_args)
if lc == 0 and self.data:
return
elif lc != 1:
if lc == 0 and self.data and not 'Token' in type(self).__name__: return
if lc != 1:
fs = '{} output{} specified, but Ethereum transactions must have exactly one'
die(1,fs.format(lc,suf(lc)))
a = list(cmd_args)[0]
if ',' in a:
a1,a2 = a.split(',',1)
if is_mmgen_id(a1) or is_coin_addr(a1):
coin_addr = mmaddr2coinaddr(a1,ad_w,ad_f) if is_mmgen_id(a1) else CoinAddr(a1)
self.add_output(coin_addr,ETHAmt(a2))
else:
die(2,"{}: invalid subargument in command-line argument '{}'".format(a1,a))
else:
die(2,'{}: invalid command-line argument'.format(a))
if not self.outputs:
die(2,'At least one output must be specified on the command line')
for a in cmd_args: self.process_cmd_arg(a,ad_f,ad_w)
def select_unspent(self,unspent):
prompt = 'Enter an account to spend from: '
@ -198,7 +190,8 @@ class EthereumMMGenTX(MMGenTX):
# given absolute fee in ETH, return gas price in Gwei using tx_gas
def fee_abs2rel(self,abs_fee,to_unit='Gwei'):
ret = ETHAmt(int(abs_fee.toWei() / self.tx_gas.toWei()),'wei')
return ret if to_unit == 'eth' else ret.to_unit(to_unit)
dmsg('fee_abs2rel() ==> {} ETH'.format(ret))
return ret if to_unit == 'eth' else ret.to_unit(to_unit,show_decimal=True)
# get rel_fee (gas price) from network, return in native wei
def get_rel_fee_from_network(self):
@ -234,6 +227,14 @@ class EthereumMMGenTX(MMGenTX):
else:
return abs_fee
def update_change_output(self,change_amt):
if self.outputs and self.outputs[0].is_chg:
self.update_output_amt(0,ETHAmt(change_amt))
def update_send_amt(self,foo):
if self.outputs:
self.send_amt = self.outputs[0].amt
def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse):
m = {}
for k in ('in','out'):
@ -253,7 +254,7 @@ class EthereumMMGenTX(MMGenTX):
return fs.format( *((self.txobj[k] if self.txobj[k] != '' else Str('None')).hl() for k in keys),
d='{}... ({} bytes)'.format(self.txobj['data'][:40],ld/2) if ld else Str('None'),
c=g.dcoin if len(self.outputs) else '',
g=yellow(str(self.txobj['gasPrice'].toGwei())),
g=yellow(str(self.txobj['gasPrice'].to_unit('Gwei',show_decimal=True))),
G=yellow(str(self.txobj['startGas'].toKwei())),
t_mmid=m['out'] if len(self.outputs) else '',
f_mmid=m['in'])
@ -266,9 +267,13 @@ class EthereumMMGenTX(MMGenTX):
def format_view_rel_fee(self,terse): return ''
def format_view_verbose_footer(self): return '' # TODO
def set_g_token(self):
die(2,"Not a Token transaction object. Have you omitted the '--token' option?")
def final_inputs_ok_msg(self,change_amt):
m = "Transaction leaves {} {} in the sender's account"
return m.format(g.proto.coin_amt(change_amt).hl(),g.coin)
chg = 0 if (self.outputs and self.outputs[0].is_chg) else change_amt
return m.format(ETHAmt(chg).hl(),g.coin)
def do_sign(self,d,wif,tx_num_str):
@ -309,7 +314,33 @@ class EthereumMMGenTX(MMGenTX):
return self.do_sign(self.txobj,keys[0].sec.wif,tx_num_str)
def get_status(self,status=False): pass # TODO
def is_in_mempool(self):
# pmsg(g.rpch.parity_pendingTransactions())
return '0x'+self.coin_txid in map(lambda x: x['hash'],g.rpch.parity_pendingTransactions())
def is_in_wallet(self):
d = g.rpch.eth_getTransactionReceipt('0x'+self.coin_txid)
if d and 'blockNumber' in d:
return 1 + int(g.rpch.eth_blockNumber(),16) - int(d['blockNumber'],16)
return False
def get_status(self,status=False):
if self.is_in_mempool():
msg('Transaction is in mempool' if status else 'Warning: transaction is in mempool!')
return
confs = self.is_in_wallet()
if confs is not False:
if self.data:
exec_status = type(self).get_exec_status(self.coin_txid)
if exec_status == 0:
msg('Contract failed to execute!')
else:
msg('Contract successfully executed with status {}'.format(exec_status))
die(0,'Transaction has {} confirmation{}'.format(confs,suf(confs,'s')))
if status:
die(1,'Transaction is neither in mempool nor blockchain!')
def send(self,prompt_user=True,exit_on_fail=False):
@ -354,6 +385,10 @@ class EthereumTokenMMGenTX(EthereumMMGenTX):
start_gas = ETHAmt(60000,'wei')
fee_is_approximate = True
def update_change_output(self,change_amt):
if self.outputs[0].is_chg:
self.update_output_amt(0,self.inputs[0].amt)
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
@ -366,13 +401,13 @@ class EthereumTokenMMGenTX(EthereumMMGenTX):
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)
send_acct_tbal = 0 if self.outputs[0].is_chg else \
Token(g.token).balance(self.inputs[0].addr) - self.outputs[0].amt
return m.format(ETHAmt(change_amt).hl(),g.coin,ETHAmt(send_acct_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
return eth_bal - self.fee
def set_g_token(self):
g.dcoin = self.dcoin
@ -390,7 +425,7 @@ class EthereumTokenMMGenTX(EthereumMMGenTX):
t = Token(g.token)
o = t.txcreate( self.inputs[0].addr,
self.outputs[0].addr,
self.outputs[0].amt,
(self.inputs[0].amt if self.outputs[0].is_chg else 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
@ -440,6 +475,9 @@ class EthereumMMGenBumpTX(EthereumMMGenTX,MMGenBumpTX):
def update_fee(self,foo,fee):
self.fee = fee
def get_nonce(self):
return self.txobj['nonce']
class EthereumTokenMMGenBumpTX(EthereumTokenMMGenTX,EthereumMMGenBumpTX): pass
class EthereumMMGenSplitTX(MMGenSplitTX): pass

View file

@ -111,4 +111,4 @@ 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()))
msg('Contract address: {}'.format(tx.token_addr.hl()))

View file

@ -68,4 +68,4 @@ 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()))
msg('Contract address: {}'.format(tx.token_addr.hl()))

View file

@ -336,8 +336,14 @@ class BTCAmt(Decimal,Hilite,InitErrors):
m = "{!r}: value cannot be converted to {} ({})"
return cls.init_fail(m.format(num,cls.__name__,e[0]),on_fail)
def toSatoshi(self): return int(Decimal(self) / self.satoshi)
def to_unit(self,unit): return int(Decimal(self) / getattr(self,unit))
def toSatoshi(self):
return int(Decimal(self) / self.satoshi)
def to_unit(self,unit,show_decimal=False):
ret = Decimal(self) / getattr(self,unit)
if show_decimal and ret < 1:
return '{:.4f}'.format(ret)
return int(ret)
@classmethod
def fmtc(cls):

View file

@ -315,16 +315,22 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
self.outputs.append(MMGenTX.MMGenTxOutput(addr=coinaddr,amt=amt,is_chg=is_chg))
def get_chg_output_idx(self):
for i in range(len(self.outputs)):
if self.outputs[i].is_chg == True:
return i
return None
try: return map(lambda x: x.is_chg,self.outputs).index(True)
except ValueError: return None
def update_output_amt(self,idx,amt):
o = self.outputs[idx].__dict__
o['amt'] = amt
self.outputs[idx] = MMGenTX.MMGenTxOutput(**o)
def update_change_output(self,change_amt):
chg_idx = self.get_chg_output_idx()
if change_amt == 0:
msg(self.no_chg_msg)
self.del_output(chg_idx)
else:
self.update_output_amt(chg_idx,g.proto.coin_amt(change_amt))
def del_output(self,idx):
self.outputs.pop(idx)
@ -1201,22 +1207,26 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
if self.dcoin: self.set_g_token()
def process_cmd_args(self,cmd_args,ad_f,ad_w):
for a in cmd_args:
if ',' in a:
a1,a2 = a.split(',',1)
if is_mmgen_id(a1) or is_coin_addr(a1):
coin_addr = mmaddr2coinaddr(a1,ad_w,ad_f) if is_mmgen_id(a1) else CoinAddr(a1)
self.add_output(coin_addr,g.proto.coin_amt(a2))
else:
die(2,"{}: invalid subargument in command-line argument '{}'".format(a1,a))
elif is_mmgen_id(a) or is_coin_addr(a):
if self.get_chg_output_idx() != None:
die(2,'ERROR: More than one change address listed on command line')
coin_addr = mmaddr2coinaddr(a,ad_w,ad_f) if is_mmgen_id(a) else CoinAddr(a)
self.add_output(coin_addr,g.proto.coin_amt('0'),is_chg=True)
def process_cmd_arg(self,arg,ad_f,ad_w):
def add_output_chk(addr,amt,err_desc):
if not amt and self.get_chg_output_idx() != None:
die(2,'ERROR: More than one change address listed on command line')
if is_mmgen_id(addr) or is_coin_addr(addr):
coin_addr = mmaddr2coinaddr(addr,ad_w,ad_f) if is_mmgen_id(addr) else CoinAddr(addr)
self.add_output(coin_addr,g.proto.coin_amt(amt or '0'),is_chg=not amt)
else:
die(2,'{}: invalid command-line argument'.format(a))
die(2,"{}: invalid {} '{}'".format(addr,err_desc,','.join((addr,amt)) if amt else addr))
if ',' in arg:
addr,amt = arg.split(',',1)
add_output_chk(addr,amt,'coin argument in command-line argument')
else:
add_output_chk(arg,None,'command-line argument')
def process_cmd_args(self,cmd_args,ad_f,ad_w):
for a in cmd_args: self.process_cmd_arg(a,ad_f,ad_w)
if self.get_chg_output_idx() == None:
die(2,( 'ERROR: No change output specified',
@ -1336,6 +1346,10 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
def check_fee(self):
assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee
def update_send_amt(self,change_amt):
if not self.send_amt:
self.send_amt = change_amt
def create(self,cmd_args,locktime,do_info=False):
assert type(locktime) == int
@ -1367,16 +1381,8 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
if opt.rbf: self.inputs[0].sequence = g.max_int - 2 # handles the locktime case too
elif locktime: self.inputs[0].sequence = g.max_int - 1
chg_idx = self.get_chg_output_idx()
if change_amt == 0:
msg(self.no_chg_msg)
self.del_output(chg_idx)
else:
self.update_output_amt(chg_idx,g.proto.coin_amt(change_amt))
if not self.send_amt and len(self.outputs):
self.send_amt = change_amt
self.update_change_output(change_amt)
self.update_send_amt(change_amt)
if not opt.yes:
self.add_comment() # edits an existing comment

View file

@ -885,6 +885,8 @@ cmd_group['ethdev'] = (
('ethdev_txsign3', 'signing the transaction'),
('ethdev_txsend3', 'sending the transaction'),
('ethdev_tx_status1', 'getting the transaction status'),
('ethdev_txcreate4', 'creating a transaction (spend from MMGen address, low TX fee)'),
('ethdev_txbump', 'bumping the transaction fee'),
@ -909,6 +911,8 @@ cmd_group['ethdev'] = (
('ethdev_token_deploy1b', 'deploying ERC20 token #1 (Owned)'),
('ethdev_token_deploy1c', 'deploying ERC20 token #1 (Token)'),
('ethdev_tx_status2', 'getting the transaction status'),
('ethdev_token_compile2', 'compiling ERC20 token #2'),
('ethdev_token_deploy2a', 'deploying ERC20 token #2 (SafeMath)'),
@ -938,10 +942,23 @@ cmd_group['ethdev'] = (
('ethdev_addrimport_token_burn_addr',"importing the token burn address"),
('ethdev_token_bal', 'the token balance'),
('ethdev_token_bal1', 'the token balance'),
('ethdev_token_bal_getbalance','the token balance (getbalance)'),
('ethdev_stop', 'stopping parity'),
('ethdev_txcreate_noamt', 'creating a transaction (full amount send)'),
('ethdev_txsign_noamt', 'signing the transaction'),
('ethdev_txsend_noamt', 'sending the transaction'),
('ethdev_token_bal2', 'the token balance'),
('ethdev_bal3', 'the ETH balance'),
('ethdev_token_txcreate_noamt', 'creating a token transaction (full amount send)'),
('ethdev_token_txsign_noamt', 'signing the transaction'),
('ethdev_token_txsend_noamt', 'sending the transaction'),
('ethdev_token_bal3', 'the token balance'),
# ('ethdev_stop', 'stopping parity'),
)
cmd_group['autosign'] = (
@ -3239,6 +3256,16 @@ class MMGenTestSuite(object):
def ethdev_txsign3(self,name): self.ethdev_txsign(name,ni=True,ext='2.345,50000].rawtx')
def ethdev_txsend3(self,name): self.ethdev_txsend(name,ext='2.345,50000].sigtx')
def ethdev_tx_status(self,name,ext,expect_str):
tx_fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
t = MMGenExpect(name,'mmgen-txsend', eth_args + ['--status',tx_fn])
t.expect(expect_str)
t.read()
t.ok()
def ethdev_tx_status1(self,name):
self.ethdev_tx_status(name,ext='2.345,50000].sigtx',expect_str='has 1 confirmation')
def ethdev_txcreate4(self,name):
args = ['98831F3A:E:2,23.45495']
interactive_fee='40G'
@ -3321,12 +3348,12 @@ class MMGenTestSuite(object):
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'):
def ethdev_token_deploy(self,name,num,key,gas,mmgen_cmd='txdo',tx_fee='8G'):
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']
args = ['-B','--tx-fee='+tx_fee,'--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':
@ -3339,7 +3366,7 @@ class MMGenTestSuite(object):
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: ')
addr = t.expect_getend('Contract 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':
@ -3351,7 +3378,11 @@ class MMGenTestSuite(object):
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_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')
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)
@ -3388,8 +3419,8 @@ class MMGenTestSuite(object):
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)
def ethdev_token_txcreate(self,name,args=[],token='',inputs='1',fee='50G'):
t = MMGenExpect(name,'mmgen-txcreate', eth_args + ['--token='+token,'-B','--tx-fee='+fee] + args)
self.txcreate_ui_common(t,name,menu=[],
input_sels_prompt='to spend from',
inputs=inputs,file_desc='Ethereum token transaction',
@ -3432,16 +3463,42 @@ class MMGenTestSuite(object):
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):
def ethdev_token_bal(self,name,expect_str):
t = MMGenExpect(name,'mmgen-tool', eth_args + ['--token=mm1','twview','wide=1'])
t.expect(r'deadbeef.* '+eth_amt2,regex=True)
t.expect(expect_str,regex=True)
t.read()
t.ok()
def ethdev_token_bal1(self,name):
self.ethdev_token_bal(name,expect_str=r'deadbeef.* '+eth_amt2)
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_txcreate_noamt(self,name):
return self.ethdev_txcreate(name,args=['98831F3A:E:12'])
def ethdev_txsign_noamt(self,name):
self.ethdev_txsign(name,ext='99.99895,50000].rawtx')
def ethdev_txsend_noamt(self,name):
self.ethdev_txsend(name,ext='99.99895,50000].sigtx')
def ethdev_token_bal2(self,name):
self.ethdev_token_bal(name,expect_str=r'98831F3A:E:12\s+1.23456\s+99.99895\s')
def ethdev_bal3(self,name,expect_str=''):
self.ethdev_bal(name,expect_str=r'98831F3A:E:1\s+0\n')
def ethdev_token_txcreate_noamt(self,name):
return self.ethdev_token_txcreate(name,args=['98831F3A:E:13'],token='mm1',inputs='2',fee='51G')
def ethdev_token_txsign_noamt(self,name):
self.ethdev_token_txsign(name,ext='1.23456,51000].rawtx',token='mm1')
def ethdev_token_txsend_noamt(self,name):
self.ethdev_token_txsend(name,ext='1.23456,51000].sigtx',token='mm1')
def ethdev_token_bal3(self,name):
self.ethdev_token_bal(name,expect_str=r'98831F3A:E:13\s+1.23456\s')
def ethdev_stop(self,name):
MMGenExpect(name,'',msg_only=True)
pid = read_from_tmpfile(cfg,cfg['parity_pidfile'])