tx.py,tw.py: cleanups, support tx inputs from cmdline

This commit is contained in:
The MMGen Project 2018-07-23 21:17:05 +00:00
commit fad573eccd
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
16 changed files with 537 additions and 247 deletions

View file

@ -140,10 +140,9 @@ class EthereumTrackingWallet(TrackingWallet):
m = "Address '{}' not found in '{}' section of tracking wallet"
return ('rpcfail',(None,2,m.format(coinaddr,self.data_root_desc())))
# Use consistent naming, even though Ethereum doesn't have unspent outputs
class EthereumTwUnspentOutputs(TwUnspentOutputs):
show_txid = False
disp_type = 'eth'
can_group = False
hdr_fmt = 'TRACKED ACCOUNTS (sort order: {})\nTotal {}: {}'
desc = 'account balances'
@ -155,32 +154,36 @@ Display options: show [D]ays, show [m]mgen addr, r[e]draw screen
def do_sort(self,key=None,reverse=False):
if key == 'txid': return
super(type(self),self).do_sort(key=key,reverse=reverse)
super(EthereumTwUnspentOutputs,self).do_sort(key=key,reverse=reverse)
def get_addr_bal(self,addr):
return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
def get_unspent_rpc(self):
rpc_init()
return map(lambda d: {
'account': TwLabel(d['mmid']+' '+d['comment'],on_fail='raise'),
'address': d['addr'],
'amount': ETHAmt(int(g.rpch.eth_getBalance('0x'+d['addr']),16),'wei'),
'amount': self.get_addr_bal(d['addr']),
'confirmations': 0, # TODO
}, TrackingWallet().sorted_list())
class EthereumTwAddrList(TwAddrList):
def __init__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels):
tw = TrackingWallet().mmid_ordered_dict()
self.total = g.proto.coin_amt('0')
rpc_init()
# cur_blk = int(g.rpch.eth_blockNumber(),16)
if g.token: self.token = Token(g.token)
tw = TrackingWallet().mmid_ordered_dict()
self.total = g.proto.coin_amt('0')
from mmgen.obj import CoinAddr
for mmid,d in tw.items():
# if d['confirmations'] < minconf: continue
label = TwLabel(mmid+' '+d['comment'],on_fail='raise')
if usr_addr_list and (label.mmid not in usr_addr_list): continue
bal = ETHAmt(int(g.rpch.eth_getBalance('0x'+d['addr']),16),'wei')
bal = self.get_addr_balance(d['addr'])
if bal == 0 and not showempty:
if not label.comment: continue
if not all_labels: continue
@ -191,6 +194,9 @@ class EthereumTwAddrList(TwAddrList):
self[label.mmid]['amt'] += bal
self.total += bal
def get_addr_balance(self,addr):
return ETHAmt(int(g.rpch.eth_getBalance('0x'+addr),16),'wei')
from mmgen.tw import TwGetBalance
class EthereumTwGetBalance(TwGetBalance):

View file

@ -28,20 +28,47 @@ from mmgen.obj import *
from mmgen.tx import MMGenTX,MMGenBumpTX,MMGenSplitTX,DeserializedTX,mmaddr2coinaddr
class EthereumMMGenTX(MMGenTX):
desc = 'Ethereum transaction'
tx_gas = ETHAmt(21000,'wei') # tx_gas 21000 * gasPrice 50 Gwei = fee 0.00105
chg_msg_fs = 'Transaction leaves {} {} in the account'
tx_gas = ETHAmt(21000,'wei') # an approximate number, used for fee estimation purposes
start_gas = ETHAmt(21000,'wei') # the actual startgas amt used in the transaction
# for simple sends with no data, tx_gas = start_gas = 21000
fee_fail_fs = 'Network fee estimation failed'
no_chg_msg = 'Warning: Transaction leaves account with zero balance'
rel_fee_desc = 'gas price'
rel_fee_disp = 'gas price in Gwei'
txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} Sig={s} Locktime={l}\n'
txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} Sig={s} Locktime={l}\n'
txview_ftr_fs = 'Total in account: {i} {d}\nTotal to spend: {o} {d}\nTX fee: {a} {c}{r}\n'
txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n'
usr_fee_prompt = 'Enter transaction fee or gas price: '
fn_fee_unit = 'Mwei'
usr_rel_fee = None # not in MMGenTX
txobj_data = None # ""
disable_fee_check = False
txobj = None # ""
data = HexStr('')
def __init__(self,*args,**kwargs):
super(EthereumMMGenTX,self).__init__(*args,**kwargs)
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:
self.data = HexStr(open(opt.contract_data).read().strip())
self.disable_fee_check = True
@classmethod
def get_receipt(cls,txid):
return g.rpch.eth_getTransactionReceipt('0x'+txid)
@classmethod
def get_exec_status(cls,txid):
return int(g.rpch.eth_getTransactionReceipt('0x'+txid)['status'],16)
def is_replaceable(self): return True
def get_fee_from_tx(self):
return self.fee
def check_fee(self):
if self.disable_fee_check: return
assert self.fee <= g.proto.max_tx_fee
def get_hex_locktime(self): return None # TODO
@ -54,46 +81,65 @@ class EthereumMMGenTX(MMGenTX):
return True
return False
# hex data if signed, json if unsigned
def check_tx_hex_data(self):
# hex data if signed, json if unsigned: see create_raw()
def check_txfile_hex_data(self):
if self.check_sigs():
from ethereum.transactions import Transaction
import rlp
etx = rlp.decode(self.hex.decode('hex'),Transaction)
d = etx.to_dict()
self.txobj_data = {
'from': CoinAddr(d['sender'][2:]),
'to': CoinAddr(d['to'][2:]),
'amt': ETHAmt(d['value'],'wei'),
'gasPrice': ETHAmt(d['gasprice'],'wei'),
'nonce': ETHNonce(d['nonce'])
}
d = etx.to_dict() # ==> hex values have '0x' prefix, 0 is '0x'
for k in ('sender','to','data'):
if k in d: d[k] = d[k].replace('0x','',1)
o = { 'from': CoinAddr(d['sender']),
'to': CoinAddr(d['to']) if d['to'] else Str(''),
'amt': ETHAmt(d['value'],'wei'),
'gasPrice': ETHAmt(d['gasprice'],'wei'),
'startGas': ETHAmt(d['startgas'],'wei'),
'nonce': ETHNonce(d['nonce']),
'data': HexStr(d['data']) }
if o['data'] and not o['to']:
self.token_addr = TokenAddr(etx.creates.encode('hex'))
txid = CoinTxID(etx.hash.encode('hex'))
assert txid == self.coin_txid,"txid in tx.hex doesn't match value in MMGen tx file"
assert txid == self.coin_txid,"txid in tx.hex doesn't match value in MMGen transaction file"
else:
d = json.loads(self.hex)
self.txobj_data = {
'from': CoinAddr(d['from']),
'to': CoinAddr(d['to']),
'amt': ETHAmt(d['amt']),
'gasPrice': ETHAmt(d['gasPrice']),
'nonce': ETHNonce(d['nonce']),
'chainId': d['chainId']
}
self.gasPrice = self.txobj_data['gasPrice']
o = { 'from': CoinAddr(d['from']),
'to': CoinAddr(d['to']) if d['to'] else Str(''),
'amt': ETHAmt(d['amt']),
'gasPrice': ETHAmt(d['gasPrice']),
'startGas': ETHAmt(d['startGas']),
'nonce': ETHNonce(d['nonce']),
'chainId': Int(d['chainId']),
'data': HexStr(d['data']) }
self.tx_gas = o['startGas'] # approximate, but better than nothing
self.data = o['data']
if o['data'] and not o['to']: self.disable_fee_check = True
self.fee = self.fee_rel2abs(o['gasPrice'].toWei())
self.txobj = o
return d # 'token_addr','decimals' required by subclass
def create_raw(self):
for k in 'input','output':
assert len(getattr(self,k+'s')) == 1,'Transaction has more than one {}!'.format(k)
self.txobj_data = {
def make_txobj(self): # create_raw
self.txobj = {
'from': self.inputs[0].addr,
'to': self.outputs[0].addr,
'amt': self.outputs[0].amt,
'gasPrice': self.usr_rel_fee or self.fee_abs2rel(self.fee,in_eth=True),
'to': self.outputs[0].addr if self.outputs else Str(''),
'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)),
'chainId': g.rpch.parity_chainId()
'chainId': Int(g.rpch.parity_chainId(),16),
'data': self.data,
}
self.hex = json.dumps(dict([(k,str(v))for k,v in self.txobj_data.items()]))
# Instead of serializing tx data as with BTC, just create a JSON dump.
# This complicates things but means we avoid using the rlp library to deserialize the data,
# thus removing an attack vector
def create_raw(self):
assert len(self.inputs) == 1,'Transaction has more than one input!'
o_ok = (0,1) if self.data else (1,)
o_num = len(self.outputs)
assert o_num in o_ok,'Transaction has invalid number of outputs!'.format(o_num)
self.make_txobj()
self.hex = json.dumps(dict([(k,str(v))for k,v in self.txobj.items()]))
self.update_txid()
def del_output(self,idx): pass
@ -105,12 +151,15 @@ class EthereumMMGenTX(MMGenTX):
self.txid = MMGenTxID(make_chksum_6(self.hex).upper())
def get_blockcount(self):
return int(g.rpch.eth_blockNumber(),16)
return Int(g.rpch.eth_blockNumber(),16)
def process_cmd_args(self,cmd_args,ad_f,ad_w):
lc = len(cmd_args)
if lc != 1:
fs = '{} output{} specified, but Ethereum transactions must have only one'
if lc == 0 and self.data:
return
elif lc != 1:
fs = '{} output{} specified, but Ethereum transactions must have exactly one'
die(1,fs.format(lc,suf(lc)))
a = list(cmd_args)[0]
@ -124,6 +173,9 @@ class EthereumMMGenTX(MMGenTX):
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')
def select_unspent(self,unspent):
prompt = 'Enter an account to spend from: '
while True:
@ -142,13 +194,13 @@ class EthereumMMGenTX(MMGenTX):
def get_relay_fee(self): return ETHAmt(0) # TODO
# given absolute fee in ETH, return gas price in Gwei using tx_gas
def fee_abs2rel(self,abs_fee,in_eth=False): # in_eth not in MMGenTX
def fee_abs2rel(self,abs_fee,to_unit='Gwei'):
ret = ETHAmt(int(abs_fee.toWei() / self.tx_gas.toWei()),'wei')
return ret if in_eth else ret.toGwei()
return ret if to_unit == 'eth' else ret.to_unit(to_unit)
# get rel_fee (gas price) from network, return in native wei
def get_rel_fee_from_network(self):
return int(g.rpch.eth_gasPrice(),16),'eth_gasPrice' # ==> rel_fee,fe_type
return Int(g.rpch.eth_gasPrice(),16),'eth_gasPrice' # ==> rel_fee,fe_type
# given rel fee and units, return absolute fee using tx_gas
def convert_fee_spec(self,foo,units,amt,unit):
@ -157,7 +209,7 @@ class EthereumMMGenTX(MMGenTX):
# given rel fee in wei, return absolute fee using tx_gas (not in MMGenTX)
def fee_rel2abs(self,rel_fee):
assert type(rel_fee) is int,"'{}': incorrect type for fee estimate (not an integer)".format(rel_fee)
assert type(rel_fee) in (int,Int),"'{}': incorrect type for fee estimate (not an integer)".format(rel_fee)
return ETHAmt(rel_fee * self.tx_gas.toWei(),'wei')
# given fee estimate (gas price) in wei, return absolute fee, adjusting by opt.tx_fee_adj
@ -171,6 +223,8 @@ class EthereumMMGenTX(MMGenTX):
abs_fee = self.process_fee_spec(tx_fee,None,on_fail='return')
if abs_fee == False:
return False
elif self.disable_fee_check:
return abs_fee
elif abs_fee > g.proto.max_tx_fee:
m = '{} {c}: {} fee too large (maximum fee: {} {c})'
msg(m.format(abs_fee.hl(),desc,g.proto.max_tx_fee.hl(),c=g.coin))
@ -181,25 +235,67 @@ class EthereumMMGenTX(MMGenTX):
def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse):
m = {}
for k in ('in','out'):
m[k] = getattr(self,k+'puts')[0].mmid
m[k] = ' ' + m[k].hl() if m[k] else ' ' + MMGenID.hlc(nonmm_str)
if len(getattr(self,k+'puts')):
m[k] = getattr(self,k+'puts')[0].mmid if len(getattr(self,k+'puts')) else ''
m[k] = ' ' + m[k].hl() if m[k] else ' ' + MMGenID.hlc(nonmm_str)
fs = """From: {}{f_mmid}
To: {}{t_mmid}
Amount: {} ETH
Amount: {} {c}
Gas price: {g} Gwei
Nonce: {}\n\n""".replace('\t','')
Start gas: {G} Kwei
Nonce: {}
Data: {d}
\n""".replace('\t','')
keys = ('from','to','amt','nonce')
return fs.format( *(self.txobj_data[k].hl() for k in keys),
g=yellow(str(self.txobj_data['gasPrice'].toGwei())),
t_mmid=m['out'],
ld = len(self.txobj['data'])
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['startGas'].toKwei())),
t_mmid=m['out'] if len(self.outputs) else '',
f_mmid=m['in'])
def format_view_abs_fee(self):
return self.fee_rel2abs(self.txobj_data['gasPrice'].toWei()).hl()
fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei())
note = ' (max)' if self.data else ''
return fee.hl() + note
def format_view_rel_fee(self,terse): return ''
def format_view_verbose_footer(self): return '' # TODO
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)
def do_sign(self,d,wif,tx_num_str):
d_in = {'to': d['to'].decode('hex'),
'startgas': d['startGas'].toWei(),
'gasprice': d['gasPrice'].toWei(),
'value': d['amt'].toWei() if d['amt'] else 0,
'nonce': d['nonce'],
'data': d['data'].decode('hex')}
msg_r('Signing transaction{}...'.format(tx_num_str))
try:
from ethereum.transactions import Transaction
etx = Transaction(**d_in)
etx.sign(wif,d['chainId'])
import rlp
self.hex = rlp.encode(etx).encode('hex')
self.coin_txid = CoinTxID(etx.hash.encode('hex'))
msg('OK')
if d['data']:
self.token_addr = TokenAddr(etx.creates.encode('hex'))
except Exception as e:
m = "{!r}: transaction signing failed!"
msg(m.format(e[0]))
return False
return self.check_sigs()
def sign(self,tx_num_str,keys): # return true or false; don't exit
if self.marked_signed():
@ -209,32 +305,7 @@ class EthereumMMGenTX(MMGenTX):
if not self.check_correct_chain(on_fail='return'):
return False
wif = keys[0].sec.wif
d = self.txobj_data
out = { 'to': '0x'+d['to'],
'startgas': self.tx_gas.toWei(),
'gasprice': d['gasPrice'].toWei(),
'value': d['amt'].toWei(),
'nonce': d['nonce'],
'data': ''}
msg_r('Signing transaction{}...'.format(tx_num_str))
try:
from ethereum.transactions import Transaction
etx = Transaction(**out)
etx.sign(wif,int(d['chainId'],16))
import rlp
self.hex = rlp.encode(etx).encode('hex')
self.coin_txid = CoinTxID(etx.hash.encode('hex'))
msg('OK')
except Exception as e:
m = "{!r}: transaction signing failed!"
msg(m.format(e[0]))
return False
return self.check_sigs()
return self.do_sign(self.txobj,keys[0].sec.wif,tx_num_str)
def get_status(self,status=False): pass # TODO
@ -247,9 +318,9 @@ class EthereumMMGenTX(MMGenTX):
bogus_send = os.getenv('MMGEN_BOGUS_SEND')
fee = self.fee_rel2abs(self.txobj_data['gasPrice'].toWei())
fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei())
if fee > g.proto.max_tx_fee:
if not self.disable_fee_check and fee > g.proto.max_tx_fee:
die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
fee,g.proto.name.capitalize(),g.proto.max_tx_fee,g.coin))
@ -275,6 +346,15 @@ class EthereumMMGenTX(MMGenTX):
self.add_blockcount()
return True
class EthereumMMGenBumpTX(MMGenBumpTX): pass
class EthereumMMGenBumpTX(EthereumMMGenTX,MMGenBumpTX):
def choose_output(self): pass
def set_min_fee(self):
self.min_fee = ETHAmt(self.fee * Decimal('1.101'))
def update_fee(self,foo,fee):
self.fee = fee
class EthereumMMGenSplitTX(MMGenSplitTX): pass
class EthereumDeserializedTX(DeserializedTX): pass

View file

@ -102,20 +102,18 @@ if not silent:
tx.set_min_fee()
if not [o.amt for o in tx.outputs if o.amt >= tx.min_fee]:
die(1,'Transaction cannot be bumped.' +
'\nAll outputs have less than the minimum fee ({} {})'.format(tx.min_fee,g.coin))
tx.check_bumpable()
msg('Creating new transaction')
op_idx = tx.choose_output()
if not silent:
msg('Minimum fee for new transaction: {} {}'.format(tx.min_fee,g.coin))
msg('Minimum fee for new transaction: {} {}'.format(tx.min_fee.hl(),g.coin))
fee = tx.get_usr_fee_interactive(tx_fee=opt.tx_fee,desc='User-selected')
tx.update_output_amt(op_idx,tx.sum_inputs()-tx.sum_outputs(exclude=op_idx)-fee)
tx.update_fee(op_idx,fee)
d = tx.get_fee_from_tx()
assert d == fee and d <= g.proto.max_tx_fee

View file

@ -28,26 +28,30 @@ opts_data = lambda: {
'usage': '[opts] <addr,amt> ... [change addr] [addr file] ...',
'sets': ( ('yes', True, 'quiet', True), ),
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-a, --tx-fee-adj= f Adjust transaction fee by factor 'f' (see below)
-B, --no-blank Don't blank screen before displaying unspent outputs
-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
-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
calculated using network fee estimation.
-i, --info Display unspent outputs and exit
-L, --locktime= t Lock time (block height or unix seconds) (default: 0)
-m, --minconf= n Minimum number of confirmations required to spend
outputs (default: 1)
-q, --quiet Suppress warnings; overwrite files without prompting
-r, --rbf Make transaction BIP 125 replaceable (replace-by-fee)
-v, --verbose Produce more verbose output
-V, --vsize-adj= f Adjust transaction's estimated vsize by factor 'f'
-y, --yes Answer 'yes' to prompts, suppress non-essential output
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-a, --tx-fee-adj= f Adjust transaction fee by factor 'f' (see below)
-B, --no-blank Don't blank screen before displaying unspent outputs
-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
-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
calculated using network fee estimation.
-g, --tx-gas= g Specify start gas amount in Wei (ETH only)
-i, --info Display unspent outputs and exit
-I, --inputs= i Specify transaction inputs (comma-separated list of
MMGen IDs or coin addresses). Note that ALL unspent
outputs associated with each address will be included.
-L, --locktime= t Lock time (block height or unix seconds) (default: 0)
-m, --minconf= n Minimum number of confirmations required to spend
outputs (default: 1)
-q, --quiet Suppress warnings; overwrite files without prompting
-r, --rbf Make transaction BIP 125 replaceable (replace-by-fee)
-v, --verbose Produce more verbose output
-V, --vsize-adj= f Adjust transaction's estimated vsize by factor 'f'
-y, --yes Answer 'yes' to prompts, suppress non-essential output
""",
'options_fmt_args': lambda: dict(
g=g,cu=g.coin,

View file

@ -41,9 +41,13 @@ opts_data = lambda: {
{fu} (an integer followed by {fl}).
See FEE SPECIFICATION below. If omitted, fee will be
calculated using network fee estimation.
-g, --tx-gas= g Specify start gas amount in Wei (ETH only)
-H, --hidden-incog-input-params=f,o Read hidden incognito data from file
'f' at offset 'o' (comma-separated)
-i, --in-fmt= f Input is from wallet format 'f' (see FMT CODES below)
-I, --inputs= i Specify transaction inputs (comma-separated list of
MMGen IDs or coin addresses). Note that ALL unspent
outputs associated with each address will be included.
-l, --seed-len= l Specify wallet seed length of 'l' bits. This option
is required only for brainwallet and incognito inputs
with non-standard (< {g.seed_len}-bit) seed lengths.
@ -94,9 +98,13 @@ kl = get_keylist(opt)
if kl and kal: kl.remove_dup_keys(kal)
tx = MMGenTX(caller='txdo')
tx.create(cmd_args,int(opt.locktime or 0))
txsign(tx,seed_files,kl,kal)
tx.write_to_file(ask_write=False)
tx.send(exit_on_fail=True)
tx.write_to_file(ask_overwrite=False,ask_write=False)

View file

@ -336,13 +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 toSatoshi(self): return int(Decimal(self) / self.satoshi)
def to_unit(self,unit): return int(Decimal(self) / getattr(self,unit))
@classmethod
def fmtc(cls):
raise NotImplementedError
def fmt(self,fs=None,color=False,suf=''):
def fmt(self,fs=None,color=False,suf='',prec=1000):
if fs == None: fs = self.amt_fs
s = str(int(self)) if int(self) == self else self.normalize().__format__('f')
if '.' in fs:
@ -350,9 +351,9 @@ class BTCAmt(Decimal,Hilite,InitErrors):
ss = s.split('.',1)
if len(ss) == 2:
a,b = ss
ret = a.rjust(p1) + '.' + (b+suf).ljust(p2+len(suf))
ret = a.rjust(p1) + '.' + ((b+suf).ljust(p2+len(suf)))[:prec]
else:
ret = s.rjust(p1) + suf + ' ' * (p2+1)
ret = s.rjust(p1) + suf + (' ' * (p2+1))[:prec+1-len(suf)]
else:
ret = s.ljust(int(fs))
return self.colorize(ret,color=color)
@ -424,7 +425,7 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject):
def is_for_chain(self,chain):
from mmgen.globalvars import g
if g.coin in ('ETH','ETC'):
if g.proto.__name__[:8] == 'Ethereum':
return True
def pfx_ok(pfx):
@ -562,6 +563,9 @@ class HexStr(str,Hilite,InitErrors):
m = "{!r}: value cannot be converted to {} (value is {})"
return cls.init_fail(m.format(s,cls.__name__,e[0]),on_fail)
class Str(str,Hilite): pass
class Int(int,Hilite): pass
class HexStrWithWidth(HexStr):
color = 'nocolor'
trunc_ok = False

View file

@ -86,6 +86,7 @@ def opt_postproc_initializations():
if g.platform == 'win': start_mscolor()
g.coin = g.coin.upper() # allow user to use lowercase
g.dcoin = g.coin
def set_data_dir_root():
g.data_dir_root = os.path.normpath(os.path.expanduser(opt.data_dir)) if opt.data_dir else \
@ -152,7 +153,11 @@ def override_from_cfg_file(cfg_data):
if name in g.cfg_file_opts:
pfx,cfg_var = name.split('_',1)
if pfx in CoinProtocol.coins:
cls,attr = CoinProtocol(pfx,False),cfg_var
tn = False
cv1,cv2 = cfg_var.split('_',1)
if cv1 in ('mainnet','testnet'):
tn,cfg_var = (cv1 == 'testnet'),cv2
cls,attr = CoinProtocol(pfx,tn),cfg_var
else:
cls,attr = g,name
setattr(cls,attr,set_for_type(val,getattr(cls,attr),attr,src=g.cfg_file))
@ -339,6 +344,7 @@ 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
if ret == False:
msg("'{}': invalid {}\n(not a {} amount or {} specification)".format(
val,desc,g.coin.upper(),MMGenTX().rel_fee_desc))
@ -495,7 +501,8 @@ def check_opts(usr_opts): # Returns false if any check fails
if not opt_is_in_list(val.lower(),CoinProtocol.coins.keys(),'coin'): return False
elif key == 'rbf':
if not g.proto.cap('rbf'):
die(1,'--rbf requested, but {} does not support replace-by-fee transactions'.format(g.coin))
msg('--rbf requested, but {} does not support replace-by-fee transactions'.format(g.coin))
return False
elif key in ('bob','alice'):
from mmgen.regtest import daemon_dir
m = "Regtest (Bob and Alice) mode not set up yet. Run '{}-regtest setup' to initialize."

View file

@ -64,6 +64,7 @@ def _b58chk_decode(s):
class BitcoinProtocol(MMGenObject):
name = 'bitcoin'
daemon_name = 'bitcoind'
daemon_family = 'bitcoind'
addr_ver_num = { 'p2pkh': ('00','1'), 'p2sh': ('05','3') }
wif_ver_num = { 'std': '80' }
mmtypes = ('L','C','S','B')
@ -301,6 +302,7 @@ class EthereumProtocol(DummyWIF,BitcoinProtocolAddrgen):
data_subdir = ''
daemon_name = 'parity'
daemon_family = 'parity'
rpc_port = 8545
mmcaps = ('key','addr','rpc')
coin_amt = ETHAmt

View file

@ -32,11 +32,12 @@ class TwUnspentOutputs(MMGenObject):
return MMGenObject.__new__(altcoin_subclass(cls,'tw','TwUnspentOutputs'),*args,**kwargs)
txid_w = 64
show_txid = True
disp_type = 'btc'
can_group = True
hdr_fmt = 'UNSPENT OUTPUTS (sort order: {}) Total {}: {}'
desc = 'unspent outputs'
dump_fn_pfx = 'listunspent'
prompt_fs = 'Total to spend, excluding fees: {} {}\n\n'
prompt = """
Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
@ -49,6 +50,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
txid = MMGenListItemAttr('txid','CoinTxID')
vout = MMGenListItemAttr('vout',int,typeconv=False)
amt = MMGenImmutableAttr('amt',g.proto.coin_amt.__name__)
amt2 = MMGenListItemAttr('amt2',g.proto.coin_amt.__name__)
label = MMGenListItemAttr('label','TwComment',reassign_ok=True)
twmmid = MMGenImmutableAttr('twmmid','TwMMGenID')
addr = MMGenImmutableAttr('addr','CoinAddr')
@ -78,8 +80,10 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
self.sort_key = 'age'
self.do_sort()
self.total = self.get_total_coin()
self.disp_prec = self.get_display_precision()
g.dcoin = g.dcoin or g.coin
def get_display_precision(self):
return g.proto.coin_amt.max_prec
def get_total_coin(self):
return sum(i.amt for i in self.unspent)
@ -157,7 +161,6 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
def format_for_display(self):
unsp = self.unspent
# unsp.pdie()
self.set_term_columns()
# allow for 7-digit confirmation nums
@ -182,16 +185,16 @@ 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())]
if self.show_txid:
fs = u' {n:%s} {t:%s} {v:2} {a} {A} {c:<}' % (col1_w,tx_w)
else:
fs = u' {n:%s} {a} {A} {c:<}' % col1_w
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]
out += [fs.format( n='Num',
t='TXid'.ljust(tx_w - 5) + ' Vout',
v='',
a='Address'.ljust(addr_w),
A='Amt({})'.format(g.dcoin).ljust(g.proto.coin_amt.max_prec+4),
c=('Confs','Age(d)')[self.show_days])]
A='Amt({})'.format(g.dcoin).ljust(self.disp_prec+3),
A2=' Amt({})'.format(g.coin).ljust(self.disp_prec+4),
c=('Confs','Age(d)')[self.show_days]
).rstrip()]
for n,i in enumerate(unsp):
addr_dots = '|' + '.'*(addr_w-1)
@ -214,26 +217,28 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
else i.txid[:tx_w-len(txdots)]+txdots,
v=i.vout,
a=addr_out,
A=i.amt.fmt(color=True),
c=i.days if self.show_days else i.confs))
A=i.amt.fmt(color=True,prec=self.disp_prec),
A2=(i.amt2.fmt(color=True,prec=self.disp_prec) if i.amt2 is not None else ''),
c=i.days if self.show_days else i.confs
).rstrip())
self.fmt_display = '\n'.join(out) + '\n'
# unsp.pdie()
return self.fmt_display
def format_for_printing(self,color=False):
addr_w = max(len(i.addr) for i in self.unspent)
mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in self.unspent) or 12 # DEADBEEF:S:1
if self.show_txid:
fs = ' {n:4} {t:%s} {a} {m} {A:%s} {c:<8} {g:<6} {l}' % (self.txid_w+3,g.proto.coin_amt.max_prec+4)
else:
fs = ' {n:4} {a} {m} {A:%s} {c:<8} {g:<6} {l}' % (g.proto.coin_amt.max_prec+4)
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
}[self.disp_type]
out = [fs.format( n='Num',
t='Tx ID,Vout',
a='Address'.ljust(addr_w),
m='MMGen ID'.ljust(mmid_w+1),
A='Amount({})'.format(g.dcoin),
A='Amount({})'.format(g.dcoin).ljust(amt_w+1),
A2='Amount({})'.format(g.coin),
c='Confs',
g='Age(d)',
l='Label')]
@ -248,6 +253,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
m=MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen'
else 'Non-{}'.format(g.proj_name),width=mmid_w,color=color),
A=i.amt.fmt(color=color),
A2=(i.amt2.fmt(color=color) if i.amt2 is not None else ''),
c=i.confs,
g=i.days,
l=i.label.hl(color=color) if i.label else
@ -291,8 +297,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
return n,s
def view_and_sort(self,tx):
fs = 'Total to spend, excluding fees: {} {}\n\n'
txos = fs.format(tx.sum_outputs().hl(),g.dcoin) if tx.outputs else ''
txos = self.prompt_fs.format(tx.sum_outputs().hl(),g.dcoin) if tx.outputs else ''
prompt = txos + self.prompt.strip()
self.display()
msg(prompt)
@ -465,7 +470,7 @@ class TwAddrList(MMGenDict):
age=mmid.confs / (1,confs_per_day)[show_days] if hasattr(mmid,'confs') else '-'
))
return '\n'.join(out + ['\nTOTAL: {} {}'.format(self.total.hl(color=True),g.coin)])
return '\n'.join(out + ['\nTOTAL: {} {}'.format(self.total.hl(color=True),g.dcoin)])
class TrackingWallet(MMGenObject):

View file

@ -74,7 +74,7 @@ def mmaddr2coinaddr(mmaddr,ad_w,ad_f):
coin_addr = ad_f.mmaddr2coinaddr(mmaddr)
if coin_addr:
msg(wmsg('addr_in_addrfile_only').format(mmaddr))
if not keypress_confirm('Continue anyway?'):
if not (opt.yes or keypress_confirm('Continue anyway?')):
sys.exit(1)
else:
die(2,wmsg('addr_not_found').format(mmaddr))
@ -212,7 +212,6 @@ class MMGenTX(MMGenObject):
sig_ext = 'sigtx'
txid_ext = 'txid'
desc = 'transaction'
chg_msg_fs = 'Transaction produces {} {} in change'
fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})'
no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
rel_fee_desc = 'satoshis per byte'
@ -222,6 +221,8 @@ class MMGenTX(MMGenObject):
txview_ftr_fs = 'Total input: {i} {d}\nTotal output: {o} {d}\nTX fee: {a} {c}{r}\n'
txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n'
usr_fee_prompt = 'Enter transaction fee: '
fee_is_approximate = False
fn_fee_unit = 'satoshi'
msg_low_coin = 'Selected outputs insufficient to fund this transaction ({} {} needed)'
msg_no_change_output = """
@ -291,8 +292,6 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
self.caller = caller
self.locktime = None
g.dcoin = g.dcoin or g.coin
if filename:
self.parse_tx_file(filename,coin_sym_only=coin_sym_only,silent_open=silent_open)
if coin_sym_only: return
@ -330,6 +329,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
self.outputs.pop(idx)
def sum_outputs(self,exclude=None):
if not len(self.outputs): return g.proto.coin_amt(0)
olist = self.outputs if exclude == None else \
self.outputs[:exclude] + self.outputs[exclude+1:]
return g.proto.coin_amt(sum(e.amt for e in olist))
@ -484,8 +484,9 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
return ret
# convert absolute BTC fee to satoshis-per-byte using estimated size
def fee_abs2rel(self,abs_fee):
return int(abs_fee/g.proto.coin_amt.min_coin_unit/self.estimate_size())
def fee_abs2rel(self,abs_fee,to_unit=None):
unit = getattr(g.proto.coin_amt,to_unit or 'min_coin_unit')
return int(abs_fee / unit / self.estimate_size())
def get_rel_fee_from_network(self): # rel_fee is in BTC/kB
try:
@ -559,9 +560,10 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
abs_fee = self.convert_and_check_fee(tx_fee,desc)
if abs_fee:
m = ('',' (after {}x adjustment)'.format(opt.tx_fee_adj))[opt.tx_fee_adj != 1]
p = u'{} TX fee{}: {} {} ({} {})\n'.format(
p = u'{} TX fee{}: {}{} {} ({} {})\n'.format(
desc,
m,
('',u'')[self.fee_is_approximate],
abs_fee.hl(),
g.coin,
pink(str(self.fee_abs2rel(abs_fee))),
@ -953,7 +955,9 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
self.txid,
('-'+g.dcoin,'')[g.coin=='BTC'],
self.send_amt,
('',',{}'.format(self.fee_abs2rel(self.get_fee_from_tx())))[self.is_rbf()],
('',',{}'.format(self.fee_abs2rel(
self.get_fee_from_tx(),to_unit=self.fn_fee_unit))
)[self.is_replaceable()],
('',',tl={}'.format(tl))[bool(tl)],
tn,self.ext,
x=u'' if g.debug_utf8 else '')
@ -991,11 +995,11 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
get_char('Press any key to continue: ')
msg('')
# def is_rbf_from_rpc(self):
# def is_replaceable_from_rpc(self):
# dec_tx = g.rpch.decoderawtransaction(self.hex)
# return None < dec_tx['vin'][0]['sequence'] <= g.max_int - 2
def is_rbf(self):
def is_replaceable(self):
return self.inputs[0].sequence == g.max_int - 2
def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse):
@ -1075,14 +1079,14 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
a=self.send_amt.hl(),
c=g.dcoin,
t=self.timestamp,
r=(red('False'),green('True'))[self.is_rbf()],
r=(red('False'),green('True'))[self.is_replaceable()],
s=self.marked_signed(color=True),
l=(green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)])
if self.chain != 'mainnet':
out += green('Chain: {}\n'.format(self.chain.upper()))
if self.coin_txid:
out += '{} TxID: {}\n'.format(g.dcoin,self.coin_txid.hl())
out += '{} TxID: {}\n'.format(g.coin,self.coin_txid.hl())
enl = ('\n','')[bool(terse)]
out += enl
if self.label:
@ -1101,7 +1105,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
return out # TX label might contain non-ascii chars
def check_tx_hex_data(self):
def check_txfile_hex_data(self):
self.hex = HexStr(self.hex,on_fail='raise')
def parse_tx_file(self,infile,coin_sym_only=False,silent_open=False):
@ -1116,7 +1120,8 @@ 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)
assert len(d),'no {}!'.format(desc)
if not (desc == 'outputs' and g.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 = (
(MMGenTX.MMGenTxOutput,MMGenTX.MMGenTxOutputList),
@ -1179,7 +1184,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
desc = 'block count in metadata'
self.blockcount = int(blockcount)
desc = 'transaction hex data'
self.check_tx_hex_data()
self.check_txfile_hex_data()
# the following ops will all fail if g.coin doesn't match self.coin
desc = 'coin type in metadata'
assert self.coin == g.coin,self.coin
@ -1194,6 +1199,8 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
if not self.chain and not self.inputs[0].addr.is_for_chain('testnet'):
self.chain = 'mainnet'
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:
@ -1219,6 +1226,9 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain'
rdie(2,fs.format(g.proj_name))
if not self.outputs:
die(2,'At least one output must be specified on the command line')
def get_outputs_from_cmdline(self,cmd_args):
from mmgen.addr import AddrList,AddrData
addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext]
@ -1233,9 +1243,6 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
self.process_cmd_args(cmd_args,ad_f,ad_w)
if not self.outputs:
die(2,'At least one output must be specified on the command line')
self.add_mmaddrs_to_outputs(ad_w,ad_f)
self.check_dup_addrs('outputs')
@ -1250,9 +1257,9 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
return selected
msg('Unspent output number must be <= {}'.format(len(unspent)))
def check_sufficient_funds(self,inputs,foo):
if self.send_amt > inputs:
msg(self.msg_low_coin.format(self.send_amt-inputs,g.coin))
def check_sufficient_funds(self,inputs_sum,foo):
if self.send_amt > inputs_sum:
msg(self.msg_low_coin.format(self.send_amt-inputs_sum,g.coin))
return False
return True
@ -1262,23 +1269,55 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
def warn_insufficient_chg(self,change_amt):
msg(self.msg_low_coin.format(g.proto.coin_amt(-change_amt).hl(),g.coin))
def final_inputs_ok_msg(self,change_amt):
m = 'Transaction produces {} {} in change'
return m.format(g.proto.coin_amt(change_amt).hl(),g.coin)
def select_unspent_cmdline(self,unspent):
sel_nums = []
for i in opt.inputs.split(','):
ls = len(sel_nums)
if is_mmgen_id(i):
for j in range(len(unspent)):
if unspent[j].twmmid == i:
sel_nums.append(j+1)
elif is_coin_addr(i):
for j in range(len(unspent)):
if unspent[j].addr == i:
sel_nums.append(j+1)
else:
die(1,"'{}': not an MMGen ID or coin address".format(i))
ldiff = len(sel_nums) - ls
if ldiff:
sel_inputs = ','.join([str(i) for i in sel_nums[-ldiff:]])
ul = unspent[sel_nums[-1]-1]
mmid_disp = ' (' + ul.twmmid + ')' if ul.twmmid.type == 'mmgen' else ''
msg('Adding input{}: {} {}{}'.format(suf(ldiff),sel_inputs,ul.addr,mmid_disp))
else:
die(1,"'{}': address not found in tracking wallet".format(i))
return set(sel_nums) # silently discard duplicates
def get_inputs_from_user(self,tw):
while True:
sel_nums = self.select_unspent(tw.unspent)
us_f = ('select_unspent','select_unspent_cmdline')[bool(opt.inputs)]
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])
t_inputs = sum(s.amt for s in sel_unspent)
if not self.check_sufficient_funds(t_inputs,sel_unspent):
inputs_sum = sum(s.amt for s in sel_unspent)
if not self.check_sufficient_funds(inputs_sum,sel_unspent):
continue
non_mmaddrs = [i for i in sel_unspent if i.twmmid.type == 'non-mmgen']
if non_mmaddrs and self.caller != 'txdo':
msg(self.msg_non_mmgen_inputs.format(
', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs])))))
if not keypress_confirm('Accept?'):
if not (opt.yes or keypress_confirm('Accept?')):
continue
self.copy_inputs_from_tw(sel_unspent) # makes self.inputs
@ -1287,9 +1326,9 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
change_amt = self.get_change_amt()
if change_amt >= 0:
p = self.chg_msg_fs.format(change_amt.hl(),g.coin)
if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
if change_amt >= 0: # TODO: show both ETH and token amts remaining
p = self.final_inputs_ok_msg(change_amt)
if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
if opt.yes: msg(p)
return change_amt
else:
@ -1309,7 +1348,10 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
from mmgen.tw import TwUnspentOutputs
tw = TwUnspentOutputs(minconf=opt.minconf)
tw.view_and_sort(self)
if not opt.inputs:
tw.view_and_sort(self)
tw.display_total()
if do_info: sys.exit(0)
@ -1334,7 +1376,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
else:
self.update_output_amt(chg_idx,g.proto.coin_amt(change_amt))
if not self.send_amt:
if not self.send_amt and len(self.outputs):
self.send_amt = change_amt
if not opt.yes:
@ -1360,15 +1402,18 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
class MMGenBumpTX(MMGenTX):
def __new__(cls,*args,**kwargs):
return MMGenTX.__new__(altcoin_subclass(cls,'tx','MMGenBumpTX'),*args,**kwargs)
min_fee = None
bump_output_idx = None
def __init__(self,filename,send=False):
super(type(self),self).__init__(filename)
super(MMGenBumpTX,self).__init__(filename)
if not self.is_rbf():
die(1,"Transaction '{}' is not replaceable (RBF)".format(self.txid))
if not self.is_replaceable():
die(1,"Transaction '{}' is not replaceable".format(self.txid))
# If sending, require tx to have been signed
if send:
@ -1380,6 +1425,11 @@ class MMGenBumpTX(MMGenTX):
self.coin_txid = ''
self.mark_raw()
def check_bumpable(self):
if not [o.amt for o in self.outputs if o.amt >= self.min_fee]:
die(1,'Transaction cannot be bumped.' +
'\nAll outputs have less than the minimum fee ({} {})'.format(self.min_fee,g.coin))
def choose_output(self):
chg_idx = self.get_chg_output_idx()
init_reply = opt.output_to_reduce
@ -1412,15 +1462,18 @@ class MMGenBumpTX(MMGenTX):
def set_min_fee(self):
self.min_fee = self.sum_inputs() - self.sum_outputs() + self.get_relay_fee()
def update_fee(self,op_idx,fee):
self.update_output_amt(op_idx,self.sum_inputs()-self.sum_outputs(exclude=op_idx)-fee)
def convert_and_check_fee(self,tx_fee,desc):
ret = super(type(self),self).convert_and_check_fee(tx_fee,desc)
ret = super(MMGenBumpTX,self).convert_and_check_fee(tx_fee,desc)
if ret < self.min_fee:
msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} {})'.format(
ret,desc,self.min_fee,self.fee_abs2rel(self.min_fee),self.rel_fee_desc,c=g.coin))
ret,desc,self.min_fee,self.fee_abs2rel(self.min_fee.hl()),self.rel_fee_desc,c=g.coin))
return False
output_amt = self.outputs[self.bump_output_idx].amt
if ret >= output_amt:
msg('{} {c}: {} fee too large. Maximum fee: <{} {c}'.format(ret,desc,output_amt,c=g.coin))
msg('{} {c}: {} fee too large. Maximum fee: <{} {c}'.format(ret.hl(),desc,output_amt.hl(),c=g.coin))
return False
return ret

View file

@ -211,6 +211,7 @@ i_eth='Ethereum'
s_eth='Testing transaction and tracking wallet operations for Ethereum'
t_eth=(
"$test_py -On --coin=eth ref_tx_chk"
"$test_py -On --coin=eth --testnet=1 ref_tx_chk"
"$test_py -On ethdev"
)
f_eth='Ethereum tests completed'

View file

@ -111,7 +111,6 @@ setup(
'mmgen.addr',
'mmgen.altcoin',
'mmgen.bech32',
'mmgen.protocol',
'mmgen.color',
'mmgen.common',
'mmgen.crypto',
@ -123,6 +122,7 @@ setup(
'mmgen.mn_tirosh',
'mmgen.obj',
'mmgen.opts',
'mmgen.protocol',
'mmgen.regtest',
'mmgen.rpc',
'mmgen.seed',

View file

@ -50,6 +50,7 @@ def my_send(p,t,delay=send_delay,s=False):
return ret
def my_expect(p,s,t='',delay=send_delay,regex=False,nonl=False,silent=False):
quo = ('',"'")[type(s) == str]
if not silent:

View file

@ -1,6 +1,6 @@
5eb350
ETH FOUNDATION BC79AB 0.123 20180530_125230 7513928
{"nonce": "0", "chainId": "0x1", "from": "e704b6cfd9f0edb2e6cfbd0c913438d37ede7b35", "to": "62ff8e4dbd251b98102e3fb5e4b14119e24cadde", "amt": "0.123", "gasPrice": "0.000000050"}
0a7b6f
ETH FOUNDATION 4ED554 0.123 20180530_125230 7513928
{"nonce": "0", "chainId": "1", "from": "e704b6cfd9f0edb2e6cfbd0c913438d37ede7b35", "to": "62ff8e4dbd251b98102e3fb5e4b14119e24cadde", "amt": "0.123", "gasPrice": "0.000000050"}
[{'confs': 0, 'addr': 'e704b6cfd9f0edb2e6cfbd0c913438d37ede7b35', 'vout': 0, 'txid': '0000000000000000000000000000000000000000000000000000000000000000', 'label': u'', 'amt': '1.234567', 'mmid': '98831F3A:E:1'}]
[{'mmid': '98831F3A:E:31', 'amt': '0.123', 'addr': '62ff8e4dbd251b98102e3fb5e4b14119e24cadde'}]
qRHzrPVpZFYxnQvk3atLzUtp41bZupJ2UQNnKe3ZnmqFsEngS6vaCCvesKKy9khzVq6y2RqarVBcZLnjtXxMpbAcdEtyBWiBYmZdoU8SN4uAbroHT1c7gEbmUNVKKdqHD86ZRRqDNpdh1ztmLiMAy3ibM83puwJHNpGGHgUGjZ1RSEgyVKCs2rZ9wXN8rBMibDDPYo1LgtAst2FkB36Mgf4Vf7ekoRAdiRNGd5YZ3RXAVsSdnZcyn4rdeQDMDkCq7JJDoB25eNEuXQutZFUcf2fEfxkMbW1sXJDNFQq

View file

@ -1,6 +1,6 @@
8f7b85
ETH KOVAN F04889 0.123 20180530_125230 7513928
{"nonce": "0", "chainId": "0x2a", "from": "97ccc3a117b3696340c42561361054b1c9c793d5", "to": "07f575951e67f855ceffe512ee33a362e177924f", "amt": "0.123", "gasPrice": "0.000000008"}
bc835b
ETH KOVAN 7EE763 0.123 20180530_125230 7513928
{"nonce": "0", "chainId": "42", "from": "97ccc3a117b3696340c42561361054b1c9c793d5", "to": "07f575951e67f855ceffe512ee33a362e177924f", "amt": "0.123", "gasPrice": "0.000000008"}
[{'confs': 0, 'addr': '97ccc3a117b3696340c42561361054b1c9c793d5', 'label': u'', 'amt': '1.234567', 'mmid': '98831F3A:E:1'}]
[{'mmid': '98831F3A:E:31', 'amt': '0.123', 'addr': '07f575951e67f855ceffe512ee33a362e177924f'}]
qRHzrPVpZFYxnQvk3atLzUtp41bZupJ2UQNnKe3ZnmqFsEngS6vaCCvesKKy9khzVq6y2RqarVBcZLnjtXxMpbAcdEtyBWiBYmZdoU8SN4uAbroHT1c7gEbmUNVKKdqHD86ZRRqDNpdh1ztmLiMAy3ibM83puwJHNpGGHgUGjZ1RSEgyVKCs2rZ9wXN8rBMibDDPYo1LgtAst2FkB36Mgf4Vf7ekoRAdiRNGd5YZ3RXAVsSdnZcyn4rdeQDMDkCq7JJDoB25eNEuXQutZFUcf2fEfxkMbW1sXJDNFQq

View file

@ -20,7 +20,8 @@
test/test.py: Test suite for the MMGen suite
"""
import sys,os,subprocess,shutil,time,re
import sys,os,subprocess,shutil,time,re,json
from decimal import Decimal
repo_root = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]),os.pardir)))
os.chdir(repo_root)
@ -29,7 +30,7 @@ sys.path.__setitem__(0,repo_root)
# Import these _after_ local path's been added to sys.path
from mmgen.common import *
from mmgen.test import *
from mmgen.protocol import CoinProtocol
from mmgen.protocol import CoinProtocol,init_coin
set_debug_all()
@ -54,7 +55,7 @@ ref_wallet_brainpass = 'abc'
ref_wallet_hash_preset = '1'
ref_wallet_incog_offset = 123
from mmgen.obj import MMGenTXLabel,PrivKey
from mmgen.obj import MMGenTXLabel,PrivKey,ETHAmt
from mmgen.addr import AddrGenerator,KeyGenerator,AddrList,AddrData,AddrIdxList
ref_tx_label_jp = u'必要なのは、信用ではなく暗号化された証明に基づく電子取引システムであり、これにより希望する二者が信用できる第三者機関を介さずに直接取引できるよう' # 72 chars ('W'ide)
@ -160,6 +161,8 @@ sys.argv = [sys.argv[0]] + ['--data-dir',data_dir] + sys.argv[1:]
cmd_args = opts.init(opts_data)
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
altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin
tn_ext = ('','.testnet')[g.testnet]
@ -183,6 +186,11 @@ rtBals = {
'bch': ('499.9999484','399.9999194','399.9998972','399.9997692','6.79000000','993.20966920','999.99966920'),
'ltc': ('5499.99744','5399.994425','5399.993885','5399.987535','13.00000000','10986.93753500','10999.93753500'),
}[coin_sel]
rtBals_gb = {
'btc': ('116.77629233','283.22339537'),
'bch': ('WIP'),
'ltc': ('WIP'),
}[coin_sel]
rtBobOp3 = {'btc':'S:2','bch':'L:3','ltc':'S:2'}[coin_sel]
if opt.segwit and 'S' not in g.proto.mmtypes:
@ -571,8 +579,8 @@ cfgs = {
'359FD5-BCH[6.68868,tl=1320969600].testnet.rawtx'),
'ltc': ('AF3CDF-LTC[620.76194,1453,tl=1320969600].rawtx',
'A5A1E0-LTC[1454.64322,1453,tl=1320969600].testnet.rawtx'),
'eth': ('BC79AB-ETH[0.123].rawtx',
'F04889-ETH[0.123].testnet.rawtx'),
'eth': ('4ED554-ETH[0.123].rawtx',
'7EE763-ETH[0.123].testnet.rawtx'),
},
'ic_wallet': u'98831F3A-5482381C-18460FB1[256,1].mmincog',
'ic_wallet_hex': u'98831F3A-1630A9F2-870376A9[256,1].mmincox',
@ -613,6 +621,9 @@ dfl_words = os.path.join(ref_dir,cfgs['8']['seed_id']+'.mmwords')
eth_addr = '00a329c0648769a73afac7f9381e08fb43dbea72'
eth_key = '4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7'
eth_args = [u'--outdir={}'.format(cfgs['22']['tmpdir']),'--coin=eth','--rpc-port=8549','--quiet']
eth_burn_addr = 'deadbeef'*5
eth_amt1 = '999999.12345689012345678'
eth_amt2 = '888.111122223333444455'
from copy import deepcopy
for a,b in (('6','11'),('7','12'),('8','13')):
@ -651,7 +662,6 @@ cmd_group['main'] = OrderedDict([
['passchg_usrlabel',(5,'password, label and hash preset change (interactive label)',[[['mmdat',pwfile],1]])],
['walletchk_newpass',(5,'wallet check with new pw, label and hash preset',[[['mmdat',pwfile],5]])],
['addrgen', (1,'address generation', [[['mmdat',pwfile],1]])],
['addrimport', (1,'address import', [[['addrs'],1]])],
['txcreate', (1,'transaction creation', [[['addrs'],1]])],
['txbump', (1,'transaction fee bumping (no send)',[[['rawtx'],1]])],
['txsign', (1,'transaction signing', [[['mmdat','rawtx',pwfile,'txbump'],1]])],
@ -676,6 +686,8 @@ cmd_group['main'] = OrderedDict([
['keyaddrgen', (1,'key-address file generation', [[['mmdat',pwfile],1]])],
['txsign_keyaddr',(1,'transaction signing with key-address file', [[['akeys.mmenc','rawtx'],1]])],
['txcreate_ni', (1,'transaction creation (non-interactive)', [[['addrs'],1]])],
['walletgen2',(2,'wallet generation (2), 128-bit seed', [[['del_dw_run'],15]])],
['addrgen2', (2,'address generation (2)', [[['mmdat'],2]])],
['txcreate2', (2,'transaction creation (2)', [[['addrs'],2]])],
@ -801,6 +813,7 @@ cmd_group['regtest'] = (
('regtest_bob_split2', "splitting Bob's funds"),
('regtest_generate', 'mining a block'),
('regtest_bob_bal5', "Bob's balance"),
('regtest_bob_bal5_getbalance',"Bob's balance"),
('regtest_bob_send_non_mmgen', 'sending funds to Alice (from non-MMGen addrs)'),
('regtest_generate', 'mining a block'),
('regtest_bob_alice_bal', "Bob and Alice's balances"),
@ -852,18 +865,38 @@ cmd_group['ethdev'] = (
('ethdev_addrgen', 'generating addresses'),
('ethdev_addrimport', 'importing addresses'),
('ethdev_addrimport_dev_addr', "importing Parity dev address 'Ox00a329c..'"),
('ethdev_txcreate', 'creating a transaction (spend from dev address)'),
('ethdev_txsign', 'signing the transaction'),
('ethdev_txsign_ni', 'signing the transaction (non-interactive)'),
('ethdev_txsend', 'sending the transaction'),
('ethdev_bal', 'the balance'),
('ethdev_txcreate2', 'creating a transaction (spend from MMGen address)'),
('ethdev_txcreate1', 'creating a transaction (spend from dev address)'),
('ethdev_txsign1', 'signing the transaction'),
('ethdev_txsign1_ni', 'signing the transaction (non-interactive)'),
('ethdev_txsend1', 'sending the transaction'),
('ethdev_txcreate2', 'creating a transaction (spend to address 11)'),
('ethdev_txsign2', 'signing the transaction'),
('ethdev_txsend2', 'sending the transaction'),
('ethdev_bal2', 'the balance'),
('ethdev_txcreate3', 'creating a transaction (spend to address 21)'),
('ethdev_txsign3', 'signing the transaction'),
('ethdev_txsend3', 'sending the transaction'),
('ethdev_txcreate4', 'creating a transaction (spend from MMGen address, low TX fee)'),
('ethdev_txbump', 'bumping the transaction fee'),
('ethdev_txsign4', 'signing the transaction'),
('ethdev_txsend4', 'sending the transaction'),
('ethdev_txcreate5', 'creating a transaction (fund burn address)'),
('ethdev_txsign5', 'signing the transaction'),
('ethdev_txsend5', 'sending the transaction'),
('ethdev_addrimport_burn_addr',"importing burn address"),
('ethdev_bal1', 'the balance'),
('ethdev_add_label', 'adding a UTF-8 label'),
('ethdev_chk_label', 'the label'),
('ethdev_remove_label', 'removing the label'),
('ethdev_stop', 'stopping parity'),
)
@ -993,7 +1026,7 @@ addrs_per_wallet = 8
meta_cmds = OrderedDict([
['gen', ('walletgen','addrgen')],
['pass', ('passchg','walletchk_newpass')],
['tx', ('addrimport','txcreate','txsign','txsend')],
['tx', ('txcreate','txsign','txsend')],
['export', [k for k in cmd_data if k[:7] == 'export_' and cmd_data[k][0] == 1]],
['gen_sp', [k for k in cmd_data if k[:8] == 'addrgen_' and cmd_data[k][0] == 1]],
['online', ('keyaddrgen','txsign_keyaddr')],
@ -1040,6 +1073,8 @@ def get_segwit_arg(cfg):
# Tell spawned programs they're running in the test suite
os.environ['MMGEN_TEST_SUITE'] = '1'
def imsg(s): sys.stderr.write(s+'\n') # never gets redefined
if opt.exact_output:
def msg(s): pass
vmsg = vmsg_r = msg_r = msg
@ -1145,7 +1180,6 @@ class MMGenExpect(MMGenPexpect):
passthru_args = ['testnet','rpc_host','rpc_port','regtest','coin']
if not opt.system:
os.environ['PYTHONPATH'] = repo_root
mmgen_cmd = os.path.relpath(os.path.join(repo_root,'cmds',mmgen_cmd))
elif g.platform == 'win':
mmgen_cmd = os.path.join('/mingw64','opt','bin',mmgen_cmd)
@ -1279,7 +1313,7 @@ def make_txcreate_cmdline(tx_data):
for idx,mod in enumerate(mods):
cfgs[k]['amts'][idx] = '{}.{}'.format(getrandnum(4) % mod, str(getrandnum(4))[:5])
cmd_args = ['-d',cfg['tmpdir']]
cmd_args = ['--outdir='+cfg['tmpdir']]
for num in tx_data:
s = tx_data[num]
cmd_args += [
@ -1667,46 +1701,45 @@ class MMGenTestSuite(object):
msg('Skipping non-Segwit address generation'); return True
self.addrgen(name,wf,pf=pf,check_ref=True,mmtype='compressed')
def addrimport(self,name,addrfile):
outfile = os.path.join(cfg['tmpdir'],u'addrfile_w_comments')
add_comments_to_addr_file(addrfile,outfile)
t = MMGenExpect(name,'mmgen-addrimport', [outfile])
t.expect_getend(r'Checksum for address data .*\[.*\]: ',regex=True)
t.expect("Type uppercase 'YES' to confirm: ",'\n')
vmsg('This is a simulation, so no addresses were actually imported into the tracking\nwallet')
t.ok(exit_val=1)
def txcreate_ui_common(self,t,name,
menu=[],inputs='1',
file_desc='Transaction',
input_sels_prompt='to spend',
bad_input_sels=False,non_mmgen_inputs=0,
fee_desc='transaction fee',fee='',fee_res=None,
add_comment='',view='t',save=True):
interactive_fee='',
fee_desc='transaction fee',fee_res=None,
add_comment='',view='t',save=True,no_ok=False):
for choice in menu + ['q']:
t.expect(r"'q'=quit view, .*?:.",choice,regex=True)
if bad_input_sels:
for r in ('x','3-1','9999'):
t.expect(input_sels_prompt+': ',r+'\n')
t.expect(input_sels_prompt+': ',inputs+'\n')
for i in range(non_mmgen_inputs):
t.expect('Accept? (y/N): ','y')
if fee:
t.expect(fee_desc+': ',fee+'\n')
if not name[:4] == 'txdo':
for i in range(non_mmgen_inputs):
t.expect('Accept? (y/N): ','y')
have_est_fee = t.expect([fee_desc+': ','OK? (Y/n): ']) == 1
if have_est_fee and not interactive_fee:
t.send('y')
else:
if have_est_fee: t.send('n')
t.send(interactive_fee+'\n')
if fee_res: t.expect(fee_res)
t.expect('OK? (Y/n): ','y') # fee OK?
t.expect('OK? (Y/n): ','y')
t.expect('(Y/n): ','\n') # chg amt OK?
t.do_comment(add_comment)
t.view_tx(view)
if not name[:4] == 'txdo':
t.expect('(y/N): ',('n','y')[save])
t.written_to_file(file_desc)
t.ok()
if not no_ok: t.ok()
def txsign_ui_common(self,t,name, view='t',add_comment='',
ni=False,save=True,do_passwd=False,
file_desc='Signed transaction'):
file_desc='Signed transaction',no_ok=False):
txdo = name[:4] == 'txdo'
if do_passwd:
@ -1719,7 +1752,7 @@ class MMGenTestSuite(object):
t.written_to_file(file_desc)
if not txdo: t.ok()
if not txdo and not no_ok: t.ok()
def do_confirm_send(self,t,quiet=False,confirm_send=True):
t.expect('Are you sure you want to broadcast this')
@ -1728,7 +1761,7 @@ class MMGenTestSuite(object):
def txsend_ui_common(self,t,name, view='n',add_comment='',
confirm_send=True,bogus_send=True,quiet=False,
file_desc='Sent transaction'):
file_desc='Sent transaction',no_ok=False):
txdo = name[:4] == 'txdo'
if not txdo:
@ -1739,13 +1772,16 @@ class MMGenTestSuite(object):
self.do_confirm_send(t,quiet=quiet,confirm_send=confirm_send)
if bogus_send:
txid = ''
t.expect('BOGUS transaction NOT sent')
else:
txid = t.expect_getend('Transaction sent: ')
assert len(txid) == 64,"'{}': Incorrect txid length!".format(txid)
t.written_to_file(file_desc)
if not txdo: t.ok()
if not txdo and not no_ok: t.ok()
return txid
def txcreate_common(self,name,
sources=['1'],
@ -1755,7 +1791,8 @@ class MMGenTestSuite(object):
add_args=[],
view='n',
addrs_per_wallet=addrs_per_wallet,
non_mmgen_input_compressed=True):
non_mmgen_input_compressed=True,
cmdline_inputs=False):
if opt.verbose or opt.exact_output:
sys.stderr.write(green('Generating fake tracking wallet info\n'))
@ -1765,6 +1802,13 @@ class MMGenTestSuite(object):
dfake = create_fake_unspent_data(ad,tx_data,non_mmgen_input,non_mmgen_input_compressed)
write_fake_data_to_file(repr(dfake))
cmd_args = make_txcreate_cmdline(tx_data)
if cmdline_inputs:
from mmgen.tx import TwLabel
cmd_args = ['--inputs={},{},{},{},{},{}'.format(
TwLabel(dfake[0]['account']).mmid,dfake[1]['address'],
TwLabel(dfake[2]['account']).mmid,dfake[3]['address'],
TwLabel(dfake[4]['account']).mmid,dfake[5]['address']
),'--outdir='+trash_dir] + cmd_args[1:]
end_silence()
if opt.verbose or opt.exact_output: sys.stderr.write('\n')
@ -1773,6 +1817,12 @@ class MMGenTestSuite(object):
'mmgen-'+('txcreate','txdo')[bool(txdo_args)],
([],['--rbf'])[g.proto.cap('rbf')] +
['-f',tx_fee,'-B'] + add_args + cmd_args + txdo_args)
if cmdline_inputs:
t.written_to_file('Transaction')
t.ok()
return
t.license()
if txdo_args and add_args: # txdo4
@ -1809,6 +1859,9 @@ class MMGenTestSuite(object):
def txcreate(self,name,addrfile):
self.txcreate_common(name,sources=['1'],add_args=['--vsize-adj=1.01'])
def txcreate_ni(self,name,addrfile):
self.txcreate_common(name,sources=['1'],cmdline_inputs=True,add_args=['--yes'])
def txbump(self,name,txfile,prepend_args=[],seed_args=[]):
if not g.proto.cap('rbf'):
msg('Skipping RBF'); return True
@ -2656,6 +2709,16 @@ class MMGenTestSuite(object):
def regtest_bob_bal5(self,name):
return self.regtest_user_bal(name,'bob',rtBals[3])
def regtest_bob_bal5_getbalance(self,name):
t_ext,t_mmgen = rtBals_gb[0],rtBals_gb[1]
assert Decimal(t_ext) + Decimal(t_mmgen) == Decimal(rtBals[3])
t = MMGenExpect(name,'mmgen-tool',['--bob','getbalance'])
t.expect(r'\n[0-9A-F]{8}: .* '+t_mmgen,regex=True)
t.expect(r'\nNon-MMGen: .* '+t_ext,regex=True)
t.expect(r'\nTOTAL: .* '+rtBals[3],regex=True)
t.read()
t.ok()
def regtest_bob_alice_bal(self,name):
t = MMGenExpect(name,'mmgen-regtest',['get_balances'])
t.expect('Switching')
@ -2686,7 +2749,7 @@ class MMGenTestSuite(object):
self.txcreate_ui_common(t,'txdo',
menu=['M'],inputs=outputs_list,
file_desc='Signed transaction',
fee=(tx_fee,'')[bool(fee)],
interactive_fee=(tx_fee,'')[bool(fee)],
add_comment=ref_tx_label_jp,
view='t',save=True)
@ -3046,89 +3109,144 @@ class MMGenTestSuite(object):
pid = read_from_tmpfile(cfg,cfg['parity_pidfile'])
ok()
def ethdev_addrgen(self,name):
def ethdev_addrgen(self,name,addrs='1-3,11-13,21-23'):
from mmgen.addr import MMGenAddrType
t = MMGenExpect(name,'mmgen-addrgen', eth_args + [dfl_words,'1-10'])
t = MMGenExpect(name,'mmgen-addrgen', eth_args + [dfl_words,addrs])
t.written_to_file('Addresses')
t.ok()
def ethdev_addrimport(self,name):
fn = get_file_with_ext('addrs',cfg['tmpdir'])
t = MMGenExpect(name,'mmgen-addrimport', eth_args[1:] + [fn])
if g.debug: t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
t.expect('Importing')
t.expect('10/10')
t.read()
t.ok()
def ethdev_addrimport_dev_addr(self,name):
t = MMGenExpect(name,'mmgen-addrimport', eth_args[1:] + ['--address='+eth_addr])
def ethdev_addrimport(self,name,ext='21-23].addrs',expect='9/9',add_args=[]):
fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True,delete=False)
t = MMGenExpect(name,'mmgen-addrimport', eth_args[1:] + add_args + [fn])
if g.debug: t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
t.expect('Importing')
t.expect(expect)
t.read()
t.ok()
def ethdev_addrimport_one_addr(self,name,addr=None,extra_args=[]):
t = MMGenExpect(name,'mmgen-addrimport', eth_args[1:] + extra_args + ['--address='+addr])
t.expect('OK')
t.ok()
def ethdev_txcreate(self,name,arg='98831F3A:E:1,123.456',acct='1',non_mmgen_inputs=1):
t = MMGenExpect(name,'mmgen-txcreate', eth_args + ['-B',arg])
def ethdev_addrimport_dev_addr(self,name):
self.ethdev_addrimport_one_addr(name,addr=eth_addr)
def ethdev_addrimport_burn_addr(self,name):
self.ethdev_addrimport_one_addr(name,addr=eth_burn_addr)
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_desc = 'gas price'):
t = MMGenExpect(name,'mmgen-txcreate', eth_args + ['-B'] + args)
t.expect(r"'q'=quit view, .*?:.",'p', regex=True)
t.written_to_file('Account balances listing')
self.txcreate_ui_common(t,name,
menu=['a','d','A','r','M','D','e','m','m'],
menu=menu,
input_sels_prompt='to spend from',
inputs=acct,file_desc='Ethereum transaction',
bad_input_sels=True,non_mmgen_inputs=non_mmgen_inputs,
fee_desc='gas price',fee='50G',fee_res='0.00105 ETH (50 gas price in Gwei)')
interactive_fee=interactive_fee,fee_res=fee_res,fee_desc=fee_desc)
def ethdev_txsign(self,name,ni=False,ext='.rawtx'):
def ethdev_txsign(self,name,ni=False,ext='.rawtx',add_args=[]):
key_fn = get_tmpfile_fn(cfg,cfg['parity_keyfile'])
write_to_tmpfile(cfg,cfg['parity_keyfile'],eth_key+'\n')
tx_fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
t = MMGenExpect(name,'mmgen-txsign',eth_args + ([],['--yes'])[ni] + ['-k',key_fn,tx_fn,dfl_words])
t = MMGenExpect(name,'mmgen-txsign',eth_args+add_args + ([],['--yes'])[ni] + ['-k',key_fn,tx_fn,dfl_words])
self.txsign_ui_common(t,name,ni=ni)
def ethdev_txsign_ni(self,name):
self.ethdev_txsign(name,ni=True)
def ethdev_txsend(self,name,ni=False,bogus_send=False,ext='.sigtx'):
def ethdev_txsend(self,name,ni=False,bogus_send=False,ext='.sigtx',add_args=[]):
tx_fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = ''
t = MMGenExpect(name,'mmgen-txsend', eth_args + [tx_fn])
t = MMGenExpect(name,'mmgen-txsend', eth_args+add_args + [tx_fn])
if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = '1'
self.txsend_ui_common(t,name,quiet=True,bogus_send=bogus_send)
def ethdev_bal(self,name):
t = MMGenExpect(name,'mmgen-tool', eth_args + ['twview'])
t.expect(r'98831F3A:E:1\s+123\.456\s+',regex=True)
t.ok()
def ethdev_txcreate1(self,name):
menu = ['a','d','A','r','M','D','e','m','m']
args = ['98831F3A:E:1,123.456']
return self.ethdev_txcreate(name,args=args,menu=menu,acct='1',non_mmgen_inputs=1)
def ethdev_txsign1(self,name): self.ethdev_txsign(name)
def ethdev_txsign1_ni(self,name): self.ethdev_txsign(name,ni=True)
def ethdev_txsend1(self,name): self.ethdev_txsend(name)
def ethdev_txcreate2(self,name):
return self.ethdev_txcreate(name,arg='98831F3A:E:2,23.45495',acct='11',non_mmgen_inputs=0)
args = ['98831F3A:E:11,1.234']
return self.ethdev_txcreate(name,args=args,acct='10',non_mmgen_inputs=1)
def ethdev_txsign2(self,name): self.ethdev_txsign(name,ni=True,ext='1.234,50000].rawtx')
def ethdev_txsend2(self,name): self.ethdev_txsend(name,ext='1.234,50000].sigtx')
def ethdev_txsign2(self,name):
self.ethdev_txsign(name,ext='.45495].rawtx',ni=True)
def ethdev_txcreate3(self,name):
args = ['98831F3A:E:21,2.345']
return self.ethdev_txcreate(name,args=args,acct='10',non_mmgen_inputs=1)
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_txsend2(self,name):
self.ethdev_txsend(name,ni=True,ext='.45495].sigtx')
def ethdev_txcreate4(self,name):
args = ['98831F3A:E:2,23.45495']
interactive_fee='40G'
fee_res='0.00084 ETH (40 gas price in Gwei)'
return self.ethdev_txcreate(name,args=args,acct='1',non_mmgen_inputs=0,
interactive_fee=interactive_fee,fee_res=fee_res)
def ethdev_bal2(self,name):
t = MMGenExpect(name,'mmgen-tool', eth_args + ['twview'])
t.expect(r'98831F3A:E:1\s+100\s+',regex=True)
t.expect(r'98831F3A:E:2\s+23\.45495\s+',regex=True)
def ethdev_txbump(self,name,ext=',40000].rawtx',fee='50G',add_args=[]):
tx_fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
t = MMGenExpect(name,'mmgen-txbump', eth_args + add_args + ['--yes',tx_fn])
t.expect('or gas price: ',fee+'\n')
t.read()
t.ok()
def ethdev_add_label(self,name,addr='98831F3A:E:10',lbl=utf8_label):
def ethdev_txsign4(self,name): self.ethdev_txsign(name,ni=True,ext='.45495,50000].rawtx')
def ethdev_txsend4(self,name): self.ethdev_txsend(name,ext='.45495,50000].sigtx')
def ethdev_txcreate5(self,name):
args = [eth_burn_addr + ','+eth_amt1]
return self.ethdev_txcreate(name,args=args,acct='10',non_mmgen_inputs=1)
def ethdev_txsign5(self,name): self.ethdev_txsign(name,ni=True,ext=eth_amt1+',50000].rawtx')
def ethdev_txsend5(self,name): self.ethdev_txsend(name,ext=eth_amt1+',50000].sigtx')
def ethdev_bal(self,name,expect_str=''):
t = MMGenExpect(name,'mmgen-tool', eth_args + ['twview'])
t.expect(expect_str,regex=True)
t.read()
t.ok()
def ethdev_bal_getbalance(self,name,t_non_mmgen='',t_mmgen='',extra_args=[]):
t = MMGenExpect(name,'mmgen-tool', eth_args + extra_args + ['getbalance'])
t.expect(r'\n[0-9A-F]{8}: .* '+t_mmgen,regex=True)
t.expect(r'\nNon-MMGen: .* '+t_non_mmgen,regex=True)
total = t.expect_getend(r'\nTOTAL:\s+',regex=True).split()[0]
t.read()
assert Decimal(t_non_mmgen) + Decimal(t_mmgen) == Decimal(total)
t.ok()
def ethdev_bal1(self,name,expect_str=''):
self.ethdev_bal(name,expect_str=r'98831F3A:E:2\s+23\.45495\s+')
def ethdev_add_label(self,name,addr='98831F3A:E:3',lbl=utf8_label):
t = MMGenExpect(name,'mmgen-tool', eth_args + ['add_label',addr,lbl])
t.expect('Added label.*in tracking wallet',regex=True)
t.ok()
def ethdev_chk_label(self,name,addr='98831F3A:E:10',label_pat=utf8_label_pat):
def ethdev_chk_label(self,name,addr='98831F3A:E:3',label_pat=utf8_label_pat):
t = MMGenExpect(name,'mmgen-tool', eth_args + ['listaddresses','all_labels=1'])
t.expect(r'{}\s+\S{{30}}\S+\s+{}\s+'.format(addr,(label_pat or label).encode('utf8')),regex=True)
t.ok()
def ethdev_remove_label(self,name,addr='98831F3A:E:10'):
def ethdev_remove_label(self,name,addr='98831F3A:E:3'):
t = MMGenExpect(name,'mmgen-tool', eth_args + ['remove_label',addr])
t.expect('Removed label.*in tracking wallet',regex=True)
t.ok()
def init_ethdev_common(self):
g.testnet = True
init_coin('eth')
g.proto.rpc_port = 8549
rpc_init()
def ethdev_stop(self,name):
MMGenExpect(name,'',msg_only=True)
pid = read_from_tmpfile(cfg,cfg['parity_pidfile'])
@ -3265,9 +3383,12 @@ try:
except KeyboardInterrupt:
die(1,'\nExiting at user request')
except opt.traceback and Exception:
with open('my.err') as f:
t = f.readlines()
if t: msg_r('\n'+yellow(''.join(t[:-1]))+red(t[-1]))
try:
os.stat('my.err')
with open('my.err') as f:
t = f.readlines()
if t: msg_r('\n'+yellow(''.join(t[:-1]))+red(t[-1]))
except: pass
die(1,blue('Test script exited with error'))
except:
sys.stderr = stderr_save