rpc.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  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. proto.btc.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,fmt
  17. from ...fileutil import get_lines_from_file
  18. from ...rpc import RPCClient,auth_data
  19. no_credentials_errmsg = """
  20. Error: no {proto_name} RPC authentication method found
  21. RPC credentials must be supplied using one of the following methods:
  22. 1) If daemon is local and running as same user as you:
  23. - no credentials required, or matching rpcuser/rpcpassword and
  24. rpc_user/rpc_password values in {cf_name}.conf and mmgen.cfg
  25. 2) If daemon is running remotely or as different user:
  26. - matching credentials in {cf_name}.conf and mmgen.cfg as described
  27. above
  28. The --rpc-user/--rpc-password options may be supplied on the MMGen command
  29. line. They override the corresponding values in mmgen.cfg. Set them to an
  30. empty string to use cookie authentication with a local server when the
  31. options are set in mmgen.cfg.
  32. For better security, rpcauth should be used in {cf_name}.conf instead of
  33. rpcuser/rpcpassword.
  34. """
  35. class CallSigs:
  36. class bitcoin_core:
  37. @classmethod
  38. def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True):
  39. """
  40. Quirk: when --datadir is specified (even if standard), wallet is created directly in
  41. datadir, otherwise in datadir/wallets
  42. """
  43. return (
  44. 'createwallet',
  45. wallet_name, # 1. wallet_name
  46. no_keys, # 2. disable_private_keys
  47. blank, # 3. blank (no keys or seed)
  48. passphrase, # 4. passphrase (empty string for non-encrypted)
  49. False, # 5. avoid_reuse (track address reuse)
  50. False, # 6. descriptors (native descriptor wallet)
  51. load_on_startup # 7. load_on_startup
  52. )
  53. @classmethod
  54. def gettransaction(cls,txid,include_watchonly,verbose):
  55. return (
  56. 'gettransaction',
  57. txid, # 1. transaction id
  58. include_watchonly, # 2. optional, default=true for watch-only wallets, otherwise false
  59. verbose, # 3. optional, default=false -- include a `decoded` field containing
  60. # the decoded transaction (equivalent to RPC decoderawtransaction)
  61. )
  62. class litecoin_core(bitcoin_core):
  63. @classmethod
  64. def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True):
  65. return (
  66. 'createwallet',
  67. wallet_name, # 1. wallet_name
  68. no_keys, # 2. disable_private_keys
  69. blank, # 3. blank (no keys or seed)
  70. )
  71. @classmethod
  72. def gettransaction(cls,txid,include_watchonly,verbose):
  73. return (
  74. 'gettransaction',
  75. txid, # 1. transaction id
  76. include_watchonly, # 2. optional, default=true for watch-only wallets, otherwise false
  77. )
  78. class bitcoin_cash_node(litecoin_core):
  79. pass
  80. class BitcoinRPCClient(RPCClient,metaclass=AsyncInit):
  81. auth_type = 'basic'
  82. has_auth_cookie = True
  83. wallet_path = '/'
  84. async def __init__(self,proto,daemon,backend,ignore_wallet):
  85. self.proto = proto
  86. self.daemon = daemon
  87. self.call_sigs = getattr(CallSigs,daemon.id,None)
  88. super().__init__(
  89. host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'),
  90. port = daemon.rpc_port )
  91. self.set_auth()
  92. await self.set_backend_async(backend) # backend requires self.auth
  93. self.cached = {}
  94. self.caps = ('full_node',)
  95. for func,cap in (
  96. ('setlabel','label_api'),
  97. ('getdeploymentinfo','deployment_info'),
  98. ('signrawtransactionwithkey','sign_with_key') ):
  99. if len((await self.call('help',func)).split('\n')) > 3:
  100. self.caps += (cap,)
  101. call_group = [
  102. ('getblockcount',()),
  103. ('getblockhash',(0,)),
  104. ('getnetworkinfo',()),
  105. ('getblockchaininfo',()),
  106. ] + (
  107. [('getdeploymentinfo',())] if 'deployment_info' in self.caps else []
  108. )
  109. (
  110. self.blockcount,
  111. block0,
  112. self.cached['networkinfo'],
  113. self.cached['blockchaininfo'],
  114. self.cached['deploymentinfo'],
  115. ) = (
  116. await self.gathered_call(None,tuple(call_group))
  117. ) + (
  118. [] if 'deployment_info' in self.caps else [None]
  119. )
  120. self.daemon_version = self.cached['networkinfo']['version']
  121. self.daemon_version_str = self.cached['networkinfo']['subversion']
  122. self.chain = self.cached['blockchaininfo']['chain']
  123. tip = await self.call('getblockhash',self.blockcount)
  124. self.cur_date = (await self.call('getblockheader',tip))['time']
  125. if self.chain != 'regtest':
  126. self.chain += 'net'
  127. assert self.chain in self.proto.networks
  128. async def check_chainfork_mismatch(block0):
  129. try:
  130. if block0 != self.proto.block0:
  131. raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol')
  132. for fork in self.proto.forks:
  133. if fork.height == None or self.blockcount < fork.height:
  134. break
  135. if fork.hash != await self.call('getblockhash',fork.height):
  136. die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?')
  137. except Exception as e:
  138. die(2,'{!s}\n{c!r} requested, but this is not the {c} chain!'.format(e,c=self.proto.coin))
  139. if self.chain == 'mainnet': # skip this for testnet, as Genesis block may change
  140. await check_chainfork_mismatch(block0)
  141. if not ignore_wallet:
  142. await self.check_or_create_daemon_wallet()
  143. # for regtest, wallet path must remain '/' until Carol’s user wallet has been created
  144. if g.regtest_user:
  145. self.wallet_path = f'/wallet/{g.regtest_user}'
  146. def set_auth(self):
  147. """
  148. MMGen's credentials override coin daemon's
  149. """
  150. if g.rpc_user:
  151. user,passwd = (g.rpc_user,g.rpc_password)
  152. else:
  153. user,passwd = self.get_daemon_cfg_options(('rpcuser','rpcpassword')).values()
  154. if not (user and passwd):
  155. user,passwd = (self.daemon.rpc_user,self.daemon.rpc_password)
  156. if user and passwd:
  157. self.auth = auth_data(user,passwd)
  158. return
  159. if self.has_auth_cookie:
  160. cookie = self.get_daemon_auth_cookie()
  161. if cookie:
  162. self.auth = auth_data(*cookie.split(':'))
  163. return
  164. die(1, '\n\n' + fmt(no_credentials_errmsg,strip_char='\t',indent=' ').format(
  165. proto_name = self.proto.name,
  166. cf_name = (self.proto.is_fork_of or self.proto.name).lower(),
  167. ))
  168. def make_host_path(self,wallet):
  169. return f'/wallet/{wallet}' if wallet else self.wallet_path
  170. async def check_or_create_daemon_wallet(self,called=[],wallet_create=True):
  171. """
  172. Returns True if the correct tracking wallet is currently loaded or if a new one
  173. is created, False otherwise
  174. """
  175. if called or (self.chain == 'regtest' and g.regtest_user != 'carol'):
  176. return False
  177. twname = self.daemon.tracking_wallet_name
  178. loaded_wnames = await self.call('listwallets')
  179. wnames = [i['name'] for i in (await self.call('listwalletdir'))['wallets']]
  180. m = f'Please fix your {self.daemon.desc} wallet installation or cmdline options'
  181. ret = False
  182. if g.carol:
  183. if 'carol' in loaded_wnames:
  184. ret = True
  185. elif wallet_create:
  186. await self.icall('createwallet',wallet_name='carol')
  187. ymsg(f'Created {self.daemon.coind_name} wallet {"carol"!r}')
  188. ret = True
  189. elif len(loaded_wnames) == 1:
  190. loaded_wname = loaded_wnames[0]
  191. if twname in wnames and loaded_wname != twname:
  192. await self.call('unloadwallet',loaded_wname)
  193. await self.call('loadwallet',twname)
  194. elif loaded_wname == '':
  195. ymsg(f'WARNING: use of default wallet as tracking wallet is not recommended!\n{m}')
  196. elif loaded_wname != twname:
  197. ymsg(f'WARNING: loaded wallet {loaded_wname!r} is not {twname!r}\n{m}')
  198. ret = True
  199. elif len(loaded_wnames) == 0:
  200. if twname in wnames:
  201. await self.call('loadwallet',twname)
  202. ret = True
  203. elif wallet_create:
  204. await self.icall('createwallet',wallet_name=twname)
  205. ymsg(f'Created {self.daemon.coind_name} wallet {twname!r}')
  206. ret = True
  207. else: # support only one loaded wallet for now
  208. die(4,f'ERROR: more than one {self.daemon.coind_name} wallet loaded: {loaded_wnames}')
  209. if wallet_create:
  210. called.append(True)
  211. return ret
  212. def get_daemon_cfg_fn(self):
  213. # Use dirname() to remove 'bob' or 'alice' component
  214. return os.path.join(
  215. (os.path.dirname(g.data_dir) if self.proto.regtest else self.daemon.datadir),
  216. self.daemon.cfg_file )
  217. def get_daemon_cfg_options(self,req_keys):
  218. fn = self.get_daemon_cfg_fn()
  219. from ...opts import opt
  220. try:
  221. lines = get_lines_from_file(fn,'daemon config file',silent=not opt.verbose)
  222. except:
  223. vmsg(f'Warning: {fn!r} does not exist or is unreadable')
  224. return dict((k,None) for k in req_keys)
  225. def gen():
  226. for key in req_keys:
  227. val = None
  228. for l in lines:
  229. if l.startswith(key):
  230. res = l.split('=',1)
  231. if len(res) == 2 and not ' ' in res[1].strip():
  232. val = res[1].strip()
  233. yield (key,val)
  234. return dict(gen())
  235. def get_daemon_auth_cookie(self):
  236. fn = self.daemon.auth_cookie_fn
  237. return get_lines_from_file(fn,'cookie',quiet=True)[0] if os.access(fn,os.R_OK) else ''
  238. def info(self,info_id):
  239. def segwit_is_active():
  240. if 'deployment_info' in self.caps:
  241. return (
  242. self.cached['deploymentinfo']['deployments']['segwit']['active']
  243. or ( g.test_suite and not self.chain == 'regtest' )
  244. )
  245. d = self.cached['blockchaininfo']
  246. try:
  247. if d['softforks']['segwit']['active'] == True:
  248. return True
  249. except:
  250. pass
  251. try:
  252. if d['bip9_softforks']['segwit']['status'] == 'active':
  253. return True
  254. except:
  255. pass
  256. if g.test_suite and not self.chain == 'regtest':
  257. return True
  258. return False
  259. return locals()[info_id]()
  260. rpcmethods = (
  261. 'backupwallet',
  262. 'createrawtransaction',
  263. 'decoderawtransaction',
  264. 'disconnectnode',
  265. 'estimatefee',
  266. 'estimatesmartfee',
  267. 'getaddressesbyaccount',
  268. 'getaddressesbylabel',
  269. 'getblock',
  270. 'getblockchaininfo',
  271. 'getblockcount',
  272. 'getblockhash',
  273. 'getblockheader',
  274. 'getblockstats', # mmgen-node-tools
  275. 'getmempoolinfo',
  276. 'getmempoolentry',
  277. 'getnettotals',
  278. 'getnetworkinfo',
  279. 'getpeerinfo',
  280. 'getrawmempool',
  281. 'getmempoolentry',
  282. 'getrawtransaction',
  283. 'gettransaction',
  284. 'importaddress', # address (address or script) label rescan p2sh (Add P2SH version of the script)
  285. 'listaccounts',
  286. 'listlabels',
  287. 'listunspent',
  288. 'setlabel',
  289. 'sendrawtransaction',
  290. 'signrawtransaction',
  291. 'signrawtransactionwithkey', # method new to Core v0.17.0
  292. 'validateaddress',
  293. 'walletpassphrase',
  294. )