rpc.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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. wallet_path = '/'
  68. async def __init__(self,proto,daemon,backend):
  69. self.proto = proto
  70. self.daemon = daemon
  71. self.call_sigs = getattr(CallSigs,daemon.id,None)
  72. super().__init__(
  73. host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'),
  74. port = daemon.rpc_port )
  75. self.set_auth() # set_auth() requires cookie, so must be called after __init__() tests daemon is listening
  76. self.set_backend(backend) # backend requires self.auth
  77. self.cached = {}
  78. self.caps = ('full_node',)
  79. for func,cap in (
  80. ('setlabel','label_api'),
  81. ('getdeploymentinfo','deployment_info'),
  82. ('signrawtransactionwithkey','sign_with_key') ):
  83. if len((await self.call('help',func)).split('\n')) > 3:
  84. self.caps += (cap,)
  85. call_group = [
  86. ('getblockcount',()),
  87. ('getblockhash',(0,)),
  88. ('getnetworkinfo',()),
  89. ('getblockchaininfo',()),
  90. ] + (
  91. [('getdeploymentinfo',())] if 'deployment_info' in self.caps else []
  92. )
  93. (
  94. self.blockcount,
  95. block0,
  96. self.cached['networkinfo'],
  97. self.cached['blockchaininfo'],
  98. self.cached['deploymentinfo'],
  99. ) = (
  100. await self.gathered_call(None,tuple(call_group))
  101. ) + (
  102. [] if 'deployment_info' in self.caps else [None]
  103. )
  104. self.daemon_version = self.cached['networkinfo']['version']
  105. self.daemon_version_str = self.cached['networkinfo']['subversion']
  106. self.chain = self.cached['blockchaininfo']['chain']
  107. tip = await self.call('getblockhash',self.blockcount)
  108. self.cur_date = (await self.call('getblockheader',tip))['time']
  109. if self.chain != 'regtest':
  110. self.chain += 'net'
  111. assert self.chain in self.proto.networks
  112. async def check_chainfork_mismatch(block0):
  113. try:
  114. if block0 != self.proto.block0:
  115. raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol')
  116. for fork in self.proto.forks:
  117. if fork.height == None or self.blockcount < fork.height:
  118. break
  119. if fork.hash != await self.call('getblockhash',fork.height):
  120. die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?')
  121. except Exception as e:
  122. die(2,'{!s}\n{c!r} requested, but this is not the {c} chain!'.format(e,c=self.proto.coin))
  123. if self.chain == 'mainnet': # skip this for testnet, as Genesis block may change
  124. await check_chainfork_mismatch(block0)
  125. if not self.chain == 'regtest':
  126. await self.check_tracking_wallet()
  127. # for regtest, wallet path must remain '/' until Carol’s user wallet has been created
  128. if g.regtest_user:
  129. self.wallet_path = f'/wallet/{g.regtest_user}'
  130. def make_host_path(self,wallet):
  131. return f'/wallet/{wallet}' if wallet else self.wallet_path
  132. async def check_tracking_wallet(self,wallet_checked=[]):
  133. if not wallet_checked:
  134. wallets = await self.call('listwallets')
  135. if len(wallets) == 0:
  136. wname = self.daemon.tracking_wallet_name
  137. await self.icall('createwallet',wallet_name=wname)
  138. ymsg(f'Created {self.daemon.coind_name} wallet {wname!r}')
  139. elif len(wallets) > 1: # support only one loaded wallet for now
  140. die(4,f'ERROR: more than one {self.daemon.coind_name} wallet loaded: {wallets}')
  141. wallet_checked.append(True)
  142. def get_daemon_cfg_fn(self):
  143. # Use dirname() to remove 'bob' or 'alice' component
  144. return os.path.join(
  145. (os.path.dirname(g.data_dir) if self.proto.regtest else self.daemon.datadir),
  146. self.daemon.cfg_file )
  147. def get_daemon_auth_cookie_fn(self):
  148. return os.path.join(self.daemon.network_datadir,'.cookie')
  149. def get_daemon_cfg_options(self,req_keys):
  150. fn = self.get_daemon_cfg_fn()
  151. from ...opts import opt
  152. try:
  153. lines = get_lines_from_file(fn,'daemon config file',silent=not opt.verbose)
  154. except:
  155. vmsg(f'Warning: {fn!r} does not exist or is unreadable')
  156. return dict((k,None) for k in req_keys)
  157. def gen():
  158. for key in req_keys:
  159. val = None
  160. for l in lines:
  161. if l.startswith(key):
  162. res = l.split('=',1)
  163. if len(res) == 2 and not ' ' in res[1].strip():
  164. val = res[1].strip()
  165. yield (key,val)
  166. return dict(gen())
  167. def get_daemon_auth_cookie(self):
  168. fn = self.get_daemon_auth_cookie_fn()
  169. return get_lines_from_file(fn,'cookie',quiet=True)[0] if os.access(fn,os.R_OK) else ''
  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', # address (address or script) label rescan p2sh (Add P2SH version of the script)
  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. )