Преглед изворни кода

new command: `mmnode-addrbal`

Get balances for arbitrary addresses in the blockchain

Testing:

    $ test/test.py -e regtest

Example:

    # Top 10 Bitcoin addresses by balance (see https://blockchair.com/bitcoin/addresses)
    $ top10='
        34xp4vRoCGJym3xR7yCVPFHoCNxv4Twseo
        bc1qgdjqv0av3q56jvd82tkdjpy7gdp9ut8tlqmgrpmv24sq90ecnvqqjwvw97
        1LQoWist8KkaUXSPKZHNvEyfrEkPHzSsCd
        3LYJfcfHPXYJreMsASk2jkn69LWEYKzexb
        3M219KR5vEneNb47ewrPfWyb5jQ2DjxRP6
        bc1qazcm763858nkj2dj986etajv6wquslv8uxwczt
        37XuVSEpWW4trkfmvWzegTHQt7BdktSKUs
        1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF
        bc1qa5wkgaew2dkv56kfvj49j0av5nml45x9ek9hz6
        3Kzh9qAqVWQhEsfQz7zEQL1EuSx5tyNLNS'

    # Compact summary:
    $ mmnode-addrbal --tabular $top10

    # Full output:
    $ mmnode-addrbal $top10
The MMGen Project пре 1 година
родитељ
комит
6c370dec4b
5 измењених фајлова са 249 додато и 1 уклоњено
  1. 17 0
      cmds/mmnode-addrbal
  2. 1 1
      mmgen_node_tools/data/version
  3. 170 0
      mmgen_node_tools/main_addrbal.py
  4. 1 0
      setup.cfg
  5. 60 0
      test/test_py_d/ts_regtest.py

+ 17 - 0
cmds/mmnode-addrbal

@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
+#   https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
+
+"""
+mmnode-addrbal: Get balances for arbitrary addresses in the blockchain
+"""
+
+from mmgen.main import launch
+
+launch('addrbal',package='mmgen_node_tools')

+ 1 - 1
mmgen_node_tools/data/version

@@ -1 +1 @@
-3.1.dev2
+3.1.dev3

+ 170 - 0
mmgen_node_tools/main_addrbal.py

@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen https://github.com/mmgen/mmgen-node-tools
+#   https://gitlab.com/mmgen/mmgen https://gitlab.com/mmgen/mmgen-node-tools
+
+"""
+mmnode-addrbal: Get balances for arbitrary addresses in the blockchain
+"""
+
+from mmgen.common import *
+
+opts_data = {
+	'text': {
+		'desc': 'Get balances for arbitrary addresses in the blockchain',
+		'usage': '[opts] address [address..]',
+		'options': """
+-h, --help        Print this help message
+--, --longhelp    Print help message for long options (common options)
+-f, --first-block With tabular output, additionally display first block info
+-t, --tabular     Produce compact tabular output
+"""
+	}
+}
+
+def do_output(proto,addr_data,blk_hdrs):
+
+	col1w = len(str(len(addr_data)))
+	indent = ' ' * (col1w + 2)
+
+	for n,(addr,unspents) in enumerate(addr_data.items(),1):
+		Msg(f'\n{n:{col1w}}) Address: {addr.hl()}')
+
+		if unspents:
+			heights = { u['height'] for u in unspents }
+			Msg('{}Balance: {}'.format(
+				indent,
+				proto.coin_amt(sum(u['amount'] for u in unspents)).hl2(unit=True,fs='{:,}') )),
+			Msg('{}{} unspent output{} in {} block{}'.format(
+				indent,
+				red(str(len(unspents))),
+				suf(unspents),
+				red(str(len(heights))),
+				suf(heights) ))
+			blk_w = len(str(unspents[-1]['height']))
+			fs = '%s{:%s} {:19} {:64} {:4} {}' % (indent,max(5,blk_w))
+			Msg(fs.format('Block','Date','TxID','Vout','   Amount'))
+			for u in unspents:
+				Msg(fs.format(
+					u['height'],
+					make_timestr( blk_hdrs[u['height']]['time'] ),
+					CoinTxID(u['txid']).hl(),
+					red(str(u['vout']).rjust(4)),
+					proto.coin_amt(u['amount']).fmt(color=True,fs='6.8')
+				))
+		else:
+			Msg(f'{indent}No balance')
+
+def do_output_tabular(proto,addr_data,blk_hdrs):
+
+	col1w = len(str(len(addr_data))) + 1
+	max_addrw = max(len(addr) for addr in addr_data)
+	fb_heights = [str(unspents[0]['height']) if unspents else '' for unspents in addr_data.values()]
+	lb_heights = [str(unspents[-1]['height']) if unspents else '' for unspents in addr_data.values()]
+	fb_w = max(len(h) for h in fb_heights)
+	lb_w = max(len(h) for h in lb_heights)
+
+	fs = (
+		' {n:>%s} {a} {u} {b:>%s} {t:19}  {B:>%s} {T:19} {A}' % (col1w,max(5,fb_w),max(4,lb_w))
+			if opt.first_block else
+		' {n:>%s} {a} {u} {B:>%s} {T:19} {A}' % (col1w,max(4,lb_w)) )
+
+	Msg('\n' + fs.format(
+		n = '',
+		a = 'Address'.ljust(max_addrw),
+		u = 'UTXOs',
+		b = 'First',
+		t = 'Block',
+		B = 'Last',
+		T = 'Block',
+		A = '     Amount' ))
+
+	for n,(addr,unspents) in enumerate(addr_data.items(),1):
+		if unspents:
+			Msg(fs.format(
+				n = str(n) + ')',
+				a = addr.fmt(width=max_addrw,color=True),
+				u = red(str(len(unspents)).rjust(5)),
+				b = unspents[0]['height'],
+				t = make_timestr( blk_hdrs[unspents[0]['height']]['time'] ),
+				B = unspents[-1]['height'],
+				T = make_timestr( blk_hdrs[unspents[-1]['height']]['time'] ),
+				A = proto.coin_amt(sum(u['amount'] for u in unspents)).fmt(color=True,fs='7.8')
+			))
+		else:
+			Msg(fs.format(
+				n = str(n) + ')',
+				a = addr.fmt(width=max_addrw,color=True),
+				u = '    -',
+				b = '-',
+				t = '',
+				B = '-',
+				T = '',
+				A = '     -' ))
+
+async def main(req_addrs):
+
+	from mmgen.protocol import init_proto_from_opts
+	proto = init_proto_from_opts(need_amt=True)
+
+	from mmgen.addr import CoinAddr
+	addrs = [CoinAddr(proto,addr) for addr in req_addrs]
+
+	from mmgen.rpc import rpc_init
+	rpc = await rpc_init(proto)
+
+	height = await rpc.call('getblockcount')
+	Msg(f'{proto.coin} {proto.network.upper()} [height {height}]')
+
+	from mmgen.base_proto.bitcoin.misc import scantxoutset
+	res = await scantxoutset( rpc, [f'addr({addr})' for addr in addrs] )
+
+	if not res['success']:
+		die(1,'UTXO scanning failed or was interrupted')
+	elif not res['unspents']:
+		msg('Address has no balance' if len(addrs) == 1 else
+			'Addresses have no balances' )
+	else:
+		addr_data = {k:[] for k in addrs}
+
+		if 'desc' in res['unspents'][0]:
+			import re
+			for unspent in sorted(res['unspents'],key=lambda x: x['height']):
+				addr = re.match('addr\((.*?)\)',unspent['desc'])[1]
+				addr_data[addr].append(unspent)
+		else:
+			from mmgen.base_proto.bitcoin.tx.base import scriptPubKey2addr
+			for unspent in sorted(res['unspents'],key=lambda x: x['height']):
+				addr = scriptPubKey2addr( proto, unspent['scriptPubKey'] )[0]
+				addr_data[addr].append(unspent)
+
+		good_addrs = len([v for v in addr_data.values() if v])
+
+		Msg('Total: {} in {} address{}'.format(
+			proto.coin_amt(res['total_amount']).hl2(unit=True,fs='{:,}'),
+			red(str(good_addrs)),
+			suf(good_addrs,'es')
+		))
+
+		blk_heights = {i['height'] for i in res['unspents']}
+		blk_hashes = await rpc.batch_call('getblockhash', [(h,) for h in blk_heights])
+		blk_hdrs = await rpc.batch_call('getblockheader', [(H,) for H in blk_hashes])
+
+		(do_output_tabular if opt.tabular else do_output)( proto, addr_data, dict(zip(blk_heights,blk_hdrs)) )
+
+cmd_args = opts.init(opts_data,init_opts={'rpc_backend':'aiohttp'})
+
+if len(cmd_args) < 1:
+	die(1,'This command requires at least one coin address argument')
+
+from mmgen.obj import CoinTxID,Int
+
+try:
+	run_session(main(cmd_args))
+except KeyboardInterrupt:
+	sys.stderr.write('\n')

+ 1 - 0
setup.cfg

@@ -30,6 +30,7 @@ packages =
 	mmgen_node_tools.data
 
 scripts =
+	cmds/mmnode-addrbal
 	cmds/mmnode-blocks-info
 	cmds/mmnode-feeview
 	cmds/mmnode-halving-calculator

+ 60 - 0
test/test_py_d/ts_regtest.py

@@ -13,13 +13,25 @@ ts_regtest.py: Regtest tests for the test.py test suite
 """
 
 import os
+from mmgen.globalvars import g
 from mmgen.opts import opt
 from mmgen.util import die,gmsg
+from mmgen.protocol import init_proto
 from ..include.common import *
 from .common import *
 
 from .ts_base import *
 
+args1 = ['--bob']
+args2 = ['--bob','--rpc-backend=http']
+
+def gen_addrs(proto,network,keys):
+	from mmgen.tool.api import tool_api
+	tool = tool_api()
+	tool.init_coin(proto.coin,'regtest')
+	tool.addrtype = proto.mmtypes[-1]
+	return [tool.privhex2addr('{:064x}'.format(key)) for key in keys]
+
 class TestSuiteRegtest(TestSuiteBase):
 	'various operations via regtest mode'
 	networks = ('btc','ltc','bch')
@@ -30,6 +42,18 @@ class TestSuiteRegtest(TestSuiteBase):
 	deterministic = False
 	cmd_group = (
 		('setup',                     'regtest (Bob and Alice) mode setup'),
+		('sendto1',                   'sending funds to address #1 (1)'),
+		('sendto2',                   'sending funds to address #1 (2)'),
+		('sendto3',                   'sending funds to address #2'),
+		('addrbal_single',            'getting address balance (single address)'),
+		('addrbal_multiple',          'getting address balances (multiple addresses)'),
+		('addrbal_multiple_tabular1', 'getting address balances (multiple addresses, tabular output)'),
+		('addrbal_multiple_tabular2', 'getting address balances (multiple addresses, tabular, show first block)'),
+		('addrbal_nobal1',            'getting address balances (no balance)'),
+		('addrbal_nobal2',            'getting address balances (no balances)'),
+		('addrbal_nobal3',            'getting address balances (one null balance)'),
+		('addrbal_nobal3_tabular1',   'getting address balances (one null balance, tabular output)'),
+		('addrbal_nobal3_tabular2',   'getting address balances (one null balance, tabular, show first block)'),
 		('stop',                      'stopping regtest daemon'),
 	)
 
@@ -39,6 +63,8 @@ class TestSuiteRegtest(TestSuiteBase):
 			return
 		if self.proto.testnet:
 			die(2,'--testnet and --regtest options incompatible with regtest test suite')
+		self.proto = init_proto(self.proto.coin,network='regtest',need_amt=True)
+		self.addrs = gen_addrs(self.proto,'regtest',[1,2,3,4,5])
 
 		os.environ['MMGEN_TEST_SUITE_REGTEST'] = '1'
 
@@ -55,6 +81,40 @@ class TestSuiteRegtest(TestSuiteBase):
 			t.expect(s)
 		return t
 
+	def sendto(self,addr,amt):
+		return self.spawn('mmgen-regtest',['send',addr,amt])
+
+	def sendto1(self): return self.sendto(self.addrs[0],'0.123')
+	def sendto2(self): return self.sendto(self.addrs[0],'0.234')
+	def sendto3(self): return self.sendto(self.addrs[1],'0.345')
+
+	def addrbal_single(self):
+		return self.spawn('mmnode-addrbal',args2+[self.addrs[0]])
+
+	def addrbal_multiple(self):
+		return self.spawn('mmnode-addrbal',args2+[self.addrs[1],self.addrs[0]])
+
+	def addrbal_multiple_tabular1(self):
+		return self.spawn('mmnode-addrbal',args2+['--tabular',self.addrs[1],self.addrs[0]])
+
+	def addrbal_multiple_tabular2(self):
+		return self.spawn('mmnode-addrbal',args1+['--tabular','--first-block',self.addrs[1],self.addrs[0]])
+
+	def addrbal_nobal1(self):
+		return self.spawn('mmnode-addrbal',args2+[self.addrs[2]])
+
+	def addrbal_nobal2(self):
+		return self.spawn('mmnode-addrbal',args2+[self.addrs[2],self.addrs[3]])
+
+	def addrbal_nobal3(self):
+		return self.spawn('mmnode-addrbal',args2+[self.addrs[4],self.addrs[0],self.addrs[3]])
+
+	def addrbal_nobal3_tabular1(self):
+		return self.spawn('mmnode-addrbal',args2+['--tabular',self.addrs[4],self.addrs[0],self.addrs[3]])
+
+	def addrbal_nobal3_tabular2(self):
+		return self.spawn('mmnode-addrbal',args1+['--tabular','--first-block',self.addrs[4],self.addrs[0],self.addrs[3]])
+
 	def stop(self):
 		if opt.no_daemon_stop:
 			self.spawn('',msg_only=True)