2019-09-13 21:12:25 +00:00
|
|
|
import re
|
2018-01-29 14:11:53 +00:00
|
|
|
import sys
|
2018-11-30 00:38:38 +00:00
|
|
|
import warnings
|
2018-01-29 14:11:53 +00:00
|
|
|
from .address import address
|
|
|
|
from .numbers import PaymentID
|
|
|
|
|
2018-01-25 07:50:09 +00:00
|
|
|
class Payment(object):
|
2018-02-16 12:46:14 +00:00
|
|
|
"""
|
|
|
|
A payment base class, representing payment not associated with any
|
|
|
|
:class:`Account <monero.account.Account>`.
|
|
|
|
|
|
|
|
This class is not intended to be turned into objects by the user,
|
|
|
|
it is used by backends.
|
|
|
|
"""
|
2018-01-25 07:50:09 +00:00
|
|
|
payment_id = None
|
|
|
|
amount = None
|
2017-12-27 00:49:59 +00:00
|
|
|
timestamp = None
|
2018-01-25 07:50:09 +00:00
|
|
|
transaction = None
|
2018-01-30 08:43:08 +00:00
|
|
|
local_address = None
|
|
|
|
note = ''
|
2017-12-27 00:49:59 +00:00
|
|
|
|
2018-02-05 17:15:29 +00:00
|
|
|
_reprstr = "{} @ {} {:.12f} id={}"
|
|
|
|
|
2018-01-25 07:50:09 +00:00
|
|
|
def __init__(self, **kwargs):
|
2018-01-30 08:43:08 +00:00
|
|
|
self.amount = kwargs.pop('amount', self.amount)
|
|
|
|
self.timestamp = kwargs.pop('timestamp', self.timestamp)
|
|
|
|
self.payment_id = kwargs.pop('payment_id', self.payment_id)
|
|
|
|
self.transaction = kwargs.pop('transaction', self.transaction)
|
|
|
|
self.local_address = kwargs.pop('local_address', self.local_address)
|
|
|
|
self.note = kwargs.pop('note', self.note)
|
|
|
|
if len(kwargs):
|
|
|
|
raise ValueError("Excessive arguments for {}: {}".format(type(self), kwargs))
|
2017-12-27 00:49:59 +00:00
|
|
|
|
2018-01-29 14:11:53 +00:00
|
|
|
def __repr__(self):
|
2018-02-05 17:15:29 +00:00
|
|
|
return self._reprstr.format(
|
|
|
|
self.transaction.hash, self.transaction.height or 'pool', self.amount, self.payment_id)
|
2017-12-27 00:49:59 +00:00
|
|
|
|
2018-01-22 02:55:08 +00:00
|
|
|
|
2018-01-29 14:11:53 +00:00
|
|
|
class IncomingPayment(Payment):
|
2018-02-16 12:46:14 +00:00
|
|
|
"""
|
|
|
|
An incoming payment (one that increases the balance of an
|
|
|
|
:class:`Account <monero.account.Account>`)
|
|
|
|
"""
|
2018-02-05 17:15:29 +00:00
|
|
|
_reprstr = "in: {} @ {} {:.12f} id={}"
|
2018-01-22 02:55:08 +00:00
|
|
|
|
|
|
|
|
2018-01-25 07:50:09 +00:00
|
|
|
class OutgoingPayment(Payment):
|
2018-02-16 12:46:14 +00:00
|
|
|
"""
|
|
|
|
An outgoing payment (one that decreases the balance of an
|
|
|
|
:class:`Account <monero.account.Account>`)
|
|
|
|
"""
|
2018-10-18 03:44:54 +00:00
|
|
|
destinations = None
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
2018-10-18 21:47:22 +00:00
|
|
|
self.destinations = kwargs.pop('destinations', [])
|
2018-10-18 04:47:01 +00:00
|
|
|
super(OutgoingPayment, self).__init__(**kwargs)
|
2018-10-18 03:44:54 +00:00
|
|
|
|
2018-02-05 17:15:29 +00:00
|
|
|
_reprstr = "out: {} @ {} {:.12f} id={}"
|
2017-12-27 00:49:59 +00:00
|
|
|
|
|
|
|
|
2018-01-25 07:50:09 +00:00
|
|
|
class Transaction(object):
|
2018-02-16 12:46:14 +00:00
|
|
|
"""
|
|
|
|
A Monero transaction. Identified by `hash`, it can be a part of a block of some `height`
|
|
|
|
or not yet mined (`height` is `None` then).
|
|
|
|
|
|
|
|
This class is not intended to be turned into objects by the user,
|
|
|
|
it is used by backends.
|
|
|
|
"""
|
2018-01-25 07:50:09 +00:00
|
|
|
hash = None
|
|
|
|
fee = None
|
|
|
|
height = None
|
|
|
|
timestamp = None
|
2017-12-27 00:49:59 +00:00
|
|
|
key = None
|
2018-01-25 07:50:09 +00:00
|
|
|
blob = None
|
2019-12-11 10:25:43 +00:00
|
|
|
json = None
|
2018-10-18 06:15:20 +00:00
|
|
|
confirmations = None
|
2017-12-27 00:49:59 +00:00
|
|
|
|
2019-12-11 10:25:43 +00:00
|
|
|
@property
|
|
|
|
def size(self):
|
|
|
|
return len(self.blob)//2
|
|
|
|
|
2017-12-27 00:49:59 +00:00
|
|
|
def __init__(self, **kwargs):
|
2018-01-25 07:50:09 +00:00
|
|
|
self.hash = kwargs.get('hash', self.hash)
|
|
|
|
self.fee = kwargs.get('fee', self.fee)
|
|
|
|
self.height = kwargs.get('height', self.height)
|
|
|
|
self.timestamp = kwargs.get('timestamp', self.timestamp)
|
2017-12-27 00:49:59 +00:00
|
|
|
self.key = kwargs.get('key', self.key)
|
2018-01-25 07:50:09 +00:00
|
|
|
self.blob = kwargs.get('blob', self.blob)
|
2019-12-11 10:25:43 +00:00
|
|
|
self.json = kwargs.get('json', self.json)
|
2018-10-18 06:15:20 +00:00
|
|
|
self.confirmations = kwargs.get('confirmations', self.confirmations)
|
2018-01-29 14:11:53 +00:00
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return self.hash
|
|
|
|
|
|
|
|
|
2019-01-03 18:03:33 +00:00
|
|
|
if sys.version_info < (3,): # pragma: no cover
|
2018-01-29 14:11:53 +00:00
|
|
|
_str_types = (str, bytes, unicode)
|
2019-01-03 18:03:33 +00:00
|
|
|
else: # pragma: no cover
|
2018-01-29 14:11:53 +00:00
|
|
|
_str_types = (str, bytes)
|
|
|
|
|
|
|
|
|
|
|
|
class PaymentManager(object):
|
2018-02-16 12:46:14 +00:00
|
|
|
"""
|
|
|
|
A payment query manager, handling either incoming or outgoing payments of
|
|
|
|
an :class:`Account <monero.account.Account>`.
|
|
|
|
|
|
|
|
This class is not intended to be turned into objects by the user,
|
|
|
|
it is used by backends.
|
|
|
|
"""
|
2018-01-29 14:11:53 +00:00
|
|
|
account_idx = 0
|
|
|
|
backend = None
|
|
|
|
|
|
|
|
def __init__(self, account_idx, backend, direction):
|
|
|
|
self.account_idx = account_idx
|
|
|
|
self.backend = backend
|
|
|
|
self.direction = direction
|
|
|
|
|
|
|
|
def __call__(self, **filterparams):
|
|
|
|
fetch = self.backend.transfers_in if self.direction == 'in' else self.backend.transfers_out
|
|
|
|
return fetch(self.account_idx, PaymentFilter(**filterparams))
|
|
|
|
|
|
|
|
|
2019-09-13 21:12:25 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2018-10-18 23:26:15 +00:00
|
|
|
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.
|
|
|
|
|
|
|
|
**WARNING:** Integer sorting is reversed here.
|
|
|
|
"""
|
|
|
|
def __init__(self, pmt):
|
|
|
|
self.pmt = pmt
|
|
|
|
def _cmp(self, other):
|
|
|
|
sh = self.pmt.transaction.height
|
|
|
|
oh = other.pmt.transaction.height
|
|
|
|
if sh is oh is None:
|
|
|
|
return 0
|
|
|
|
if sh is None:
|
|
|
|
return 1
|
|
|
|
if oh is None:
|
|
|
|
return -1
|
|
|
|
return (sh > oh) - (sh < oh)
|
|
|
|
def __lt__(self, other):
|
|
|
|
return self._cmp(other) > 0
|
|
|
|
def __le__(self, other):
|
|
|
|
return self._cmp(other) >= 0
|
|
|
|
def __eq__(self, other):
|
|
|
|
return self._cmp(other) == 0
|
|
|
|
def __ge__(self, other):
|
|
|
|
return self._cmp(other) <= 0
|
|
|
|
def __gt__(self, other):
|
|
|
|
return self._cmp(other) < 0
|
|
|
|
def __ne__(self, other):
|
|
|
|
return self._cmp(other) != 0
|
|
|
|
|
|
|
|
|
2018-01-29 14:11:53 +00:00
|
|
|
class PaymentFilter(object):
|
2018-02-16 12:46:14 +00:00
|
|
|
"""
|
|
|
|
A helper class that filters payments retrieved by the backend.
|
|
|
|
|
|
|
|
This class is not intended to be turned into objects by the user,
|
|
|
|
it is used by backends.
|
|
|
|
"""
|
2018-01-29 14:11:53 +00:00
|
|
|
def __init__(self, **filterparams):
|
|
|
|
self.min_height = filterparams.pop('min_height', None)
|
|
|
|
self.max_height = filterparams.pop('max_height', None)
|
|
|
|
self.unconfirmed = filterparams.pop('unconfirmed', False)
|
|
|
|
self.confirmed = filterparams.pop('confirmed', True)
|
2018-01-30 08:43:08 +00:00
|
|
|
_local_address = filterparams.pop('local_address', None)
|
2019-09-13 21:12:25 +00:00
|
|
|
_tx_id = filterparams.pop('tx_id', None)
|
2018-01-29 14:11:53 +00:00
|
|
|
_payment_id = filterparams.pop('payment_id', None)
|
|
|
|
if len(filterparams) > 0:
|
2018-01-30 08:43:08 +00:00
|
|
|
raise ValueError("Excessive arguments for payment query: {}".format(filterparams))
|
2018-11-30 00:38:38 +00:00
|
|
|
if self.unconfirmed and (self.min_height is not None or self.max_height is not None):
|
|
|
|
warnings.warn("Height filtering (min_height/max_height) has been requested while "
|
|
|
|
"also asking for unconfirmed transactions. These are mutually exclusive. "
|
|
|
|
"As mempool transactions have no height at all, they will be excluded "
|
|
|
|
"from the result.",
|
|
|
|
RuntimeWarning)
|
2018-01-30 08:43:08 +00:00
|
|
|
if _local_address is None:
|
|
|
|
self.local_addresses = []
|
2018-01-29 14:11:53 +00:00
|
|
|
else:
|
2018-01-30 08:43:08 +00:00
|
|
|
if isinstance(_local_address, _str_types):
|
|
|
|
local_addresses = [_local_address]
|
2018-01-29 14:11:53 +00:00
|
|
|
else:
|
|
|
|
try:
|
2018-01-30 08:43:08 +00:00
|
|
|
iter(_local_address)
|
|
|
|
local_addresses = _local_address
|
2018-01-29 14:11:53 +00:00
|
|
|
except TypeError:
|
2018-01-30 08:43:08 +00:00
|
|
|
local_addresses = [_local_address]
|
|
|
|
self.local_addresses = list(map(address, local_addresses))
|
2019-09-13 21:12:25 +00:00
|
|
|
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))
|
2018-01-29 14:11:53 +00:00
|
|
|
if _payment_id is None:
|
|
|
|
self.payment_ids = []
|
|
|
|
else:
|
|
|
|
if isinstance(_payment_id, _str_types):
|
|
|
|
payment_ids = [_payment_id]
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
iter(_payment_id)
|
|
|
|
payment_ids = _payment_id
|
|
|
|
except TypeError:
|
|
|
|
payment_ids = [_payment_id]
|
|
|
|
self.payment_ids = list(map(PaymentID, payment_ids))
|
|
|
|
|
|
|
|
def check(self, payment):
|
|
|
|
ht = payment.transaction.height
|
|
|
|
if ht is None:
|
|
|
|
if not self.unconfirmed:
|
|
|
|
return False
|
|
|
|
if self.min_height is not None or self.max_height is not None:
|
|
|
|
# mempool txns are filtered out if any height range check is present
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
if not self.confirmed:
|
|
|
|
return False
|
|
|
|
if self.min_height is not None and ht < self.min_height:
|
|
|
|
return False
|
|
|
|
if self.max_height is not None and ht > self.max_height:
|
|
|
|
return False
|
|
|
|
if self.payment_ids and payment.payment_id not in self.payment_ids:
|
|
|
|
return False
|
2019-09-13 21:12:25 +00:00
|
|
|
if self.tx_ids and payment.transaction.hash not in self.tx_ids:
|
|
|
|
return False
|
2018-01-30 08:43:08 +00:00
|
|
|
if self.local_addresses and payment.local_address not in self.local_addresses:
|
2018-01-29 14:11:53 +00:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
def filter(self, payments):
|
2018-10-18 23:26:15 +00:00
|
|
|
return sorted(
|
|
|
|
filter(self.check, payments),
|
|
|
|
key=_ByHeight)
|