main_addrbal.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet https://github.com/mmgen/mmgen-node-tools
  9. # https://gitlab.com/mmgen/mmgen-wallet https://gitlab.com/mmgen/mmgen-node-tools
  10. """
  11. mmnode-addrbal: Get balances for arbitrary addresses in the blockchain
  12. """
  13. import sys
  14. from mmgen.obj import CoinTxID,Int
  15. from mmgen.cfg import Config
  16. from mmgen.util import msg,Msg,die,suf,make_timestr,async_run
  17. from mmgen.color import red
  18. opts_data = {
  19. 'text': {
  20. 'desc': 'Get balances for arbitrary addresses in the blockchain',
  21. 'usage': '[opts] address [address..]',
  22. 'options': """
  23. -h, --help Print this help message
  24. --, --longhelp Print help message for long options (common options)
  25. -f, --first-block With tabular output, additionally display first block info
  26. -t, --tabular Produce compact tabular output
  27. """
  28. }
  29. }
  30. def do_output(proto,addr_data,blk_hdrs):
  31. col1w = len(str(len(addr_data)))
  32. indent = ' ' * (col1w + 2)
  33. for n,(addr,unspents) in enumerate(addr_data.items(),1):
  34. Msg(f'\n{n:{col1w}}) Address: {addr.hl()}')
  35. if unspents:
  36. heights = { u['height'] for u in unspents }
  37. Msg('{}Balance: {}'.format(
  38. indent,
  39. proto.coin_amt(sum(u['amount'] for u in unspents)).hl2(unit=True,fs='{:,}') )),
  40. Msg('{}{} unspent output{} in {} block{}'.format(
  41. indent,
  42. red(str(len(unspents))),
  43. suf(unspents),
  44. red(str(len(heights))),
  45. suf(heights) ))
  46. blk_w = len(str(unspents[-1]['height']))
  47. fs = '%s{:%s} {:19} {:64} {:4} {}' % (indent,max(5,blk_w))
  48. Msg(fs.format('Block','Date','TxID','Vout',' Amount'))
  49. for u in unspents:
  50. Msg(fs.format(
  51. u['height'],
  52. make_timestr( blk_hdrs[u['height']]['time'] ),
  53. CoinTxID(u['txid']).hl(),
  54. red(str(u['vout']).rjust(4)),
  55. proto.coin_amt(u['amount']).fmt(color=True,iwidth=6,prec=8)
  56. ))
  57. else:
  58. Msg(f'{indent}No balance')
  59. def do_output_tabular(proto,addr_data,blk_hdrs):
  60. col1w = len(str(len(addr_data))) + 1
  61. max_addrw = max(len(addr) for addr in addr_data)
  62. fb_heights = [str(unspents[0]['height']) if unspents else '' for unspents in addr_data.values()]
  63. lb_heights = [str(unspents[-1]['height']) if unspents else '' for unspents in addr_data.values()]
  64. fb_w = max(len(h) for h in fb_heights)
  65. lb_w = max(len(h) for h in lb_heights)
  66. fs = (
  67. ' {n:>%s} {a} {u} {b:>%s} {t:19} {B:>%s} {T:19} {A}' % (col1w,max(5,fb_w),max(4,lb_w))
  68. if cfg.first_block else
  69. ' {n:>%s} {a} {u} {B:>%s} {T:19} {A}' % (col1w,max(4,lb_w)) )
  70. Msg('\n' + fs.format(
  71. n = '',
  72. a = 'Address'.ljust(max_addrw),
  73. u = 'UTXOs',
  74. b = 'First',
  75. t = 'Block',
  76. B = 'Last',
  77. T = 'Block',
  78. A = ' Amount' ))
  79. for n,(addr,unspents) in enumerate(addr_data.items(),1):
  80. if unspents:
  81. Msg(fs.format(
  82. n = str(n) + ')',
  83. a = addr.fmt(width=max_addrw,color=True),
  84. u = red(str(len(unspents)).rjust(5)),
  85. b = unspents[0]['height'],
  86. t = make_timestr( blk_hdrs[unspents[0]['height']]['time'] ),
  87. B = unspents[-1]['height'],
  88. T = make_timestr( blk_hdrs[unspents[-1]['height']]['time'] ),
  89. A = proto.coin_amt(sum(u['amount'] for u in unspents)).fmt(color=True,iwidth=7,prec=8)
  90. ))
  91. else:
  92. Msg(fs.format(
  93. n = str(n) + ')',
  94. a = addr.fmt(width=max_addrw,color=True),
  95. u = ' -',
  96. b = '-',
  97. t = '',
  98. B = '-',
  99. T = '',
  100. A = ' -' ))
  101. async def main(req_addrs):
  102. proto = cfg._proto
  103. from mmgen.addr import CoinAddr
  104. addrs = [CoinAddr(proto,addr) for addr in req_addrs]
  105. from mmgen.rpc import rpc_init
  106. rpc = await rpc_init(cfg,ignore_wallet=True)
  107. height = await rpc.call('getblockcount')
  108. Msg(f'{proto.coin} {proto.network.upper()} [height {height}]')
  109. from mmgen.proto.btc.misc import scantxoutset
  110. res = await scantxoutset( cfg, rpc, [f'addr({addr})' for addr in addrs] )
  111. if not res['success']:
  112. die(1,'UTXO scanning failed or was interrupted')
  113. elif not res['unspents']:
  114. msg('Address has no balance' if len(addrs) == 1 else
  115. 'Addresses have no balances' )
  116. else:
  117. addr_data = {k:[] for k in addrs}
  118. if 'desc' in res['unspents'][0]:
  119. import re
  120. for unspent in sorted(res['unspents'],key=lambda x: x['height']):
  121. addr = re.match('addr\((.*?)\)',unspent['desc'])[1]
  122. addr_data[addr].append(unspent)
  123. else:
  124. from mmgen.proto.btc.tx.base import scriptPubKey2addr
  125. for unspent in sorted(res['unspents'],key=lambda x: x['height']):
  126. addr = scriptPubKey2addr( proto, unspent['scriptPubKey'] )[0]
  127. addr_data[addr].append(unspent)
  128. good_addrs = len([v for v in addr_data.values() if v])
  129. Msg('Total: {} in {} address{}'.format(
  130. proto.coin_amt(res['total_amount']).hl2(unit=True,fs='{:,}'),
  131. red(str(good_addrs)),
  132. suf(good_addrs,'es')
  133. ))
  134. blk_heights = {i['height'] for i in res['unspents']}
  135. blk_hashes = await rpc.batch_call('getblockhash', [(h,) for h in blk_heights])
  136. blk_hdrs = await rpc.batch_call('getblockheader', [(H,) for H in blk_hashes])
  137. (do_output_tabular if cfg.tabular else do_output)( proto, addr_data, dict(zip(blk_heights,blk_hdrs)) )
  138. cfg = Config( opts_data=opts_data, init_opts={'rpc_backend':'aiohttp'} )
  139. if len(cfg._args) < 1:
  140. die(1,'This command requires at least one coin address argument')
  141. try:
  142. async_run(main(cfg._args))
  143. except KeyboardInterrupt:
  144. sys.stderr.write('\n')