main_addrbal.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  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
  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(addr.view_pref)}')
  35. if unspents:
  36. heights = {u['height'] for u in unspents}
  37. Msg('{}Balance: {}'.format(
  38. indent,
  39. sum(proto.coin_amt(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(6, color=True, prec=8)))
  56. else:
  57. Msg(f'{indent}No balance')
  58. def do_output_tabular(proto, addr_data, blk_hdrs):
  59. col1w = len(str(len(addr_data))) + 1
  60. max_addrw = max(len(addr) for addr in addr_data)
  61. fb_heights = [str(unspents[0]['height']) if unspents else '' for unspents in addr_data.values()]
  62. lb_heights = [str(unspents[-1]['height']) if unspents else '' for unspents in addr_data.values()]
  63. fb_w = max(len(h) for h in fb_heights)
  64. lb_w = max(len(h) for h in lb_heights)
  65. fs = (
  66. ' {n:>%s} {a} {u} {b:>%s} {t:19} {B:>%s} {T:19} {A}' % (col1w, max(5, fb_w), max(4, lb_w))
  67. if cfg.first_block else
  68. ' {n:>%s} {a} {u} {B:>%s} {T:19} {A}' % (col1w, max(4, lb_w)))
  69. Msg('\n' + fs.format(
  70. n = '',
  71. a = 'Address'.ljust(max_addrw),
  72. u = 'UTXOs',
  73. b = 'First',
  74. t = 'Block',
  75. B = 'Last',
  76. T = 'Block',
  77. A = ' Amount'))
  78. for n, (addr, unspents) in enumerate(addr_data.items(), 1):
  79. if unspents:
  80. Msg(fs.format(
  81. n = str(n) + ')',
  82. a = addr.fmt(addr.view_pref, max_addrw, color=True),
  83. u = red(str(len(unspents)).rjust(5)),
  84. b = unspents[0]['height'],
  85. t = make_timestr(blk_hdrs[unspents[0]['height']]['time']),
  86. B = unspents[-1]['height'],
  87. T = make_timestr(blk_hdrs[unspents[-1]['height']]['time']),
  88. A = sum(proto.coin_amt(u['amount']) for u in unspents).fmt(7, color=True, prec=8)))
  89. else:
  90. Msg(fs.format(
  91. n = str(n) + ')',
  92. a = addr.fmt(addr.view_pref, max_addrw, color=True),
  93. u = ' -',
  94. b = '-',
  95. t = '',
  96. B = '-',
  97. T = '',
  98. A = ' -'))
  99. async def main(req_addrs):
  100. proto = cfg._proto
  101. from mmgen.addr import CoinAddr
  102. addrs = [CoinAddr(proto, addr) for addr in req_addrs]
  103. from mmgen.rpc import rpc_init
  104. rpc = await rpc_init(cfg, ignore_wallet=True)
  105. height = await rpc.call('getblockcount')
  106. Msg(f'{proto.coin} {proto.network.upper()} [height {height}]')
  107. from mmgen.proto.btc.misc import scantxoutset
  108. res = await scantxoutset(cfg, rpc, [f'addr({addr})' for addr in addrs])
  109. if not res['success']:
  110. die(1, 'UTXO scanning failed or was interrupted')
  111. elif not res['unspents']:
  112. msg('Address has no balance' if len(addrs) == 1 else
  113. 'Addresses have no balances')
  114. else:
  115. addr_data = {k:[] for k in addrs}
  116. if 'desc' in res['unspents'][0]:
  117. import re
  118. for unspent in sorted(res['unspents'], key=lambda x: x['height']):
  119. addr = re.match('addr\((.*?)\)', unspent['desc'])[1]
  120. addr_data[addr].append(unspent)
  121. else:
  122. from mmgen.proto.btc.tx.base import decodeScriptPubKey
  123. for unspent in sorted(res['unspents'], key=lambda x: x['height']):
  124. ds = decodeScriptPubKey(proto, unspent['scriptPubKey'])
  125. addr_data[ds.addr].append(unspent)
  126. good_addrs = len([v for v in addr_data.values() if v])
  127. Msg('Total: {} in {} address{}'.format(
  128. proto.coin_amt(res['total_amount']).hl2(unit=True, fs='{:,}'),
  129. red(str(good_addrs)),
  130. suf(good_addrs, 'es')))
  131. blk_heights = {i['height'] for i in res['unspents']}
  132. blk_hashes = await rpc.batch_call('getblockhash', [(h,) for h in blk_heights])
  133. blk_hdrs = await rpc.batch_call('getblockheader', [(H,) for H in blk_hashes])
  134. (do_output_tabular if cfg.tabular else do_output)(
  135. proto, addr_data, dict(zip(blk_heights, blk_hdrs)))
  136. cfg = Config(opts_data=opts_data, init_opts={'rpc_backend': 'aiohttp'})
  137. if len(cfg._args) < 1:
  138. die(1, 'This command requires at least one coin address argument')
  139. try:
  140. async_run(cfg, main, args=[cfg._args])
  141. except KeyboardInterrupt:
  142. sys.stderr.write('\n')