diff --git a/docs/source/wallet.rst b/docs/source/wallet.rst index 572f99b..926cefd 100644 --- a/docs/source/wallet.rst +++ b/docs/source/wallet.rst @@ -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 @@ -38,10 +38,10 @@ client. In [6]: w.accounts[0] Out[6]: - 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] @@ -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] @@ -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] diff --git a/monero/address.py b/monero/address.py index 827a96c..9d92353 100644 --- a/monero/address.py +++ b/monero/address.py @@ -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))) @@ -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. @@ -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))) @@ -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: diff --git a/monero/backends/jsonrpc.py b/monero/backends/jsonrpc.py index f94966e..b33ca22 100644 --- a/monero/backends/jsonrpc.py +++ b/monero/backends/jsonrpc.py @@ -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( @@ -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 {}} @@ -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 @@ -294,7 +304,6 @@ 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 @@ -302,29 +311,48 @@ def transfers_in(self, account, pmtfilter): 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): @@ -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: @@ -455,6 +484,7 @@ class MethodNotFound(RPCError): _err2exc = { -2: exceptions.WrongAddress, + -4: exceptions.GenericTransferError, -5: exceptions.WrongPaymentId, -8: exceptions.TransactionNotFound, -9: exceptions.SignatureCheckFailed, diff --git a/monero/daemon.py b/monero/daemon.py index 0cb2486..52ede66 100644 --- a/monero/daemon.py +++ b/monero/daemon.py @@ -41,3 +41,13 @@ def mempool(self): :rtype: list of :class:`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) diff --git a/monero/exceptions.py b/monero/exceptions.py index 389fbb5..2b1bc4e 100644 --- a/monero/exceptions.py +++ b/monero/exceptions.py @@ -41,3 +41,6 @@ class SignatureCheckFailed(MoneroException): class WalletIsNotDeterministic(MoneroException): pass + +class GenericTransferError(AccountException): + pass diff --git a/monero/transaction.py b/monero/transaction.py index cc1f644..c1e5c2e 100644 --- a/monero/transaction.py +++ b/monero/transaction.py @@ -1,3 +1,4 @@ +import re import sys import warnings from decimal import Decimal @@ -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. @@ -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)) @@ -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: @@ -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 diff --git a/monero/wallet.py b/monero/wallet.py index b36c27c..77c8447 100644 --- a/monero/wallet.py +++ b/monero/wallet.py @@ -229,7 +229,8 @@ def new_address(self, label=None): """ Creates a new address in the default account. - :rtype: :class:`SubAddress ` + :rtype: tuple of subaddress, subaddress index (minor): + (:class:`SubAddress `, `int`) """ return self.accounts[0].new_address(label=label) diff --git a/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-00-get_accounts.json b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-00-get_accounts.json new file mode 100644 index 0000000..7ec3821 --- /dev/null +++ b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-00-get_accounts.json @@ -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 + } +} diff --git a/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-30-get_transfer_by_txid.json b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-30-get_transfer_by_txid.json new file mode 100644 index 0000000..d516656 --- /dev/null +++ b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-30-get_transfer_by_txid.json @@ -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 + } + ] + } +} diff --git a/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-31b52-get_transfer_by_txid.json b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-31b52-get_transfer_by_txid.json new file mode 100644 index 0000000..dbbaa6b --- /dev/null +++ b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-31b52-get_transfer_by_txid.json @@ -0,0 +1,58 @@ +{ + "id": 0, + "jsonrpc": "2.0", + "result": { + "transfer": { + "address": "7AwMU2kQkqseHgdVWPaD6J8QvUbomAR3ThBkyaBH3dFTTwT2CcQaZyrSetwq2TXtweHFpprTN1SmEKM2wG64oFdZQ5mqkLe", + "amount": 6000000000000, + "confirmations": 0, + "double_spend_seen": false, + "fee": 195990000, + "height": 0, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 0, + "minor": 233 + }, + "subaddr_indices": [ + { + "major": 0, + "minor": 233 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568409539, + "txid": "31b527fb9c27e759d56892fef93136df1057186c5cf4e3c93c5298b70160f562", + "type": "pool", + "unlock_time": 0 + }, + "transfers": [ + { + "address": "7AwMU2kQkqseHgdVWPaD6J8QvUbomAR3ThBkyaBH3dFTTwT2CcQaZyrSetwq2TXtweHFpprTN1SmEKM2wG64oFdZQ5mqkLe", + "amount": 6000000000000, + "confirmations": 0, + "double_spend_seen": false, + "fee": 195990000, + "height": 0, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 0, + "minor": 233 + }, + "subaddr_indices": [ + { + "major": 0, + "minor": 233 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568409539, + "txid": "31b527fb9c27e759d56892fef93136df1057186c5cf4e3c93c5298b70160f562", + "type": "pool", + "unlock_time": 0 + } + ] + } +} diff --git a/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-55e75-get_transfer_by_txid.json b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-55e75-get_transfer_by_txid.json new file mode 100644 index 0000000..d516656 --- /dev/null +++ b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-55e75-get_transfer_by_txid.json @@ -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 + } + ] + } +} diff --git a/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-7ab84-get_transfer_by_txid.json b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-7ab84-get_transfer_by_txid.json new file mode 100644 index 0000000..f3c2edb --- /dev/null +++ b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-7ab84-get_transfer_by_txid.json @@ -0,0 +1,108 @@ +{ + "id": 0, + "jsonrpc": "2.0", + "result": { + "transfer": { + "address": "76SJ4sPWzgQKE3fBbAoRTC7HtewGAo37VEgMpmHPEfPMRssYQgdeVyfJt5rcEcHw9dJJ4cLSkZ9c5fPTnKFHKh43UKmJs25", + "amount": 1000000000000, + "confirmations": 208, + "double_spend_seen": false, + "fee": 292510000, + "height": 409227, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 0, + "minor": 8 + }, + "subaddr_indices": [ + { + "major": 0, + "minor": 8 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568388430, + "txid": "7ab84fe2fb34467c590cde2f7d6ba7de5928a2db6c84c6ccfff8962eca0ad99c", + "type": "in", + "unlock_time": 0 + }, + "transfers": [ + { + "address": "76SJ4sPWzgQKE3fBbAoRTC7HtewGAo37VEgMpmHPEfPMRssYQgdeVyfJt5rcEcHw9dJJ4cLSkZ9c5fPTnKFHKh43UKmJs25", + "amount": 1000000000000, + "confirmations": 208, + "double_spend_seen": false, + "fee": 292510000, + "height": 409227, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 0, + "minor": 8 + }, + "subaddr_indices": [ + { + "major": 0, + "minor": 8 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568388430, + "txid": "7ab84fe2fb34467c590cde2f7d6ba7de5928a2db6c84c6ccfff8962eca0ad99c", + "type": "in", + "unlock_time": 0 + }, + { + "address": "75LwnK3zfQS5mEzxgEdyep8SSwnvGSKcMLnCpzUqCF4CMFHDNrSnCojfoTRV9EWy2Y3ejeFLMPH9tAjagMAim8F8EKWjHos", + "amount": 1000000000000, + "confirmations": 208, + "double_spend_seen": false, + "fee": 292510000, + "height": 409227, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 0, + "minor": 19 + }, + "subaddr_indices": [ + { + "major": 0, + "minor": 19 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568388430, + "txid": "7ab84fe2fb34467c590cde2f7d6ba7de5928a2db6c84c6ccfff8962eca0ad99c", + "type": "in", + "unlock_time": 0 + }, + { + "address": "74sZRQ2sHs4YLh8PnW8fseUoUoM4bXQ3wQ6bfCr6YyxmK5QRawKLytF9CfRbuv581LEnXBj27Dwg6eNC4fhhrH9kUvpbWQ5", + "amount": 2000000000000, + "confirmations": 208, + "double_spend_seen": false, + "fee": 292510000, + "height": 409227, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 0, + "minor": 29 + }, + "subaddr_indices": [ + { + "major": 0, + "minor": 29 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568388430, + "txid": "7ab84fe2fb34467c590cde2f7d6ba7de5928a2db6c84c6ccfff8962eca0ad99c", + "type": "in", + "unlock_time": 0 + } + ] + } +} diff --git a/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-e0b15-get_transfer_by_txid.json b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-e0b15-get_transfer_by_txid.json new file mode 100644 index 0000000..5ed0357 --- /dev/null +++ b/tests/data/test_jsonrpcwallet/test_incoming_by_tx_id-e0b15-get_transfer_by_txid.json @@ -0,0 +1,8 @@ +{ + "error": { + "code": -8, + "message": "Transaction not found." + }, + "id": 0, + "jsonrpc": "2.0" +} diff --git a/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-00-get_accounts.json b/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-00-get_accounts.json new file mode 100644 index 0000000..533ff8b --- /dev/null +++ b/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-00-get_accounts.json @@ -0,0 +1,26 @@ +{ + "id": 0, + "jsonrpc": "2.0", + "result": { + "subaddress_accounts": [ + { + "account_index": 0, + "balance": 130141601989972, + "base_address": "56cXYWG13YKaT9z1aEy2hb9TZNnxrW3zE9S4nTQVDux5Qq7UYsmjuux3Zstxkorj9HAufyWLU3FwHW4uERQF6tkeUVogGN3", + "label": "Primary account", + "tag": "", + "unlocked_balance": 123141601989972 + }, + { + "account_index": 1, + "balance": 458323130000, + "base_address": "79kTZg96pMf2Dt9rLEWnLzTUB8XC1wMhxaJyxa79hJu6bK9CfFnfbSL1GJNZbqhv9xPqJhRj2Yfb7QUWa2zeEw56H4KiUfN", + "label": "Untitled account", + "tag": "", + "unlocked_balance": 458323130000 + } + ], + "total_balance": 130599925119972, + "total_unlocked_balance": 123599925119972 + } +} diff --git a/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-362c3-get_transfer_by_txid.json b/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-362c3-get_transfer_by_txid.json new file mode 100644 index 0000000..5a6a23f --- /dev/null +++ b/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-362c3-get_transfer_by_txid.json @@ -0,0 +1,78 @@ +{ + "id": 0, + "jsonrpc": "2.0", + "result": { + "transfer": { + "address": "79kTZg96pMf2Dt9rLEWnLzTUB8XC1wMhxaJyxa79hJu6bK9CfFnfbSL1GJNZbqhv9xPqJhRj2Yfb7QUWa2zeEw56H4KiUfN", + "amount": 520000000000, + "confirmations": 23, + "destinations": [ + { + "address": "77yjHxBeLNq4mf4dhEB7ksD6WDaAEEguqHHvuFYyiLiMWwSrvYHftzT5c1HRS9iWa2UBn4MQLuz8jEiE6sPDfMzB81UMHaK", + "amount": 220000000000 + }, + { + "address": "787CXWuevtt2SdD9x6rB7hCk73pYVv7HYgAUAPsYQJ9zAmQN7Ksxr5KieQFXuEL6ZSMqMDNbbaUze365iF2DbkjB9bcL82t", + "amount": 300000000000 + } + ], + "double_spend_seen": false, + "fee": 280650000, + "height": 409449, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 1, + "minor": 0 + }, + "subaddr_indices": [ + { + "major": 1, + "minor": 1 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568408151, + "txid": "362c3a4e601d5847b3882c3debfd28a0ee31654e433c38498539677199c304c2", + "type": "out", + "unlock_time": 0 + }, + "transfers": [ + { + "address": "79kTZg96pMf2Dt9rLEWnLzTUB8XC1wMhxaJyxa79hJu6bK9CfFnfbSL1GJNZbqhv9xPqJhRj2Yfb7QUWa2zeEw56H4KiUfN", + "amount": 520000000000, + "confirmations": 23, + "destinations": [ + { + "address": "77yjHxBeLNq4mf4dhEB7ksD6WDaAEEguqHHvuFYyiLiMWwSrvYHftzT5c1HRS9iWa2UBn4MQLuz8jEiE6sPDfMzB81UMHaK", + "amount": 220000000000 + }, + { + "address": "787CXWuevtt2SdD9x6rB7hCk73pYVv7HYgAUAPsYQJ9zAmQN7Ksxr5KieQFXuEL6ZSMqMDNbbaUze365iF2DbkjB9bcL82t", + "amount": 300000000000 + } + ], + "double_spend_seen": false, + "fee": 280650000, + "height": 409449, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 1, + "minor": 0 + }, + "subaddr_indices": [ + { + "major": 1, + "minor": 1 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568408151, + "txid": "362c3a4e601d5847b3882c3debfd28a0ee31654e433c38498539677199c304c2", + "type": "out", + "unlock_time": 0 + } + ] + } +} diff --git a/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-afaf0-get_transfer_by_txid.json b/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-afaf0-get_transfer_by_txid.json new file mode 100644 index 0000000..b6ea637 --- /dev/null +++ b/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-afaf0-get_transfer_by_txid.json @@ -0,0 +1,70 @@ +{ + "id": 0, + "jsonrpc": "2.0", + "result": { + "transfer": { + "address": "79kTZg96pMf2Dt9rLEWnLzTUB8XC1wMhxaJyxa79hJu6bK9CfFnfbSL1GJNZbqhv9xPqJhRj2Yfb7QUWa2zeEw56H4KiUfN", + "amount": 40000000000, + "confirmations": 0, + "destinations": [ + { + "address": "72nfCSqpigFWaMeKyZYjKMQRvYFxWvJaf8Nnb1KxVndeTuL7avqoCF5NME4WGMqwmK58i8BnKxCz543mFoZhuUMpGhN1dcm", + "amount": 40000000000 + } + ], + "double_spend_seen": false, + "fee": 979320000, + "height": 0, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 1, + "minor": 0 + }, + "subaddr_indices": [ + { + "major": 1, + "minor": 0 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568413596, + "txid": "afaf04e5e40c6b60fc7cc928a88843fc96031ec2b567c310ee61abf3d00020da", + "type": "pending", + "unlock_time": 0 + }, + "transfers": [ + { + "address": "79kTZg96pMf2Dt9rLEWnLzTUB8XC1wMhxaJyxa79hJu6bK9CfFnfbSL1GJNZbqhv9xPqJhRj2Yfb7QUWa2zeEw56H4KiUfN", + "amount": 40000000000, + "confirmations": 0, + "destinations": [ + { + "address": "72nfCSqpigFWaMeKyZYjKMQRvYFxWvJaf8Nnb1KxVndeTuL7avqoCF5NME4WGMqwmK58i8BnKxCz543mFoZhuUMpGhN1dcm", + "amount": 40000000000 + } + ], + "double_spend_seen": false, + "fee": 979320000, + "height": 0, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 1, + "minor": 0 + }, + "subaddr_indices": [ + { + "major": 1, + "minor": 0 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568413596, + "txid": "afaf04e5e40c6b60fc7cc928a88843fc96031ec2b567c310ee61abf3d00020da", + "type": "pending", + "unlock_time": 0 + } + ] + } +} diff --git a/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-eda89-get_transfer_by_txid.json b/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-eda89-get_transfer_by_txid.json new file mode 100644 index 0000000..1e3e9cf --- /dev/null +++ b/tests/data/test_jsonrpcwallet/test_outgoing_by_tx_id-eda89-get_transfer_by_txid.json @@ -0,0 +1,70 @@ +{ + "id": 0, + "jsonrpc": "2.0", + "result": { + "transfer": { + "address": "79kTZg96pMf2Dt9rLEWnLzTUB8XC1wMhxaJyxa79hJu6bK9CfFnfbSL1GJNZbqhv9xPqJhRj2Yfb7QUWa2zeEw56H4KiUfN", + "amount": 21200000000, + "confirmations": 23, + "destinations": [ + { + "address": "72quvHYJ8QzivQyV4NMZ9h1gyZXyJZvsWZTwgVFRCw9b1eJ4yibHEnU3CVCCXJ7evqXhSEKJL2rjzCMV3LpXirR5B8EnkaE", + "amount": 21200000000 + } + ], + "double_spend_seen": false, + "fee": 196220000, + "height": 409449, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 1, + "minor": 0 + }, + "subaddr_indices": [ + { + "major": 1, + "minor": 2 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568408151, + "txid": "eda891adf76993f9066abd56a8a5aa5c51a7618298cab59ec37739f1c960596d", + "type": "out", + "unlock_time": 0 + }, + "transfers": [ + { + "address": "79kTZg96pMf2Dt9rLEWnLzTUB8XC1wMhxaJyxa79hJu6bK9CfFnfbSL1GJNZbqhv9xPqJhRj2Yfb7QUWa2zeEw56H4KiUfN", + "amount": 21200000000, + "confirmations": 23, + "destinations": [ + { + "address": "72quvHYJ8QzivQyV4NMZ9h1gyZXyJZvsWZTwgVFRCw9b1eJ4yibHEnU3CVCCXJ7evqXhSEKJL2rjzCMV3LpXirR5B8EnkaE", + "amount": 21200000000 + } + ], + "double_spend_seen": false, + "fee": 196220000, + "height": 409449, + "note": "", + "payment_id": "0000000000000000", + "subaddr_index": { + "major": 1, + "minor": 0 + }, + "subaddr_indices": [ + { + "major": 1, + "minor": 2 + } + ], + "suggested_confirmations_threshold": 1, + "timestamp": 1568408151, + "txid": "eda891adf76993f9066abd56a8a5aa5c51a7618298cab59ec37739f1c960596d", + "type": "out", + "unlock_time": 0 + } + ] + } +} diff --git a/tests/test_address.py b/tests/test_address.py index 89363d0..c830f38 100644 --- a/tests/test_address.py +++ b/tests/test_address.py @@ -14,18 +14,34 @@ def __test__(cls): def test_from_and_to_string(self): a = Address(self.addr) self.assertEqual(str(a), self.addr) + self.assertEqual("{:s}".format(a), self.addr) self.assertEqual(a.spend_key(), self.psk) self.assertEqual(a.view_key(), self.pvk) + self.assertEqual(hash(a), hash(self.addr)) + ba = Address(self.addr.encode()) + self.assertEqual(ba, a) + ba = address(self.addr.encode()) + self.assertEqual(ba, a) ia = IntegratedAddress(self.iaddr) self.assertEqual(ia.payment_id(), self.pid) self.assertEqual(str(ia), self.iaddr) + self.assertEqual("{:s}".format(ia), self.iaddr) self.assertEqual(ia.spend_key(), self.psk) self.assertEqual(ia.view_key(), self.pvk) self.assertEqual(ia.base_address(), a) + ba = IntegratedAddress(self.iaddr.encode()) + self.assertEqual(ba, ia) + ba = address(self.iaddr.encode()) + self.assertEqual(ba, ia) sa = SubAddress(self.subaddr) self.assertEqual(str(sa), self.subaddr) + self.assertEqual("{:s}".format(sa), self.subaddr) + ba = SubAddress(self.subaddr.encode()) + self.assertEqual(ba, sa) + ba = address(self.subaddr.encode()) + self.assertEqual(ba, sa) def test_payment_id(self): a = Address(self.addr) @@ -41,6 +57,7 @@ def test_recognition_and_comparisons(self): self.assertEqual(a, a2) self.assertEqual(a, self.addr) self.assertEqual(self.addr, a) + self.assertEqual(hash(a), hash(self.addr)) self.assertEqual(a.is_mainnet(), self.mainnet) self.assertEqual(a.is_testnet(), self.testnet) self.assertEqual(a.is_stagenet(), self.stagenet) @@ -54,6 +71,7 @@ def test_recognition_and_comparisons(self): self.assertEqual(ia, ia2) self.assertEqual(ia, self.iaddr) self.assertEqual(self.iaddr, ia) + self.assertEqual(hash(ia), hash(self.iaddr)) self.assertEqual(ia.is_mainnet(), self.mainnet) self.assertEqual(ia.is_testnet(), self.testnet) self.assertEqual(ia.is_stagenet(), self.stagenet) @@ -71,6 +89,7 @@ def test_recognition_and_comparisons(self): self.assertEqual(sa, sa2) self.assertEqual(sa, self.subaddr) self.assertEqual(self.subaddr, sa) + self.assertEqual(hash(sa), hash(self.subaddr)) self.assertEqual(sa.is_mainnet(), self.mainnet) self.assertEqual(sa.is_testnet(), self.testnet) self.assertEqual(sa.is_stagenet(), self.stagenet) diff --git a/tests/test_jsonrpcwallet.py b/tests/test_jsonrpcwallet.py index 7c5e5d6..b35f728 100644 --- a/tests/test_jsonrpcwallet.py +++ b/tests/test_jsonrpcwallet.py @@ -467,6 +467,7 @@ def test_incoming_confirmed_and_unconfirmed(self, mock_post): self.assertIsInstance(pmt.transaction.fee, Decimal) self.assertIsInstance(pmt.transaction.height, (int, type(None))) + @patch('monero.backends.jsonrpc.requests.post') def test_incoming_unconfirmed(self, mock_post): mock_post.return_value.status_code = 200 @@ -510,6 +511,165 @@ def test_incoming_unconfirmed(self, mock_post): self.assertIsInstance(pmt.transaction.fee, Decimal) self.assertIs(pmt.transaction.height, None) + @responses.activate + def test_incoming_by_tx_id(self): + # 3 payments in one transaction + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-00-get_accounts.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-7ab84-get_transfer_by_txid.json'), + status=200) + self.wallet = Wallet(JSONRPCWallet()) + pmts = self.wallet.incoming(tx_id='7ab84fe2fb34467c590cde2f7d6ba7de5928a2db6c84c6ccfff8962eca0ad99c') + self.assertEqual(len(pmts), 3) + self.assertEqual(pmts[0].amount, Decimal(1)) + self.assertEqual(pmts[1].amount, Decimal(1)) + self.assertEqual(pmts[2].amount, Decimal(2)) + + @responses.activate + def test_incoming_by_tx_id__with_min_height(self): + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-00-get_accounts.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-7ab84-get_transfer_by_txid.json'), + status=200) + self.wallet = Wallet(JSONRPCWallet()) + pmts = self.wallet.incoming(min_height=409223, + tx_id='7ab84fe2fb34467c590cde2f7d6ba7de5928a2db6c84c6ccfff8962eca0ad99c') + self.assertEqual(len(pmts), 3) + self.assertEqual(pmts[0].amount, Decimal(1)) + self.assertEqual(pmts[1].amount, Decimal(1)) + self.assertEqual(pmts[2].amount, Decimal(2)) + + @responses.activate + def test_incoming_by_tx_id__with_max_height(self): + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-00-get_accounts.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-7ab84-get_transfer_by_txid.json'), + status=200) + self.wallet = Wallet(JSONRPCWallet()) + pmts = self.wallet.incoming(max_height=409223, + tx_id='7ab84fe2fb34467c590cde2f7d6ba7de5928a2db6c84c6ccfff8962eca0ad99c') + self.assertEqual(len(pmts), 0) + + @responses.activate + def test_incoming_by_tx_id__not_found(self): + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-00-get_accounts.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-e0b15-get_transfer_by_txid.json'), + status=200) + self.wallet = Wallet(JSONRPCWallet()) + pmts = self.wallet.incoming(tx_id='e0b15ac819c94ed9ba81edb955a98c696f3216335960ccf90018d76a8dcb0e7e') + self.assertEqual(len(pmts), 0) + + @responses.activate + def test_incoming_by_tx_id__multiple_ids(self): + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-00-get_accounts.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-7ab84-get_transfer_by_txid.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-55e75-get_transfer_by_txid.json'), + status=200) + self.wallet = Wallet(JSONRPCWallet()) + pmts = self.wallet.incoming(tx_id=[ + '7ab84fe2fb34467c590cde2f7d6ba7de5928a2db6c84c6ccfff8962eca0ad99c', + '55e758d7d259bb316551ddcdd4808711de99c30b8b5c52de3e95e563fd92d156']) + self.assertEqual(len(pmts), 4) + self.assertEqual(pmts[0].amount, Decimal(4)) + self.assertEqual(pmts[1].amount, Decimal(1)) + self.assertEqual(pmts[2].amount, Decimal(1)) + self.assertEqual(pmts[3].amount, Decimal(2)) + + @responses.activate + def test_incoming_by_tx_id__mempool(self): + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-00-get_accounts.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-31b52-get_transfer_by_txid.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-31b52-get_transfer_by_txid.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_incoming_by_tx_id-31b52-get_transfer_by_txid.json'), + status=200) + self.wallet = Wallet(JSONRPCWallet()) + pmts = self.wallet.incoming( + tx_id='31b527fb9c27e759d56892fef93136df1057186c5cf4e3c93c5298b70160f562') + self.assertEqual(len(pmts), 0) + pmts = self.wallet.incoming( + tx_id='31b527fb9c27e759d56892fef93136df1057186c5cf4e3c93c5298b70160f562', + unconfirmed=True) + self.assertEqual(len(pmts), 1) + pmts = self.wallet.incoming( + tx_id='31b527fb9c27e759d56892fef93136df1057186c5cf4e3c93c5298b70160f562', + confirmed=False) + self.assertEqual(len(pmts), 0) + + @responses.activate + def test_outgoing_by_tx_id(self): + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_outgoing_by_tx_id-00-get_accounts.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_outgoing_by_tx_id-362c3-get_transfer_by_txid.json'), + status=200) + self.wallet = Wallet(JSONRPCWallet()) + acc = self.wallet.accounts[1] + pmts = acc.outgoing(tx_id='362c3a4e601d5847b3882c3debfd28a0ee31654e433c38498539677199c304c2') + self.assertEqual(len(pmts), 1) + self.assertEqual(pmts[0].amount, Decimal('0.52')) + + @responses.activate + def test_outgoing_by_tx_id__mempool(self): + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_outgoing_by_tx_id-00-get_accounts.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_outgoing_by_tx_id-afaf0-get_transfer_by_txid.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_outgoing_by_tx_id-afaf0-get_transfer_by_txid.json'), + status=200) + self.wallet = Wallet(JSONRPCWallet()) + acc = self.wallet.accounts[1] + pmts = acc.outgoing(tx_id='afaf04e5e40c6b60fc7cc928a88843fc96031ec2b567c310ee61abf3d00020da') + self.assertEqual(len(pmts), 0) + pmts = acc.outgoing( + tx_id='afaf04e5e40c6b60fc7cc928a88843fc96031ec2b567c310ee61abf3d00020da', + unconfirmed=True) + self.assertEqual(len(pmts), 1) + + @responses.activate + def test_outgoing_by_tx_id__multiple_ids(self): + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_outgoing_by_tx_id-00-get_accounts.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_outgoing_by_tx_id-362c3-get_transfer_by_txid.json'), + status=200) + responses.add(responses.POST, self.jsonrpc_url, + json=self._read('test_outgoing_by_tx_id-eda89-get_transfer_by_txid.json'), + status=200) + self.wallet = Wallet(JSONRPCWallet()) + acc = self.wallet.accounts[1] + pmts = acc.outgoing(tx_id=[ + '362c3a4e601d5847b3882c3debfd28a0ee31654e433c38498539677199c304c2', + 'eda891adf76993f9066abd56a8a5aa5c51a7618298cab59ec37739f1c960596d']) + self.assertEqual(len(pmts), 2) + self.assertEqual(pmts[0].amount, Decimal('0.52')) + self.assertEqual(pmts[1].amount, Decimal('0.0212')) + @patch('monero.backends.jsonrpc.requests.post') def test_incoming_by_payment_ids(self, mock_post): # These queries will use get_bulk_payments RPC method instead of get_transfers diff --git a/utils/daemonping.py b/utils/daemoninfo.py similarity index 59% rename from utils/daemonping.py rename to utils/daemoninfo.py index 3afeda9..6f1c37f 100755 --- a/utils/daemonping.py +++ b/utils/daemoninfo.py @@ -30,8 +30,23 @@ def get_daemon(): d = get_daemon() info = d.info() -print("Net: {net:>18s}\n" +print("Net: {net:>15s}net\n" "Height: {height:10d}\n" - "Difficulty: {difficulty:10d}".format( - net='test' if info['testnet'] else 'live', + "Difficulty: {difficulty:10d}\n" + "Alt blocks: {alt_blocks_count:10d}\n".format( + net='test' if info['testnet'] \ + else 'stage' if info['stagenet'] \ + else 'main' if info['mainnet'] else 'unknown', **info)) +print("Last 6 blocks:") +for hdr in reversed(d.headers(info['height']-6, info['height']-1)): + print("{height:10d} {hash} {block_size_kb:6.2f} kB {num_txes:3d} txn(s) " + "v{major_version:d}".format( + block_size_kb=hdr['block_size']/1024.0, **hdr)) +mempool = d.mempool() +if mempool: + print("\n{:d} txn(s) in mempool:".format(len(mempool))) + for tx in d.mempool(): + print("{:>10s} {:s}".format(tx.timestamp.strftime("%H:%M:%S"), tx.hash)) +else: + print("\nMempool is empty")