rpc.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 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-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. proto.xmr.rpc: Monero base protocol RPC client class
  12. """
  13. import re
  14. from ...rpc.local import RPCClient
  15. from ...rpc.util import IPPort, auth_data
  16. class MoneroRPCClient(RPCClient):
  17. auth_type = None
  18. network_proto = 'https'
  19. verify_server = False
  20. def __init__(
  21. self,
  22. cfg,
  23. proto,
  24. *,
  25. host,
  26. port,
  27. user,
  28. passwd,
  29. test_connection = True,
  30. proxy = None,
  31. daemon = None,
  32. ignore_daemon_version = False):
  33. self.proto = proto
  34. if proxy is not None:
  35. self.proxy = IPPort(proxy)
  36. test_connection = False
  37. if host.endswith('.onion'):
  38. self.network_proto = 'http'
  39. super().__init__(cfg, host, port, test_connection=test_connection)
  40. if self.auth_type:
  41. self.auth = auth_data(user, passwd)
  42. if True:
  43. self.set_backend('requests')
  44. else: # insecure, for debugging only
  45. self.set_backend('curl')
  46. self.backend.exec_opts.remove('--silent')
  47. self.backend.exec_opts.append('--verbose')
  48. self.daemon = daemon
  49. if test_connection:
  50. # https://github.com/monero-project/monero src/rpc/rpc_version_str.cpp
  51. ver_str = self.call_raw('getinfo')['version']
  52. if ver_str:
  53. self.daemon_version_str = ver_str
  54. self.daemon_version = sum(
  55. int(m) * (1000 ** n) for n, m in
  56. enumerate(reversed(re.match(r'(\d+)\.(\d+)\.(\d+)\.(\d+)', ver_str).groups()))
  57. )
  58. if self.daemon and self.daemon_version > self.daemon.coind_version:
  59. self.handle_unsupported_daemon_version(
  60. proto.name,
  61. ignore_daemon_version or proto.ignore_daemon_version or self.cfg.ignore_daemon_version)
  62. else: # restricted (public) node:
  63. self.daemon_version_str = None
  64. self.daemon_version = None
  65. def call(self, method, *params, **kwargs):
  66. assert not params, f'{self.name}.call() accepts keyword arguments only'
  67. return self.process_http_resp(self.backend.run_noasync(
  68. payload = {'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': kwargs},
  69. timeout = 3600, # allow enough time to sync ≈1,000,000 blocks
  70. host_path = '/json_rpc'
  71. ))
  72. def call_raw(self, method, *params, **kwargs):
  73. assert not params, f'{self.name}.call() accepts keyword arguments only'
  74. return self.process_http_resp(self.backend.run_noasync(
  75. payload = kwargs,
  76. timeout = self.timeout,
  77. host_path = f'/{method}'
  78. ), json_rpc=False)
  79. async def do_stop_daemon(self, *, silent=False):
  80. return self.call_raw('stop_daemon') # unreliable on macOS (daemon stops, but closes connection)
  81. rpcmethods = ('get_info',)
  82. rpcmethods_raw = ('get_height', 'send_raw_transaction', 'stop_daemon')
  83. class MoneroWalletRPCClient(MoneroRPCClient):
  84. auth_type = 'digest'
  85. def __init__(self, cfg, daemon, *, test_connection=True):
  86. RPCClient.__init__(
  87. self = self,
  88. cfg = cfg,
  89. host = 'localhost',
  90. port = daemon.rpc_port,
  91. test_connection = test_connection)
  92. self.daemon = daemon
  93. self.auth = auth_data(daemon.user, daemon.passwd)
  94. self.set_backend('requests')
  95. rpcmethods = (
  96. 'get_version',
  97. 'get_height', # sync height of the open wallet
  98. 'get_balance', # account_index=0, address_indices=[]
  99. 'create_wallet', # filename, password, language="English"
  100. 'open_wallet', # filename, password
  101. 'close_wallet',
  102. # filename,password,seed (restore_height,language,seed_offset,autosave_current)
  103. 'restore_deterministic_wallet',
  104. 'refresh', # start_height
  105. )
  106. def call_raw(self, *args, **kwargs):
  107. raise NotImplementedError('call_raw() not implemented for class MoneroWalletRPCClient')
  108. async def do_stop_daemon(self, *, silent=False):
  109. """
  110. NB: the 'stop_wallet' RPC call closes the open wallet before shutting down the daemon,
  111. returning an error if no wallet is open
  112. """
  113. try:
  114. return self.call('stop_wallet')
  115. except Exception as e:
  116. from ...util import msg, msg_r, ymsg
  117. from ...color import yellow
  118. msg(f'{type(e).__name__}: {e}')
  119. msg_r(yellow('Unable to shut down wallet daemon gracefully, so killing process instead...'))
  120. ret = self.daemon.stop(silent=True)
  121. ymsg('done')
  122. return ret