rpc.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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.rpc: Bitcoin base protocol RPC client class
  12. """
  13. import os
  14. from ...globalvars import g
  15. from ...base_obj import AsyncInit
  16. from ...util import ymsg,vmsg,die
  17. from ...fileutil import get_lines_from_file
  18. from ...rpc import RPCClient
  19. class CallSigs:
  20. class bitcoin_core:
  21. @classmethod
  22. def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True):
  23. """
  24. Quirk: when --datadir is specified (even if standard), wallet is created directly in
  25. datadir, otherwise in datadir/wallets
  26. """
  27. return (
  28. 'createwallet',
  29. wallet_name, # 1. wallet_name
  30. no_keys, # 2. disable_private_keys
  31. blank, # 3. blank (no keys or seed)
  32. passphrase, # 4. passphrase (empty string for non-encrypted)
  33. False, # 5. avoid_reuse (track address reuse)
  34. False, # 6. descriptors (native descriptor wallet)
  35. load_on_startup # 7. load_on_startup
  36. )
  37. @classmethod
  38. def gettransaction(cls,txid,include_watchonly,verbose):
  39. return (
  40. 'gettransaction',
  41. txid, # 1. transaction id
  42. include_watchonly, # 2. optional, default=true for watch-only wallets, otherwise false
  43. verbose, # 3. optional, default=false -- include a `decoded` field containing
  44. # the decoded transaction (equivalent to RPC decoderawtransaction)
  45. )
  46. class litecoin_core(bitcoin_core):
  47. @classmethod
  48. def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True):
  49. return (
  50. 'createwallet',
  51. wallet_name, # 1. wallet_name
  52. no_keys, # 2. disable_private_keys
  53. blank, # 3. blank (no keys or seed)
  54. )
  55. @classmethod
  56. def gettransaction(cls,txid,include_watchonly,verbose):
  57. return (
  58. 'gettransaction',
  59. txid, # 1. transaction id
  60. include_watchonly, # 2. optional, default=true for watch-only wallets, otherwise false
  61. )
  62. class bitcoin_cash_node(litecoin_core):
  63. pass
  64. class BitcoinRPCClient(RPCClient,metaclass=AsyncInit):
  65. auth_type = 'basic'
  66. has_auth_cookie = True
  67. async def __init__(self,proto,daemon,backend):
  68. self.proto = proto
  69. self.daemon = daemon
  70. self.call_sigs = getattr(CallSigs,daemon.id,None)
  71. super().__init__(
  72. host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'),
  73. port = daemon.rpc_port )
  74. self.set_auth() # set_auth() requires cookie, so must be called after __init__() tests daemon is listening
  75. self.set_backend(backend) # backend requires self.auth
  76. self.cached = {}
  77. self.caps = ('full_node',)
  78. for func,cap in (
  79. ('setlabel','label_api'),
  80. ('getdeploymentinfo','deployment_info'),
  81. ('signrawtransactionwithkey','sign_with_key') ):
  82. if len((await self.call('help',func)).split('\n')) > 3:
  83. self.caps += (cap,)
  84. call_group = [
  85. ('getblockcount',()),
  86. ('getblockhash',(0,)),
  87. ('getnetworkinfo',()),
  88. ('getblockchaininfo',()),
  89. ] + (
  90. [('getdeploymentinfo',())] if 'deployment_info' in self.caps else []
  91. )
  92. (
  93. self.blockcount,
  94. block0,
  95. self.cached['networkinfo'],
  96. self.cached['blockchaininfo'],
  97. self.cached['deploymentinfo'],
  98. ) = (
  99. await self.gathered_call(None,tuple(call_group))
  100. ) + (
  101. [] if 'deployment_info' in self.caps else [None]
  102. )
  103. self.daemon_version = self.cached['networkinfo']['version']
  104. self.daemon_version_str = self.cached['networkinfo']['subversion']
  105. self.chain = self.cached['blockchaininfo']['chain']
  106. tip = await self.call('getblockhash',self.blockcount)
  107. self.cur_date = (await self.call('getblockheader',tip))['time']
  108. if self.chain != 'regtest':
  109. self.chain += 'net'
  110. assert self.chain in self.proto.networks
  111. async def check_chainfork_mismatch(block0):
  112. try:
  113. if block0 != self.proto.block0:
  114. raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol')
  115. for fork in self.proto.forks:
  116. if fork.height == None or self.blockcount < fork.height:
  117. break
  118. if fork.hash != await self.call('getblockhash',fork.height):
  119. die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?')
  120. except Exception as e:
  121. die(2,'{!s}\n{c!r} requested, but this is not the {c} chain!'.format(e,c=self.proto.coin))
  122. if self.chain == 'mainnet': # skip this for testnet, as Genesis block may change
  123. await check_chainfork_mismatch(block0)
  124. if not self.chain == 'regtest':
  125. await self.check_tracking_wallet()
  126. async def check_tracking_wallet(self,wallet_checked=[]):
  127. if not wallet_checked:
  128. wallets = await self.call('listwallets')
  129. if len(wallets) == 0:
  130. wname = self.daemon.tracking_wallet_name
  131. await self.icall('createwallet',wallet_name=wname)
  132. ymsg(f'Created {self.daemon.coind_name} wallet {wname!r}')
  133. elif len(wallets) > 1: # support only one loaded wallet for now
  134. die(4,f'ERROR: more than one {self.daemon.coind_name} wallet loaded: {wallets}')
  135. wallet_checked.append(True)
  136. def get_daemon_cfg_fn(self):
  137. # Use dirname() to remove 'bob' or 'alice' component
  138. return os.path.join(
  139. (os.path.dirname(g.data_dir) if self.proto.regtest else self.daemon.datadir),
  140. self.daemon.cfg_file )
  141. def get_daemon_auth_cookie_fn(self):
  142. return os.path.join(self.daemon.network_datadir,'.cookie')
  143. def get_daemon_cfg_options(self,req_keys):
  144. fn = self.get_daemon_cfg_fn()
  145. from ...opts import opt
  146. try:
  147. lines = get_lines_from_file(fn,'daemon config file',silent=not opt.verbose)
  148. except:
  149. vmsg(f'Warning: {fn!r} does not exist or is unreadable')
  150. return dict((k,None) for k in req_keys)
  151. def gen():
  152. for key in req_keys:
  153. val = None
  154. for l in lines:
  155. if l.startswith(key):
  156. res = l.split('=',1)
  157. if len(res) == 2 and not ' ' in res[1].strip():
  158. val = res[1].strip()
  159. yield (key,val)
  160. return dict(gen())
  161. def get_daemon_auth_cookie(self):
  162. fn = self.get_daemon_auth_cookie_fn()
  163. return get_lines_from_file(fn,'cookie',quiet=True)[0] if os.access(fn,os.R_OK) else ''
  164. @staticmethod
  165. def make_host_path(wallet):
  166. return (
  167. '/wallet/{}'.format('bob' if g.bob else 'alice') if (g.bob or g.alice) else
  168. '/wallet/{}'.format(wallet) if wallet else '/'
  169. )
  170. def info(self,info_id):
  171. def segwit_is_active():
  172. if 'deployment_info' in self.caps:
  173. return (
  174. self.cached['deploymentinfo']['deployments']['segwit']['active']
  175. or ( g.test_suite and not os.getenv('MMGEN_TEST_SUITE_REGTEST') )
  176. )
  177. d = self.cached['blockchaininfo']
  178. try:
  179. if d['softforks']['segwit']['active'] == True:
  180. return True
  181. except:
  182. pass
  183. try:
  184. if d['bip9_softforks']['segwit']['status'] == 'active':
  185. return True
  186. except:
  187. pass
  188. if g.test_suite:
  189. return True
  190. return False
  191. return locals()[info_id]()
  192. rpcmethods = (
  193. 'backupwallet',
  194. 'createrawtransaction',
  195. 'decoderawtransaction',
  196. 'disconnectnode',
  197. 'estimatefee',
  198. 'estimatesmartfee',
  199. 'getaddressesbyaccount',
  200. 'getaddressesbylabel',
  201. 'getblock',
  202. 'getblockchaininfo',
  203. 'getblockcount',
  204. 'getblockhash',
  205. 'getblockheader',
  206. 'getblockstats', # mmgen-node-tools
  207. 'getmempoolinfo',
  208. 'getmempoolentry',
  209. 'getnettotals',
  210. 'getnetworkinfo',
  211. 'getpeerinfo',
  212. 'getrawmempool',
  213. 'getmempoolentry',
  214. 'getrawtransaction',
  215. 'gettransaction',
  216. 'importaddress',
  217. 'listaccounts',
  218. 'listlabels',
  219. 'listunspent',
  220. 'setlabel',
  221. 'sendrawtransaction',
  222. 'signrawtransaction',
  223. 'signrawtransactionwithkey', # method new to Core v0.17.0
  224. 'validateaddress',
  225. 'walletpassphrase',
  226. )