From fad573eccd7fc986114671316a9340c0387ac730 Mon Sep 17 00:00:00 2001 From: MMGen Date: Mon, 23 Jul 2018 21:17:05 +0000 Subject: [PATCH] tx.py,tw.py: cleanups, support tx inputs from cmdline --- mmgen/altcoins/eth/tw.py | 22 +- mmgen/altcoins/eth/tx.py | 234 ++++++++++----- mmgen/main_txbump.py | 8 +- mmgen/main_txcreate.py | 44 +-- mmgen/main_txdo.py | 8 + mmgen/obj.py | 14 +- mmgen/opts.py | 11 +- mmgen/protocol.py | 2 + mmgen/tw.py | 45 +-- mmgen/tx.py | 125 +++++--- scripts/test-release.sh | 1 + setup.py | 2 +- test/mmgen_pexpect.py | 1 + ...H[0.123].rawtx => 4ED554-ETH[0.123].rawtx} | 6 +- ....rawtx => 7EE763-ETH[0.123].testnet.rawtx} | 6 +- test/test.py | 283 +++++++++++++----- 16 files changed, 551 insertions(+), 261 deletions(-) rename test/ref/ethereum/{BC79AB-ETH[0.123].rawtx => 4ED554-ETH[0.123].rawtx} (71%) rename test/ref/ethereum/{F04889-ETH[0.123].testnet.rawtx => 7EE763-ETH[0.123].testnet.rawtx} (68%) diff --git a/mmgen/altcoins/eth/tw.py b/mmgen/altcoins/eth/tw.py index c2d95b35..983f232b 100755 --- a/mmgen/altcoins/eth/tw.py +++ b/mmgen/altcoins/eth/tw.py @@ -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): diff --git a/mmgen/altcoins/eth/tx.py b/mmgen/altcoins/eth/tx.py index 11ca2745..67c7d5f5 100755 --- a/mmgen/altcoins/eth/tx.py +++ b/mmgen/altcoins/eth/tx.py @@ -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 diff --git a/mmgen/main_txbump.py b/mmgen/main_txbump.py index 1a028887..f494dca4 100755 --- a/mmgen/main_txbump.py +++ b/mmgen/main_txbump.py @@ -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 diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index 9626257f..3054bf7b 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -28,26 +28,30 @@ opts_data = lambda: { 'usage': '[opts] ... [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, diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index 3c0817de..2d81fed9 100755 --- a/mmgen/main_txdo.py +++ b/mmgen/main_txdo.py @@ -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) diff --git a/mmgen/obj.py b/mmgen/obj.py index f739edd5..517b432e 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -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 diff --git a/mmgen/opts.py b/mmgen/opts.py index 872098b1..8bc20959 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -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." diff --git a/mmgen/protocol.py b/mmgen/protocol.py index 6055b860..8570e020 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -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 diff --git a/mmgen/tw.py b/mmgen/tw.py index 22a22a3c..082b3fb0 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -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): diff --git a/mmgen/tx.py b/mmgen/tx.py index 83f164cf..6ab0b031 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -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 diff --git a/scripts/test-release.sh b/scripts/test-release.sh index 415fb596..5d8ee921 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -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' diff --git a/setup.py b/setup.py index 35828891..054a1f7e 100755 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/test/mmgen_pexpect.py b/test/mmgen_pexpect.py index c9868070..e06ab4d6 100755 --- a/test/mmgen_pexpect.py +++ b/test/mmgen_pexpect.py @@ -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: diff --git a/test/ref/ethereum/BC79AB-ETH[0.123].rawtx b/test/ref/ethereum/4ED554-ETH[0.123].rawtx similarity index 71% rename from test/ref/ethereum/BC79AB-ETH[0.123].rawtx rename to test/ref/ethereum/4ED554-ETH[0.123].rawtx index 5c4fc9aa..d558acbb 100644 --- a/test/ref/ethereum/BC79AB-ETH[0.123].rawtx +++ b/test/ref/ethereum/4ED554-ETH[0.123].rawtx @@ -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 diff --git a/test/ref/ethereum/F04889-ETH[0.123].testnet.rawtx b/test/ref/ethereum/7EE763-ETH[0.123].testnet.rawtx similarity index 68% rename from test/ref/ethereum/F04889-ETH[0.123].testnet.rawtx rename to test/ref/ethereum/7EE763-ETH[0.123].testnet.rawtx index 2c900a41..8d316687 100644 --- a/test/ref/ethereum/F04889-ETH[0.123].testnet.rawtx +++ b/test/ref/ethereum/7EE763-ETH[0.123].testnet.rawtx @@ -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 diff --git a/test/test.py b/test/test.py index 15db788b..ef8ee261 100755 --- a/test/test.py +++ b/test/test.py @@ -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