|
|
|
@ -1,214 +1,21 @@
|
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
import binascii
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
import operator
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import operator
|
|
|
|
|
import requests
|
|
|
|
|
import six
|
|
|
|
|
|
|
|
|
|
from .. import exceptions
|
|
|
|
|
from ..account import Account
|
|
|
|
|
from ..address import address, Address, SubAddress
|
|
|
|
|
from ..block import Block
|
|
|
|
|
from ..const import NET_MAIN, NET_TEST, NET_STAGE
|
|
|
|
|
from ..numbers import from_atomic, to_atomic, PaymentID
|
|
|
|
|
from ..seed import Seed
|
|
|
|
|
from ..transaction import Transaction, IncomingPayment, OutgoingPayment
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
from .exceptions import RPCError, Unauthorized, MethodNotFound
|
|
|
|
|
|
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RESTRICTED_MAX_TRANSACTIONS = 100
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_net = None
|
|
|
|
|
|
|
|
|
|
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 _set_net(self, info):
|
|
|
|
|
if info['mainnet']:
|
|
|
|
|
self._net = NET_MAIN
|
|
|
|
|
if info['testnet']:
|
|
|
|
|
self._net = NET_TEST
|
|
|
|
|
if info['stagenet']:
|
|
|
|
|
self._net = NET_STAGE
|
|
|
|
|
|
|
|
|
|
def info(self):
|
|
|
|
|
info = self.raw_jsonrpc_request('get_info')
|
|
|
|
|
self._set_net(info)
|
|
|
|
|
return info
|
|
|
|
|
|
|
|
|
|
def net(self):
|
|
|
|
|
if self._net:
|
|
|
|
|
return self._net
|
|
|
|
|
self.info()
|
|
|
|
|
return self._net
|
|
|
|
|
|
|
|
|
|
def send_transaction(self, blob, relay=True):
|
|
|
|
|
res = self.raw_request('/sendrawtransaction', {
|
|
|
|
|
'tx_as_hex': six.ensure_text(binascii.hexlify(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']),
|
|
|
|
|
blob=binascii.unhexlify(tx['tx_blob']),
|
|
|
|
|
json=json.loads(tx['tx_json']),
|
|
|
|
|
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 block(self, bhash=None, height=None):
|
|
|
|
|
data = {}
|
|
|
|
|
if bhash:
|
|
|
|
|
data['hash'] = bhash
|
|
|
|
|
if height:
|
|
|
|
|
data['height'] = height
|
|
|
|
|
res = self.raw_jsonrpc_request('get_block', data)
|
|
|
|
|
if res['status'] == 'OK':
|
|
|
|
|
bhdr = res['block_header']
|
|
|
|
|
sub_json = json.loads(res['json'])
|
|
|
|
|
data = {
|
|
|
|
|
'blob': res['blob'],
|
|
|
|
|
'hash': bhdr['hash'],
|
|
|
|
|
'height': bhdr['height'],
|
|
|
|
|
'timestamp': datetime.fromtimestamp(bhdr['timestamp']),
|
|
|
|
|
'version': (bhdr['major_version'], bhdr['minor_version']),
|
|
|
|
|
'difficulty': bhdr['difficulty'],
|
|
|
|
|
'nonce': bhdr['nonce'],
|
|
|
|
|
'orphan': bhdr['orphan_status'],
|
|
|
|
|
'prev_hash': bhdr['prev_hash'],
|
|
|
|
|
'reward': from_atomic(bhdr['reward']),
|
|
|
|
|
'transactions': self.transactions(
|
|
|
|
|
[bhdr['miner_tx_hash']] + sub_json['tx_hashes']),
|
|
|
|
|
}
|
|
|
|
|
return Block(**data)
|
|
|
|
|
raise exceptions.BackendException(res['status'])
|
|
|
|
|
|
|
|
|
|
def transactions(self, hashes):
|
|
|
|
|
"""
|
|
|
|
|
Returns a list of transactions for given hashes. Automatically chunks the request
|
|
|
|
|
into amounts acceptable by a restricted RPC server.
|
|
|
|
|
"""
|
|
|
|
|
hashes = list(hashes)
|
|
|
|
|
result = []
|
|
|
|
|
while len(hashes):
|
|
|
|
|
result.extend(self._do_get_transactions(hashes[:RESTRICTED_MAX_TRANSACTIONS]))
|
|
|
|
|
hashes = hashes[RESTRICTED_MAX_TRANSACTIONS:]
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
def _do_get_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', []):
|
|
|
|
|
as_json = json.loads(tx['as_json'])
|
|
|
|
|
fee = as_json.get('rct_signatures', {}).get('txnFee')
|
|
|
|
|
txs.append(Transaction(
|
|
|
|
|
hash=tx['tx_hash'],
|
|
|
|
|
fee=from_atomic(fee) if fee else None,
|
|
|
|
|
height=None if tx['in_pool'] else tx['block_height'],
|
|
|
|
|
timestamp=datetime.fromtimestamp(
|
|
|
|
|
tx['block_timestamp']) if 'block_timestamp' in tx else None,
|
|
|
|
|
blob=binascii.unhexlify(tx['as_hex']),
|
|
|
|
|
json=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``)
|
|
|
|
@ -511,19 +318,6 @@ class JSONRPCWallet(object):
|
|
|
|
|
"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,
|