Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/source/wallet.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ addresses:

In [3]: w = Wallet(JSONRPCWallet(port=28088))

In [4]: w.get_address()
In [4]: w.address()
Out[4]: A2GmyHHJ9jtUhPiwoAbR2tXU9LJu2U6fJjcsv3rxgkVRWU6tEYcn6C1NBc7wqCv5V7NW3zeYuzKf6RGGgZTFTpVC4QxAiAX

Accounts and subaddresses
Expand All @@ -38,10 +38,10 @@ client.
In [6]: w.accounts[0]
Out[6]: <monero.account.Account at 0x7f78992d6898>

In [7]: w.accounts[0].get_address()
In [7]: w.accounts[0].address()
Out[7]: A2GmyHHJ9jtUhPiwoAbR2tXU9LJu2U6fJjcsv3rxgkVRWU6tEYcn6C1NBc7wqCv5V7NW3zeYuzKf6RGGgZTFTpVC4QxAiAX

In [8]: w.get_addresses()
In [8]: w.addresses()
Out[8]: [A2GmyHHJ9jtUhPiwoAbR2tXU9LJu2U6fJjcsv3rxgkVRWU6tEYcn6C1NBc7wqCv5V7NW3zeYuzKf6RGGgZTFTpVC4QxAiAX]


Expand All @@ -57,7 +57,7 @@ create new instances.
In [9]: w.new_address()
Out[9]: BenuGf8eyVhjZwdcxEJY1MHrUfqHjPvE3d7Pi4XY5vQz53VnVpB38bCBsf8AS5rJuZhuYrqdG9URc2eFoCNPwLXtLENT4R7

In [10]: w.get_addresses()
In [10]: w.addresses()
Out[10]:
[A2GmyHHJ9jtUhPiwoAbR2tXU9LJu2U6fJjcsv3rxgkVRWU6tEYcn6C1NBc7wqCv5V7NW3zeYuzKf6RGGgZTFTpVC4QxAiAX,
BenuGf8eyVhjZwdcxEJY1MHrUfqHjPvE3d7Pi4XY5vQz53VnVpB38bCBsf8AS5rJuZhuYrqdG9URc2eFoCNPwLXtLENT4R7]
Expand All @@ -68,13 +68,13 @@ create new instances.
In [12]: len(w.accounts)
Out[12]: 2

In [13]: w.accounts[1].get_address()
In [13]: w.accounts[1].address()
Out[13]: Bhd3PRVCnq5T5jjNey2hDSM8DxUgFpNjLUrKAa2iYVhYX71RuCGTekDKZKXoJPAGL763kEXaDSAsvDYb8bV77YT7Jo19GKY

In [14]: w.accounts[1].new_address()
Out[14]: Bbz5uCtnn3Gaj1YAizaHw1FPeJ6T7kk7uQoeY48SWjezEAyrWScozLxYbqGxsV5L6VJkvw5VwECAuLVJKQtHpA3GFXJNPYu

In [15]: w.accounts[1].get_addresses()
In [15]: w.accounts[1].addresses()
Out[15]:
[Bhd3PRVCnq5T5jjNey2hDSM8DxUgFpNjLUrKAa2iYVhYX71RuCGTekDKZKXoJPAGL763kEXaDSAsvDYb8bV77YT7Jo19GKY,
Bbz5uCtnn3Gaj1YAizaHw1FPeJ6T7kk7uQoeY48SWjezEAyrWScozLxYbqGxsV5L6VJkvw5VwECAuLVJKQtHpA3GFXJNPYu]
Expand Down
9 changes: 6 additions & 3 deletions monero/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class BaseAddress(object):
label = None

def __init__(self, addr, label=None):
addr = str(addr)
addr = addr.decode() if isinstance(addr, bytes) else str(addr)
if not _ADDR_REGEX.match(addr):
raise ValueError("Address must be 95 characters long base58-encoded string, "
"is {addr} ({len} chars length)".format(addr=addr, len=len(addr)))
Expand Down Expand Up @@ -71,6 +71,9 @@ def __eq__(self, other):
def __hash__(self):
return hash(str(self))

def __format__(self, spec):
return format(str(self), spec)


class Address(BaseAddress):
"""Monero address.
Expand Down Expand Up @@ -163,7 +166,7 @@ class IntegratedAddress(Address):
# NOTE: _valid_netbytes order is (mainnet, testnet, stagenet)

def __init__(self, address):
address = str(address)
address = address.decode() if isinstance(address, bytes) else str(address)
if not _IADDR_REGEX.match(address):
raise ValueError("Integrated address must be 106 characters long base58-encoded string, "
"is {addr} ({len} chars length)".format(addr=address, len=len(address)))
Expand Down Expand Up @@ -194,7 +197,7 @@ def address(addr, index=None, label=None):

:rtype: :class:`Address`, :class:`SubAddress` or :class:`IntegratedAddress`
"""
addr = str(addr)
addr = addr.decode() if isinstance(addr, bytes) else str(addr)
if _ADDR_REGEX.match(addr):
netbyte = bytearray(unhexlify(base58.decode(addr)))[0]
if netbyte in Address._valid_netbytes:
Expand Down
66 changes: 48 additions & 18 deletions monero/backends/jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ def mempool(self):
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 Exception()

def raw_request(self, path, data):
hdr = {'Content-Type': 'application/json'}
_log.debug(u"Request: {path}\nData: {data}".format(
Expand All @@ -82,7 +91,6 @@ def raw_request(self, path, data):
_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 {}}
Expand Down Expand Up @@ -281,6 +289,8 @@ def get_incoming_transactions(
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
Expand All @@ -294,37 +304,55 @@ def transfers_in(self, account, pmtfilter):
params['out'] = False
params['pool'] = False
if method == 'get_transfers':
arg = 'in'
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
# PR#3235 makes the following obsolete
# CRYPTONOTE_MAX_BLOCK_NUMBER = 500000000
params['max_height'] = params.get('max_height', 500000000)
_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:
arg = 'payments'
# 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(arg, [])
_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):
_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', []))
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):
Expand Down Expand Up @@ -431,7 +459,8 @@ def raw_request(self, method, params=None, squelch_error_logging=False):

if 'error' in result:
err = result['error']
_log.error(u"JSON RPC error:\n{result}".format(result=_ppresult))
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:
Expand All @@ -455,6 +484,7 @@ class MethodNotFound(RPCError):

_err2exc = {
-2: exceptions.WrongAddress,
-4: exceptions.GenericTransferError,
-5: exceptions.WrongPaymentId,
-8: exceptions.TransactionNotFound,
-9: exceptions.SignatureCheckFailed,
Expand Down
10 changes: 10 additions & 0 deletions monero/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,13 @@ def mempool(self):
:rtype: list of :class:`Transaction <monero.transaction.Transaction>`
"""
return self._backend.mempool()


def headers(self, start_height, end_height=None):
"""
Returns block headers for given height range.
If no :param end_height: is given, it's assumed to be equal to :param start_height:

:rtype: list of dict
"""
return self._backend.headers(start_height, end_height)
3 changes: 3 additions & 0 deletions monero/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ class SignatureCheckFailed(MoneroException):

class WalletIsNotDeterministic(MoneroException):
pass

class GenericTransferError(AccountException):
pass
23 changes: 23 additions & 0 deletions monero/transaction.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import sys
import warnings
from decimal import Decimal
Expand Down Expand Up @@ -114,6 +115,13 @@ def __call__(self, **filterparams):
return fetch(self.account_idx, PaymentFilter(**filterparams))


def _validate_tx_id(txid):
if not bool(re.compile('^[0-9a-f]{64}$').match(txid)):
raise ValueError("Transaction ID must be a 64-character hexadecimal string, not "
"'{}'".format(txid))
return txid


class _ByHeight(object):
"""A helper class used as key in sorting of payments by height.
Mempool goes on top, blockchain payments are ordered with descending block numbers.
Expand Down Expand Up @@ -159,6 +167,7 @@ def __init__(self, **filterparams):
self.unconfirmed = filterparams.pop('unconfirmed', False)
self.confirmed = filterparams.pop('confirmed', True)
_local_address = filterparams.pop('local_address', None)
_tx_id = filterparams.pop('tx_id', None)
_payment_id = filterparams.pop('payment_id', None)
if len(filterparams) > 0:
raise ValueError("Excessive arguments for payment query: {}".format(filterparams))
Expand All @@ -180,6 +189,18 @@ def __init__(self, **filterparams):
except TypeError:
local_addresses = [_local_address]
self.local_addresses = list(map(address, local_addresses))
if _tx_id is None:
self.tx_ids = []
else:
if isinstance(_tx_id, _str_types):
tx_ids = [_tx_id]
else:
try:
iter(_tx_id)
tx_ids = _tx_id
except TypeError:
tx_ids = [_tx_id]
self.tx_ids = list(map(_validate_tx_id, tx_ids))
if _payment_id is None:
self.payment_ids = []
else:
Expand Down Expand Up @@ -210,6 +231,8 @@ def check(self, payment):
return False
if self.payment_ids and payment.payment_id not in self.payment_ids:
return False
if self.tx_ids and payment.transaction.hash not in self.tx_ids:
return False
if self.local_addresses and payment.local_address not in self.local_addresses:
return False
return True
Expand Down
3 changes: 2 additions & 1 deletion monero/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ def new_address(self, label=None):
"""
Creates a new address in the default account.

:rtype: :class:`SubAddress <monero.address.SubAddress>`
:rtype: tuple of subaddress, subaddress index (minor):
(:class:`SubAddress <monero.address.SubAddress>`, `int`)
"""
return self.accounts[0].new_address(label=label)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"id": 0,
"jsonrpc": "2.0",
"result": {
"subaddress_accounts": [
{
"account_index": 0,
"balance": 119141601989972,
"base_address": "56cXYWG13YKaT9z1aEy2hb9TZNnxrW3zE9S4nTQVDux5Qq7UYsmjuux3Zstxkorj9HAufyWLU3FwHW4uERQF6tkeUVogGN3",
"label": "Primary account",
"tag": "",
"unlocked_balance": 119141601989972
},
{
"account_index": 1,
"balance": 1000000000000,
"base_address": "79kTZg96pMf2Dt9rLEWnLzTUB8XC1wMhxaJyxa79hJu6bK9CfFnfbSL1GJNZbqhv9xPqJhRj2Yfb7QUWa2zeEw56H4KiUfN",
"label": "Untitled account",
"tag": "",
"unlocked_balance": 1000000000000
}
],
"total_balance": 120141601989972,
"total_unlocked_balance": 120141601989972
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"id": 0,
"jsonrpc": "2.0",
"result": {
"transfer": {
"address": "7AEBRUmNcjhUjiqdVpeKKYiAVZ216AYdhBFx8UUfjPhWdKujoosnsUtHCohLcYWUXFdNiqnBsMmCFCyDkSmat3Ys4H4yHUp",
"amount": 4000000000000,
"confirmations": 1,
"double_spend_seen": false,
"fee": 195890000,
"height": 409450,
"note": "",
"payment_id": "0000000000000000",
"subaddr_index": {
"major": 0,
"minor": 232
},
"subaddr_indices": [
{
"major": 0,
"minor": 232
}
],
"suggested_confirmations_threshold": 1,
"timestamp": 1568408341,
"txid": "55e758d7d259bb316551ddcdd4808711de99c30b8b5c52de3e95e563fd92d156",
"type": "in",
"unlock_time": 0
},
"transfers": [
{
"address": "7AEBRUmNcjhUjiqdVpeKKYiAVZ216AYdhBFx8UUfjPhWdKujoosnsUtHCohLcYWUXFdNiqnBsMmCFCyDkSmat3Ys4H4yHUp",
"amount": 4000000000000,
"confirmations": 1,
"double_spend_seen": false,
"fee": 195890000,
"height": 409450,
"note": "",
"payment_id": "0000000000000000",
"subaddr_index": {
"major": 0,
"minor": 232
},
"subaddr_indices": [
{
"major": 0,
"minor": 232
}
],
"suggested_confirmations_threshold": 1,
"timestamp": 1568408341,
"txid": "55e758d7d259bb316551ddcdd4808711de99c30b8b5c52de3e95e563fd92d156",
"type": "in",
"unlock_time": 0
}
]
}
}
Loading