From 54f86e08315eb3bfd287c7def6d05c9536d8a12e Mon Sep 17 00:00:00 2001 From: MMGen Date: Wed, 3 Jul 2019 18:16:41 +0000 Subject: [PATCH] mmgen-txsend: improve output of --status option --- mmgen/tw.py | 2 - mmgen/tx.py | 86 ++++++++++++++++++++---------------- test/test_py_d/ts_regtest.py | 59 ++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 43 deletions(-) diff --git a/mmgen/tw.py b/mmgen/tw.py index 44461277..278cb823 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -121,8 +121,6 @@ watch-only wallet using '{}-addrimport' and then re-run this program. # 4. include_unsafe (boolean, optional, default=true) Include outputs that are not safe to spend # 5. query_options (json object, optional) JSON with query options - # for now, self.addrs is just an empty list for Bitcoin and friends - add_args = (9999999,self.addrs) if self.addrs else () return g.rpch.listunspent(self.minconf) def get_unspent_data(self): diff --git a/mmgen/tx.py b/mmgen/tx.py index 76ee5eaa..0c631f13 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -876,58 +876,66 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam def has_segwit_outputs(self): return any(o.mmid and o.mmid.mmtype in ('S','B') for o in self.outputs) - def is_in_mempool(self): - return 'size' in g.rpch.getmempoolentry(self.coin_txid,on_fail='silent') - - def is_in_wallet(self): - ret = g.rpch.gettransaction(self.coin_txid,on_fail='silent') - if 'confirmations' in ret and ret['confirmations'] > 0: - return ret['confirmations'] - else: - return False - - def is_replaced(self): - if self.is_in_mempool(): return False - ret = g.rpch.gettransaction(self.coin_txid,on_fail='silent') - if not 'bip125-replaceable' in ret or not 'confirmations' in ret or ret['confirmations'] > 0: - return False - return -ret['confirmations'] + 1,ret # 1: replacement in mempool, 2: replacement confirmed - - def is_in_utxos(self): - return 'txid' in g.rpch.getrawtransaction(self.coin_txid,True,on_fail='silent') - def get_status(self,status=False): - if self.is_in_mempool(): + + class r(object): pass + + def is_in_wallet(): + ret = g.rpch.gettransaction(self.coin_txid,on_fail='silent') + if 'confirmations' in ret and ret['confirmations'] > 0: + r.confs = ret['confirmations'] + return True + else: + return False + + def is_in_utxos(): + return 'txid' in g.rpch.getrawtransaction(self.coin_txid,True,on_fail='silent') + + def is_in_mempool(): + return 'size' in g.rpch.getmempoolentry(self.coin_txid,on_fail='silent') + + def is_replaced(): + if is_in_mempool(): return False + ret = g.rpch.gettransaction(self.coin_txid,on_fail='silent') + + if not 'bip125-replaceable' in ret or not 'confirmations' in ret or ret['confirmations'] > 0: + return False + + r.replacing_confs = -ret['confirmations'] + r.replacing_txs = ret['walletconflicts'] + return True + + if is_in_mempool(): if status: d = g.rpch.gettransaction(self.coin_txid,on_fail='silent') brs = 'bip125-replaceable' - r = '{}replaceable'.format(('NOT ','')[brs in d and d[brs]=='yes']) + rep = '{}replaceable'.format(('NOT ','')[brs in d and d[brs]=='yes']) t = d['timereceived'] m = 'Sent {} ({} h/m/s ago)' b = m.format(time.strftime('%c',time.gmtime(t)),secs_to_dhms(int(time.time()-t))) if opt.quiet: msg('Transaction is in mempool') else: - msg('TX status: in mempool, {}\n{}'.format(r,b)) + msg('TX status: in mempool, {}\n{}'.format(rep,b)) else: msg('Warning: transaction is in mempool!') - elif self.is_in_wallet(): - confs = self.is_in_wallet() - die(0,'Transaction has {} confirmation{}'.format(confs,suf(confs,'s'))) - elif self.is_in_utxos(): + elif is_in_wallet(): + die(0,'Transaction has {} confirmation{}'.format(r.confs,suf(r.confs))) + elif is_in_utxos(): die(2,red('ERROR: transaction is in the blockchain (but not in the tracking wallet)!')) - else: - ret = self.is_replaced() # ret[0]==1: replacement in mempool, ret[0]==2: replacement confirmed - if ret and ret[0]: - m1 = 'Transaction has been replaced' - m2 = ('',', and the replacement TX is confirmed')[ret[0]==2] - msg('{}{}!'.format(m1,m2)) - if not opt.quiet: - msg('Replacing transactions:') - rt = ret[1]['walletconflicts'] - for t,s in [(tx,'size' in g.rpch.getmempoolentry(tx,on_fail='silent')) for tx in rt]: - msg(' {}{}'.format(t,('',' in mempool')[s])) - die(0,'') + elif is_replaced(): + m1 = 'Transaction has been replaced' + m2 = 'Replacement transaction is in mempool' + rc = r.replacing_confs + if rc: + m2 = 'Replacement transaction has {} confirmation{}'.format(rc,suf(rc)) + msg('{}\n{}'.format(m1,m2)) + if not opt.quiet: + msg('Replacing transactions:') + d = ((t,g.rpch.getmempoolentry(t,on_fail='silent')) for t in r.replacing_txs) + for txid,mp_entry in d: + msg(' {}{}'.format(txid,' in mempool' if ('size' in mp_entry) else '')) + die(0,'') def confirm_send(self): m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)] diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index ee0f6352..c8197e21 100755 --- a/test/test_py_d/ts_regtest.py +++ b/test/test_py_d/ts_regtest.py @@ -22,6 +22,7 @@ ts_regtest.py: Regtest tests for the test.py test suite import os,subprocess from decimal import Decimal +from ast import literal_eval from mmgen.globalvars import g from mmgen.opts import opt from mmgen.util import die,gmsg,write_data_to_file @@ -158,9 +159,16 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): ('bob_bal2f', "Bob's balance (showempty=1 sort=age,reverse)"), ('bob_rbf_send', 'sending funds to Alice (RBF)'), ('get_mempool1', 'mempool (before RBF bump)'), + ('bob_rbf_status1', 'getting status of transaction'), ('bob_rbf_bump', 'bumping RBF transaction'), ('get_mempool2', 'mempool (after RBF bump)'), + ('bob_rbf_status2', 'getting status of transaction after replacement'), + ('bob_rbf_status3', 'getting status of replacement transaction (mempool)'), ('generate', 'mining a block'), + ('bob_rbf_status4', 'getting status of transaction after confirmed (1) replacement'), + ('bob_rbf_status5', 'getting status of replacement transaction (confirmed)'), + ('generate', 'mining a block'), + ('bob_rbf_status6', 'getting status of transaction after confirmed (2) replacement'), ('bob_bal3', "Bob's balance"), ('bob_pre_import', 'sending to non-imported address'), ('generate', 'mining a block'), @@ -483,6 +491,14 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): cmp_or_die(rtBals[6],ret) return t + def user_txsend_status(self,user,tx_file,exp1='',exp2='',extra_args=[],bogus_send=False): + os.environ['MMGEN_BOGUS_SEND'] = ('','1')[bool(bogus_send)] + t = self.spawn('mmgen-txsend',['-d',self.tmpdir,'--'+user,'--status'] + extra_args + [tx_file]) + os.environ['MMGEN_BOGUS_SEND'] = '1' + if exp1: t.expect(exp1) + if exp2: t.expect(exp2) + return t + def user_txdo( self, user, fee, outputs_cl, outputs_list, extra_args = [], wf = None, @@ -590,8 +606,8 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): disable_debug() ret = self.spawn('mmgen-regtest',['show_mempool']).read() restore_debug() - from ast import literal_eval - return literal_eval(ret.split('\n')[0]) # allow for extra output by handler at end + self.mempool = literal_eval(ret.split('\n')[0]) # allow for extra output by handler at end + return self.mempool def get_mempool1(self): mp = self._get_mempool() @@ -600,6 +616,17 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): self.write_to_tmpfile('rbf_txid',mp[0]+'\n') return 'ok' + def bob_rbf_status(self,fee,exp1,exp2='',skip_bch=False): + if skip_bch and not g.proto.cap('rbf'): + msg('skipping test {} for BCH'.format(self.test_name)) + return 'skip' + ext = ',{}]{x}.testnet.sigtx'.format(fee[:-1],x='-α' if g.debug_utf8 else '') + txfile = self.get_file_with_ext(ext,delete=False,no_dot=True) + return self.user_txsend_status('bob',txfile,exp1,exp2) + + def bob_rbf_status1(self): + return self.bob_rbf_status(rtFee[1],'in mempool, replaceable',skip_bch=True) + def get_mempool2(self): if not g.proto.cap('rbf'): msg('Skipping post-RBF mempool check'); return 'skip' @@ -611,6 +638,34 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): rdie(2,'TX in mempool has not changed! RBF bump failed') return 'ok' + def bob_rbf_status2(self): + if not g.proto.cap('rbf'): return 'skip' + return self.bob_rbf_status(rtFee[1], + 'Transaction has been replaced','{} in mempool'.format(self.mempool[0]), + skip_bch=True) + + def bob_rbf_status3(self): + if not g.proto.cap('rbf'): return 'skip' + return self.bob_rbf_status(rtFee[2],'status: in mempool, replaceable',skip_bch=True) + + def bob_rbf_status4(self): + if not g.proto.cap('rbf'): return 'skip' + return self.bob_rbf_status(rtFee[1], + 'Replacement transaction has 1 confirmation', + 'Replacing transactions:\n {}'.format(self.mempool[0]), + skip_bch=True) + + def bob_rbf_status5(self): + if not g.proto.cap('rbf'): return 'skip' + return self.bob_rbf_status(rtFee[2],'Transaction has 1 confirmation',skip_bch=True) + + def bob_rbf_status6(self): + if not g.proto.cap('rbf'): return 'skip' + return self.bob_rbf_status(rtFee[1], + 'Replacement transaction has 2 confirmations', + 'Replacing transactions:\n {}'.format(self.mempool[0]), + skip_bch=True) + @staticmethod def _gen_pairs(n): disable_debug()