You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
monero-python/monero/backends/jsonrpc/wallet.py

444 lines
16 KiB

from __future__ import unicode_literals
import binascii
from datetime import datetime
import json
import logging
import operator
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
from .exceptions import RPCError, Unauthorized, MethodNotFound
_log = logging.getLogger(__name__)
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
:param proxy_url: a proxy to use
"""
_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,
proxy_url=None,
):
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.auth = requests.auth.HTTPDigestAuth(user, password)
self.session = requests.Session()
self.timeout = timeout
self.verify_ssl_certs = verify_ssl_certs
self.proxies = {protocol: proxy_url}
_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"])
# Issue #71: incoming payments to self will have excess 'destinations' key. Remove.
for pmt in pmts:
try:
del pmt["destinations"]
except KeyError:
pass
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": binascii.unhexlify(data.get("blob", "")),
"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(
"Method: {method}\nParams:\n{params}".format(
method=method, params=json.dumps(params, indent=2, sort_keys=True)
)
)
rsp = self.session.post(
self.url,
headers=hdr,
data=json.dumps(data),
auth=self.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("Result:\n{result}".format(result=_ppresult))
if "error" in result:
err = result["error"]
if not squelch_error_logging:
_log.error("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"]
_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,
}