Browse Source

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

MMGen 6 years ago
parent
commit
d1970d1473
6 changed files with 180 additions and 73 deletions
  1. 68 30
      mmgen/altcoins/eth/tx.py
  2. 1 1
      mmgen/main_txdo.py
  3. 1 1
      mmgen/main_txsend.py
  4. 8 2
      mmgen/obj.py
  5. 35 29
      mmgen/tx.py
  6. 67 10
      test/test.py

+ 68 - 30
mmgen/altcoins/eth/tx.py

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

+ 1 - 1
mmgen/main_txdo.py

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

+ 1 - 1
mmgen/main_txsend.py

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

+ 8 - 2
mmgen/obj.py

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

+ 35 - 29
mmgen/tx.py

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

+ 67 - 10
test/test.py

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