from datetime import datetime import operator import json import logging import requests from .. import exceptions from ..account import Account from ..address import address, Address, SubAddress from ..numbers import from_atomic, to_atomic, PaymentID from ..seed import Seed from ..transaction import Transaction, IncomingPayment, OutgoingPayment _log = logging.getLogger(__name__) class JSONRPCDaemon(object): """ JSON RPC backend for Monero daemon :param protocol: `http` or `https` :param host: host name or IP :param port: port number :param path: path for JSON RPC requests (should not be changed) :param timeout: request timeout :param verify_ssl_certs: verify SSL certificates when connecting :param proxy_url: a proxy to use """ def __init__(self, protocol='http', host='127.0.0.1', port=18081, path='/json_rpc', user='', password='', timeout=30, verify_ssl_certs=True, proxy_url=None): self.url = '{protocol}://{host}:{port}'.format( protocol=protocol, host=host, port=port) _log.debug("JSONRPC daemon backend URL: {url}".format(url=self.url)) self.user = user self.password = password self.timeout = timeout self.verify_ssl_certs = verify_ssl_certs self.proxies = {protocol: proxy_url} def info(self): info = self.raw_jsonrpc_request('get_info') return info def send_transaction(self, blob, relay=True): res = self.raw_request('/sendrawtransaction', { 'tx_as_hex': blob, 'do_not_relay': not relay}) if res['status'] == 'OK': return res raise exceptions.TransactionBroadcastError( "{status}: {reason}".format(**res), details=res) def mempool(self): res = self.raw_request('/get_transaction_pool', {}) txs = [] for tx in res.get('transactions', []): txs.append(Transaction( hash=tx['id_hash'], fee=from_atomic(tx['fee']), timestamp=datetime.fromtimestamp(tx['receive_time']), confirmations=0)) return txs def headers(self, start_height, end_height=None): end_height = end_height or start_height res = self.raw_jsonrpc_request('get_block_headers_range', { 'start_height': start_height, 'end_height': end_height}) if res['status'] == 'OK': return res['headers'] raise exceptions.BackendException(res['status']) def transactions(self, hashes): res = self.raw_request('/get_transactions', { 'txs_hashes': hashes, 'decode_as_json': True}) if res['status'] != 'OK': raise exceptions.BackendException(res['status']) txs = [] for tx in res.get('txs', []): txs.append(Transaction( hash=tx['tx_hash'], height=None if tx['in_pool'] else tx['block_height'], timestamp=datetime.fromtimestamp(tx['block_timestamp']) if 'block_timestamp' in tx else None, blob=tx['as_hex'], json=json.loads(tx['as_json']))) return txs def raw_request(self, path, data): hdr = {'Content-Type': 'application/json'} _log.debug(u"Request: {path}\nData: {data}".format( path=path, data=json.dumps(data, indent=2, sort_keys=True))) auth = requests.auth.HTTPDigestAuth(self.user, self.password) rsp = requests.post( self.url + path, headers=hdr, data=json.dumps(data), auth=auth, timeout=self.timeout, verify=self.verify_ssl_certs, proxies=self.proxies) if rsp.status_code != 200: raise RPCError("Invalid HTTP status {code} for path {path}.".format( code=rsp.status_code, path=path)) result = rsp.json() _ppresult = json.dumps(result, indent=2, sort_keys=True) _log.debug(u"Result:\n{result}".format(result=_ppresult)) return result def raw_jsonrpc_request(self, method, params=None): hdr = {'Content-Type': 'application/json'} data = {'jsonrpc': '2.0', 'id': 0, 'method': method, 'params': params or {}} _log.debug(u"Method: {method}\nParams:\n{params}".format( method=method, params=json.dumps(params, indent=2, sort_keys=True))) auth = requests.auth.HTTPDigestAuth(self.user, self.password) rsp = requests.post( self.url + '/json_rpc', headers=hdr, data=json.dumps(data), auth=auth, timeout=self.timeout, verify=self.verify_ssl_certs, proxies=self.proxies) if rsp.status_code == 401: raise Unauthorized("401 Unauthorized. Invalid RPC user name or password.") elif rsp.status_code != 200: raise RPCError("Invalid HTTP status {code} for method {method}.".format( code=rsp.status_code, method=method)) result = rsp.json() _ppresult = json.dumps(result, indent=2, sort_keys=True) _log.debug(u"Result:\n{result}".format(result=_ppresult)) if 'error' in result: err = result['error'] _log.error(u"JSON RPC error:\n{result}".format(result=_ppresult)) raise RPCError( "Method '{method}' failed with RPC Error of unknown code {code}, " "message: {message}".format(method=method, data=data, result=result, **err)) return result['result'] class JSONRPCWallet(object): """ JSON RPC backend for Monero wallet (``monero-wallet-rpc``) :param protocol: `http` or `https` :param host: host name or IP :param port: port number :param path: path for JSON RPC requests (should not be changed) :param user: username to authenticate with over RPC :param password: password to authenticate with over RPC :param timeout: request timeout :param verify_ssl_certs: verify ssl certs for request """ _master_address = None def __init__(self, protocol='http', host='127.0.0.1', port=18088, path='/json_rpc', user='', password='', timeout=30, verify_ssl_certs=True): self.url = '{protocol}://{host}:{port}/json_rpc'.format( protocol=protocol, host=host, port=port) _log.debug("JSONRPC wallet backend URL: {url}".format(url=self.url)) self.user = user self.password = password self.timeout = timeout self.verify_ssl_certs = verify_ssl_certs _log.debug("JSONRPC wallet backend auth: '{user}'/'{stars}'".format( user=user, stars=('*' * len(password)) if password else '')) def height(self): return self.raw_request('getheight')['height'] def spend_key(self): return self.raw_request('query_key', {'key_type': 'spend_key'})['key'] def view_key(self): return self.raw_request('query_key', {'key_type': 'view_key'})['key'] def seed(self): return Seed(self.raw_request('query_key', {'key_type': 'mnemonic'})['key']) def accounts(self): accounts = [] _accounts = self.raw_request('get_accounts') idx = 0 self._master_address = Address(_accounts['subaddress_accounts'][0]['base_address']) for _acc in _accounts['subaddress_accounts']: assert idx == _acc['account_index'] accounts.append(Account(self, _acc['account_index'], label=_acc.get('label'))) idx += 1 return accounts def new_account(self, label=None): _account = self.raw_request('create_account', {'label': label}) # NOTE: the following should re-read label by _account.get('label') but the RPC # doesn't return that detail here return Account(self, _account['account_index'], label=label), SubAddress(_account['address']) def addresses(self, account=0, addr_indices=None): qdata = {'account_index': account} if addr_indices: qdata['address_index'] = addr_indices _addresses = self.raw_request('getaddress', qdata) addresses = [None] * (max(map(operator.itemgetter('address_index'), _addresses['addresses'])) + 1) for _addr in _addresses['addresses']: addresses[_addr['address_index']] = address( _addr['address'], label=_addr.get('label', None)) return addresses def new_address(self, account=0, label=None): _address = self.raw_request( 'create_address', {'account_index': account, 'label': label}) return SubAddress(_address['address']), _address['address_index'] def balances(self, account=0): _balance = self.raw_request('getbalance', {'account_index': account}) return (from_atomic(_balance['balance']), from_atomic(_balance['unlocked_balance'])) def transfers_in(self, account, pmtfilter): params = {'account_index': account, 'pending': False} method = 'get_transfers' if pmtfilter.tx_ids: method = 'get_transfer_by_txid' if pmtfilter.unconfirmed: params['in'] = pmtfilter.confirmed params['out'] = False params['pool'] = True else: if pmtfilter.payment_ids: method = 'get_bulk_payments' params['payment_ids'] = list(map(str, pmtfilter.payment_ids)) else: params['in'] = pmtfilter.confirmed params['out'] = False params['pool'] = False if method == 'get_transfers': if pmtfilter.min_height: # NOTE: the API uses (min, max] range which is confusing params['min_height'] = pmtfilter.min_height - 1 params['filter_by_height'] = True if pmtfilter.max_height: params['max_height'] = pmtfilter.max_height params['filter_by_height'] = True _pmts = self.raw_request(method, params) pmts = _pmts.get('in', []) elif method == 'get_transfer_by_txid': pmts = [] for txid in pmtfilter.tx_ids: params['txid'] = txid try: _pmts = self.raw_request(method, params, squelch_error_logging=True) except exceptions.TransactionNotFound: continue pmts.extend(_pmts['transfers']) else: # NOTE: the API uses (min, max] range which is confusing params['min_block_height'] = (pmtfilter.min_height or 1) - 1 _pmts = self.raw_request(method, params) pmts = _pmts.get('payments', []) if pmtfilter.unconfirmed: pmts.extend(_pmts.get('pool', [])) return list(pmtfilter.filter(map(self._inpayment, pmts))) def transfers_out(self, account, pmtfilter): if pmtfilter.tx_ids: pmts = [] for txid in pmtfilter.tx_ids: try: _pmts = self.raw_request( 'get_transfer_by_txid', {'account_index': account, 'txid': txid}, squelch_error_logging=True) except exceptions.TransactionNotFound: continue pmts.extend(_pmts['transfers']) else: _pmts = self.raw_request('get_transfers', { 'account_index': account, 'in': False, 'out': pmtfilter.confirmed, 'pool': False, 'pending': pmtfilter.unconfirmed}) pmts = _pmts.get('out', []) if pmtfilter.unconfirmed: pmts.extend(_pmts.get('pending', [])) return list(pmtfilter.filter(map(self._outpayment, pmts))) def _paymentdict(self, data): pid = data.get('payment_id', None) laddr = data.get('address', None) if laddr: laddr = address(laddr) result = { 'payment_id': None if pid is None else PaymentID(pid), 'amount': from_atomic(data['amount']), 'timestamp': datetime.fromtimestamp(data['timestamp']) if 'timestamp' in data else None, 'note': data.get('note', None), 'transaction': self._tx(data), 'local_address': laddr, } if 'destinations' in data: result['destinations'] = [ (address(x['address']), from_atomic(x['amount'])) for x in data.get('destinations') ] return result def _inpayment(self, data): return IncomingPayment(**self._paymentdict(data)) def _outpayment(self, data): return OutgoingPayment(**self._paymentdict(data)) def _tx(self, data): return Transaction(**{ 'hash': data.get('txid', data.get('tx_hash')), 'fee': from_atomic(data['fee']) if 'fee' in data else None, 'key': data.get('key'), 'height': data.get('height', data.get('block_height')) or None, 'timestamp': datetime.fromtimestamp(data['timestamp']) if 'timestamp' in data else None, 'blob': data.get('blob', None), 'confirmations': data.get('confirmations', None) }) def export_outputs(self): return self.raw_request('export_outputs')['outputs_data_hex'] def import_outputs(self, outputs_hex): return self.raw_request( 'import_outputs', {'outputs_data_hex': outputs_hex})['num_imported'] def export_key_images(self): return self.raw_request('export_key_images')['signed_key_images'] def import_key_images(self, key_images): _data = self.raw_request( 'import_key_images', {'signed_key_images': key_images}) return (_data['height'], from_atomic(_data['spent']), from_atomic(_data['unspent'])) def transfer(self, destinations, priority, payment_id=None, unlock_time=0, account=0, relay=True): data = { 'account_index': account, 'destinations': list(map( lambda dst: {'address': str(address(dst[0])), 'amount': to_atomic(dst[1])}, destinations)), 'priority': priority, 'unlock_time': 0, 'get_tx_keys': True, 'get_tx_hex': True, 'new_algorithm': True, 'do_not_relay': not relay, } if payment_id is not None: data['payment_id'] = str(PaymentID(payment_id)) _transfers = self.raw_request('transfer_split', data) _pertx = [dict(_tx) for _tx in map( lambda vs: zip(('txid', 'amount', 'fee', 'key', 'blob', 'payment_id'), vs), zip(*[_transfers[k] for k in ( 'tx_hash_list', 'amount_list', 'fee_list', 'tx_key_list', 'tx_blob_list')]))] for d in _pertx: d['payment_id'] = payment_id return [self._tx(data) for data in _pertx] def sweep_all(self, destination, priority, payment_id=None, subaddr_indices=None, unlock_time=0, account=0, relay=True): if not subaddr_indices: # retrieve indices of all subaddresses with positive unlocked balance bals = self.raw_request('get_balance', {'account_index': account}) subaddr_indices = [] for subaddr in bals['per_subaddress']: if subaddr.get('unlocked_balance', 0): subaddr_indices.append(subaddr['address_index']) data = { 'account_index': account, 'address': str(address(destination)), 'subaddr_indices': list(subaddr_indices), 'priority': priority, 'unlock_time': 0, 'get_tx_keys': True, 'get_tx_hex': True, 'do_not_relay': not relay, } if payment_id is not None: data['payment_id'] = str(PaymentID(payment_id)) _transfers = self.raw_request('sweep_all', data) _pertx = [dict(_tx) for _tx in map( lambda vs: zip(('txid', 'amount', 'fee', 'key', 'blob', 'payment_id'), vs), zip(*[_transfers[k] for k in ( 'tx_hash_list', 'amount_list', 'fee_list', 'tx_key_list', 'tx_blob_list')]))] for d in _pertx: d['payment_id'] = payment_id return list(zip( [self._tx(data) for data in _pertx], map(from_atomic, _transfers['amount_list']))) def raw_request(self, method, params=None, squelch_error_logging=False): hdr = {'Content-Type': 'application/json'} data = {'jsonrpc': '2.0', 'id': 0, 'method': method, 'params': params or {}} _log.debug(u"Method: {method}\nParams:\n{params}".format( method=method, params=json.dumps(params, indent=2, sort_keys=True))) auth = requests.auth.HTTPDigestAuth(self.user, self.password) rsp = requests.post( self.url, headers=hdr, data=json.dumps(data), auth=auth, timeout=self.timeout, verify=self.verify_ssl_certs) if rsp.status_code == 401: raise Unauthorized("401 Unauthorized. Invalid RPC user name or password.") elif rsp.status_code != 200: raise RPCError("Invalid HTTP status {code} for method {method}.".format( code=rsp.status_code, method=method)) result = rsp.json() _ppresult = json.dumps(result, indent=2, sort_keys=True) _log.debug(u"Result:\n{result}".format(result=_ppresult)) if 'error' in result: err = result['error'] if not squelch_error_logging: _log.error(u"JSON RPC error:\n{result}".format(result=_ppresult)) if err['code'] in _err2exc: raise _err2exc[err['code']](err['message']) else: raise RPCError( "Method '{method}' failed with RPC Error of unknown code {code}, " "message: {message}".format(method=method, data=data, result=result, **err)) return result['result'] class RPCError(exceptions.BackendException): pass class Unauthorized(RPCError): pass class MethodNotFound(RPCError): pass _err2exc = { -2: exceptions.WrongAddress, -4: exceptions.GenericTransferError, -5: exceptions.WrongPaymentId, -8: exceptions.TransactionNotFound, -9: exceptions.SignatureCheckFailed, -14: exceptions.AccountIndexOutOfBound, -15: exceptions.AddressIndexOutOfBound, -16: exceptions.TransactionNotPossible, -17: exceptions.NotEnoughMoney, -20: exceptions.AmountIsZero, -29: exceptions.WalletIsWatchOnly, -37: exceptions.NotEnoughUnlockedMoney, -38: exceptions.NoDaemonConnection, -43: exceptions.WalletIsNotDeterministic, # https://github.com/monero-project/monero/pull/4653 -32601: MethodNotFound, }