From 7200d980d9af8433084ae3eba0d2feaaa6667985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sa=C5=82aban?= Date: Sun, 26 Nov 2017 23:22:48 +0100 Subject: [PATCH] Add draft of Account, Wallet and JSONRPC backend --- monero/__init__.py | 4 ++ monero/account.py | 37 ++++++++++ monero/backends/__init__.py | 0 monero/backends/jsonrpc.py | 131 ++++++++++++++++++++++++++++++++++++ monero/exceptions.py | 11 +++ monero/prio.py | 4 ++ monero/wallet.py | 44 ++++++++++++ 7 files changed, 231 insertions(+) create mode 100644 monero/account.py create mode 100644 monero/backends/__init__.py create mode 100644 monero/backends/jsonrpc.py create mode 100644 monero/exceptions.py create mode 100644 monero/prio.py create mode 100644 monero/wallet.py diff --git a/monero/__init__.py b/monero/__init__.py index 0627fe6..a321028 100644 --- a/monero/__init__.py +++ b/monero/__init__.py @@ -1 +1,5 @@ from .address import Address +from .account import Account +from .wallet import Wallet +from .numbers import from_atomic, to_atomic +from . import prio diff --git a/monero/account.py b/monero/account.py new file mode 100644 index 0000000..0f22785 --- /dev/null +++ b/monero/account.py @@ -0,0 +1,37 @@ +from . import address +from . import prio + + +class Account(object): + index = None + + def __init__(self, backend, index): + self.index = index + self._backend = backend + + def get_balance(self): + return self._backend.get_balance(account=self.index) + + def get_address(self): + """ + Return account's main address. + """ + return self._backend.get_address(account=self.index)[0] + + def get_addresses(self): + return self._backend.get_addresses(account=self.index) + + def get_payments_in(self): + return self._backend.get_payments_in(account=self.index) + + def get_payments_out(self): + return self._backend.get_payments_out(account=self.index) + + def transfer(self, address, amount, priority=prio.NORMAL, mixin=5): + pass + + def transfer_multi(self, destinations, priority=prio.NORMAL, mixin=5): + """ + destinations = [(address, amount), ...] + """ + pass diff --git a/monero/backends/__init__.py b/monero/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/monero/backends/jsonrpc.py b/monero/backends/jsonrpc.py new file mode 100644 index 0000000..1898189 --- /dev/null +++ b/monero/backends/jsonrpc.py @@ -0,0 +1,131 @@ +from datetime import datetime +import operator +import json +import logging +import pprint +import requests + +from .. import exceptions +from ..account import Account +from ..address import Address +from ..numbers import from_atomic, to_atomic + +_log = logging.getLogger(__name__) + + +class JSONRPC(object): + def __init__(self, protocol='http', host='127.0.0.1', port=18082, path='/json_rpc', user='', password=''): + self.url = '{protocol}://{host}:{port}/json_rpc'.format( + protocol=protocol, + host=host, + port=port) + _log.debug("JSONRPC backend URL: {url}".format(url=self.url)) + self.user = user + self.password = password + _log.debug("JSONRPC backend auth: '{user}'/'{stars}'".format( + user=user, stars=('*' * len(password)) if password else '')) + + def get_accounts(self): + accounts = [] + try: + _accounts = self.raw_request('get_accounts') + except MethodNotFound: + # monero <= 0.11 + return [Account(self, 0)] + idx = 0 + for _acc in _accounts['subaddress_accounts']: + assert idx == _acc['account_index'] + accounts.append(Account(self, _acc['account_index'])) + idx += 1 + return accounts + + def get_addresses(self, account=0): + _addresses = self.raw_request('getaddress', {'account_index': account}) + if 'addresses' not in _addresses: + # monero <= 0.11 + return [Address(_addresses['address'])] + addresses = [None] * (max(map(operator.itemgetter('address_index'), _addresses['addresses'])) + 1) + for _addr in _addresses['addresses']: + addresses[_addr['address_index']] = Address(_addr['address']) + return addresses + + def get_balance(self, account=0): + _balance = self.raw_request('getbalance', {'account_index': account}) + return (from_atomic(_balance['balance']), from_atomic(_balance['unlocked_balance'])) + + def get_payments_in(self, account=0): + _payments = self.raw_request('get_transfers', + {'account_index': account, 'in': True, 'out': False, 'pool': False}) + return map(self._pythonify_tx, _payments.get('in', [])) + + def get_payments_out(self, account=0): + _payments = self.raw_request('get_transfers', + {'account_index': account, 'in': False, 'out': True, 'pool': False}) + return map(self._pythonify_tx, _payments.get('out', '')) + + def _pythonify_tx(self, tx): + return { + 'id': tx['txid'], + 'when': datetime.fromtimestamp(tx['timestamp']), + 'amount': from_atomic(tx['amount']), + 'fee': from_atomic(tx['fee']), + 'height': tx['height'], + 'payment_id': tx['payment_id'], + 'note': tx['note'] + } + + def raw_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=pprint.pformat(params))) + auth = requests.auth.HTTPDigestAuth(self.user, self.password) + rsp = requests.post(self.url, headers=hdr, data=json.dumps(data), auth=auth) + 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 = pprint.pformat(result) + _log.debug(u"Result:\n{result}".format(result=_ppresult)) + + if 'error' in result: + err = result['error'] + # TODO: resolve code, raise exception + _log.error(u"JSON RPC error:\n{result}".format(result=_ppresult)) + if err['code'] in _err2exc: + raise _err2exc[err['code']](err['message'], method=method, data=data, result=result) + 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.MoneroException): + def __init__(self, message, method=None, data=None, result=None): + self.method = method + self.data = data + self.result = result + super().__init__(message) + + def __str__(self): + return "'{method}': {error}".format( + method=self.method, + error=super().__str__()) + + +class Unauthorized(RPCError): + pass + + +class MethodNotFound(RPCError): + pass + + +_err2exc = { + -32601: MethodNotFound, +} diff --git a/monero/exceptions.py b/monero/exceptions.py new file mode 100644 index 0000000..50971f8 --- /dev/null +++ b/monero/exceptions.py @@ -0,0 +1,11 @@ +class MoneroException(Exception): + pass + +class BackendException(MoneroException): + pass + +class AccountException(MoneroException): + pass + +class NotEnoughMoney(AccountException): + pass diff --git a/monero/prio.py b/monero/prio.py new file mode 100644 index 0000000..8626d30 --- /dev/null +++ b/monero/prio.py @@ -0,0 +1,4 @@ +UNIMPORTANT=1 +NORMAL=2 +ELEVATED=3 +PRIORITY=4 diff --git a/monero/wallet.py b/monero/wallet.py new file mode 100644 index 0000000..3763c52 --- /dev/null +++ b/monero/wallet.py @@ -0,0 +1,44 @@ +from . import address +from . import prio +from . import account + +class Wallet(object): + accounts = None + + def __init__(self, backend): + self._backend = backend + self.refresh() + + def refresh(self): + self.accounts = self.accounts or [] + idx = 0 + for _acc in self._backend.get_accounts(): + try: + if self.accounts[idx]: + continue + except IndexError: + pass + self.accounts.append(_acc) + idx += 1 + + # Following methods operate on default account (index=0) + def get_balance(self): + return self.accounts[0].get_balance() + + def get_address(self, index=0): + return self.accounts[0].get_addresses()[0] + + def get_payments_in(self): + return self.accounts[0].get_payments_in() + + def get_payments_out(self): + return self.accounts[0].get_payments_out() + + def transfer(self, address, amount, priority=prio.NORMAL, mixin=5): + self.accounts[0].transfer(address, amount, priority=priority, mixin=mixin) + + def transfer_multi(self, destinations, priority=prio.NORMAL, mixin=5): + """ + destinations = [(address, amount), ...] + """ + return self.accounts[0].transfer_multi(destinations, priority=priority, mixin=mixin)