ctl.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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
  9. # https://gitlab.com/mmgen/mmgen
  10. """
  11. base_proto.bitcoin.twctl: Bitcoin base protocol tracking wallet control class
  12. """
  13. from ....globalvars import g
  14. from ....tw.ctl import TrackingWallet
  15. from ....util import msg,msg_r,rmsg,vmsg,die,suf,fmt_list,write_mode
  16. class BitcoinTrackingWallet(TrackingWallet):
  17. def init_empty(self):
  18. self.data = { 'coin': self.proto.coin, 'addresses': {} }
  19. def upgrade_wallet_maybe(self):
  20. pass
  21. async def rpc_get_balance(self,addr):
  22. raise NotImplementedError('not implemented')
  23. @write_mode
  24. async def import_address(self,addr,label): # rescan is True by default, so set to False
  25. return await self.rpc.call('importaddress',addr,label,False)
  26. @write_mode
  27. def batch_import_address(self,arg_list): # rescan is True by default, so set to False
  28. return self.rpc.batch_call('importaddress',[a+(False,) for a in arg_list])
  29. @write_mode
  30. async def remove_address(self,addr):
  31. raise NotImplementedError(f'address removal not implemented for coin {self.proto.coin}')
  32. @write_mode
  33. async def set_label(self,coinaddr,lbl):
  34. args = self.rpc.daemon.set_label_args( self.rpc, coinaddr, lbl )
  35. try:
  36. return await self.rpc.call(*args)
  37. except Exception as e:
  38. rmsg(e.args[0])
  39. return False
  40. @write_mode
  41. async def rescan_blockchain(self,start,stop):
  42. start = start or 0
  43. endless = stop == None
  44. CR = '\n' if g.test_suite else '\r'
  45. if not ( start >= 0 and (stop if stop is not None else start) >= start ):
  46. die(1,f'{start} {stop}: invalid range')
  47. async def do_scan(chunks,tip):
  48. res = None
  49. for a,b in chunks:
  50. msg_r(f'{CR}Scanning blocks {a}-{b} ')
  51. res = await self.rpc.call('rescanblockchain',a,b,timeout=7200)
  52. if res['start_height'] != a or res['stop_height'] != b:
  53. die(1,f'\nAn error occurred in block range {a}-{b}')
  54. msg('')
  55. return b if res else tip
  56. def gen_chunks(start,stop,tip):
  57. n = start
  58. if endless:
  59. stop = tip
  60. elif stop > tip:
  61. die(1,f'{stop}: stop value is higher than chain tip')
  62. while n <= stop:
  63. yield ( n, min(n+99,stop) )
  64. n += 100
  65. last_block = await do_scan(gen_chunks(start,stop,self.rpc.blockcount),self.rpc.blockcount)
  66. if endless:
  67. tip = await self.rpc.call('getblockcount')
  68. while last_block < tip:
  69. last_block = await do_scan(gen_chunks(last_block+1,tip),tip)
  70. tip = await self.rpc.call('getblockcount')
  71. msg('Done')
  72. @write_mode
  73. async def rescan_address(self,addrspec):
  74. res = await self.resolve_address(addrspec,None)
  75. if not res:
  76. return False
  77. return await self.rescan_addresses([res.coinaddr])
  78. @write_mode
  79. async def rescan_addresses(self,coin_addrs):
  80. import asyncio
  81. async def do_scan():
  82. return await self.rpc.call(
  83. 'scantxoutset',
  84. 'start',
  85. [f'addr({a})' for a in coin_addrs],
  86. timeout = 720 ) # call may take several minutes to complete
  87. async def do_status():
  88. m = f'{CR}Scanning UTXO set: '
  89. msg_r(m)
  90. while True:
  91. await asyncio.sleep(2)
  92. res = await self.rpc.call('scantxoutset','status')
  93. if res:
  94. msg_r(m + f'{res["progress"]}% completed ')
  95. if task1.done():
  96. msg('')
  97. return
  98. CR = '\r'
  99. if self.rpc.backend.name == 'aiohttp':
  100. task1 = asyncio.create_task( do_scan() )
  101. task2 = asyncio.create_task( do_status() )
  102. res = await task1
  103. await task2
  104. else:
  105. msg_r(f'Scanning UTXO set, this could take several minutes...')
  106. res = await do_scan()
  107. msg('done')
  108. if not res['success']:
  109. msg('UTXO scanning failed or was interrupted')
  110. return False
  111. elif res['unspents']:
  112. blocks = sorted({ i['height'] for i in res['unspents'] })
  113. msg('Found {} unspent output{} in {} block{}'.format(
  114. len(res['unspents']),
  115. suf(res['unspents']),
  116. len(blocks),
  117. suf(blocks) ))
  118. vmsg(f'Blocks to rescan: {fmt_list(blocks,fmt="bare")}')
  119. for n,block in enumerate(blocks):
  120. msg_r(f'{CR}Rescanning block: {block} ({n+1}/{len(blocks)})')
  121. # httplib seems to require fresh connection here, so specify timeout
  122. await self.rpc.call('rescanblockchain',block,block,timeout=60)
  123. msg('\nRescan completed OK')
  124. return True
  125. else:
  126. msg('Imported address has no balance' if len(coin_addrs) == 1 else
  127. 'Imported addresses have no balances' )
  128. return True