diff --git a/README.rst b/README.rst index 14c6c72..9b4b2bc 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ Python Monero module A comprehensive Python module for handling Monero cryptocurrency. -* release 0.4.3 +* release 0.4.4 * open source: https://github.com/emesik/monero-python * works with Monero 0.12.x and `the latest source`_ (at least we try to keep up) * Python 2.x and 3.x compatible diff --git a/docs/source/conf.py b/docs/source/conf.py index 90e4d93..3d96ff8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -55,9 +55,9 @@ author = 'Michal Salaban' # built documents. # # The short X.Y version. -version = '0.4.3' +version = '0.4.4' # The full version, including alpha/beta/rc tags. -release = '0.4.3' +release = '0.4.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -102,9 +102,13 @@ html_static_path = ['_static'] # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { - '**': [ + 'index': [ 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html' + ], + '**': [ 'searchbox.html', + 'globaltoc.html' ] } @@ -164,6 +168,3 @@ texinfo_documents = [ author, 'MoneroPythonmodule', 'One line description of project.', 'Miscellaneous'), ] - - - diff --git a/docs/source/transactions.rst b/docs/source/transactions.rst index b40b286..e26f487 100644 --- a/docs/source/transactions.rst +++ b/docs/source/transactions.rst @@ -177,6 +177,10 @@ and ``unconfirmed`` query parameters that accept boolean values: You may as well query for both confirmed and unconfirmed transactions using ``wallet.incoming(unconfirmed=True)`` (the default value for ``confirmed`` is ``True``). +.. note:: Mempool transactions don't belong to the blockchain (yet), therefore they have no height. + Setting ``min_height`` or ``max_height`` arguments will **always exclude mempool + transactions**. If ``unconfirmed`` is also set to ``True``, a warning will be issued. + .. _sending-payments: Sending payments diff --git a/monero/__init__.py b/monero/__init__.py index d77b169..e40a442 100644 --- a/monero/__init__.py +++ b/monero/__init__.py @@ -1,3 +1,3 @@ from . import address, account, daemon, wallet, numbers, prio, wordlists, seed -__version__ = '0.4.3' +__version__ = '0.4.4' diff --git a/monero/address.py b/monero/address.py index 5376bc9..1e1a060 100644 --- a/monero/address.py +++ b/monero/address.py @@ -4,6 +4,7 @@ import struct from sha3 import keccak_256 from . import base58 +from . import ed25519 from . import numbers _ADDR_REGEX = re.compile(r'^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{95}$') @@ -90,6 +91,20 @@ class Address(BaseAddress): """ return hexlify(self._decoded[1:33]).decode() + def check_private_view_key(self, key): + """Checks if private view key matches this address. + + :rtype: bool + """ + return ed25519.public_from_secret_hex(key) == self.view_key() + + def check_private_spend_key(self, key): + """Checks if private spend key matches this address. + + :rtype: bool + """ + return ed25519.public_from_secret_hex(key) == self.spend_key() + def with_payment_id(self, payment_id=0): """Integrates payment id into the address. diff --git a/monero/ed25519.py b/monero/ed25519.py index d1fc836..354223c 100644 --- a/monero/ed25519.py +++ b/monero/ed25519.py @@ -3,6 +3,7 @@ # # Parts Copyright (c) 2016 The MoneroPy Developers. Released under the BSD 3-Clause +from binascii import hexlify, unhexlify import hashlib import operator as _oper import sys as _sys @@ -133,3 +134,10 @@ def scalarmultbase(e): if e & 1: Q = edwards(Q, B) return Q +def public_from_secret(k): + keyInt = decodeint(k) + aG = scalarmultbase(keyInt) + return encodepoint(aG) + +def public_from_secret_hex(hk): + return hexlify(public_from_secret(unhexlify(hk))).decode() diff --git a/monero/seed.py b/monero/seed.py index b887aa4..21f31e9 100644 --- a/monero/seed.py +++ b/monero/seed.py @@ -137,14 +137,10 @@ class Seed(object): return self.sc_reduce(h.digest()) def public_spend_key(self): - keyInt = ed25519.decodeint(unhexlify(self.secret_spend_key())) - aG = ed25519.scalarmultbase(keyInt) - return hexlify(ed25519.encodepoint(aG)).decode() + return ed25519.public_from_secret_hex(self.secret_spend_key()) def public_view_key(self): - keyInt = ed25519.decodeint(unhexlify(self.secret_view_key())) - aG = ed25519.scalarmultbase(keyInt) - return hexlify(ed25519.encodepoint(aG)).decode() + return ed25519.public_from_secret_hex(self.secret_view_key()) def public_address(self, net='mainnet'): """Returns the master :class:`Address ` represented by the seed. diff --git a/monero/transaction.py b/monero/transaction.py index 20e9bcf..73afeae 100644 --- a/monero/transaction.py +++ b/monero/transaction.py @@ -1,4 +1,5 @@ import sys +import warnings from .address import address from .numbers import PaymentID @@ -160,7 +161,12 @@ class PaymentFilter(object): _payment_id = filterparams.pop('payment_id', None) if len(filterparams) > 0: raise ValueError("Excessive arguments for payment query: {}".format(filterparams)) - + 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) if _local_address is None: self.local_addresses = [] else: diff --git a/tests/test_address.py b/tests/test_address.py index 73e8efd..261afd5 100644 --- a/tests/test_address.py +++ b/tests/test_address.py @@ -78,6 +78,22 @@ class Tests(object): self.assertNotEqual(a, 0) + def test_check_private_view_key(self): + a = Address(self.addr) + self.assertFalse(a.check_private_view_key(self.ssk)) + self.assertTrue(a.check_private_view_key(self.svk)) + self.assertFalse(a.check_private_view_key(self.psk)) + self.assertFalse(a.check_private_view_key(self.pvk)) + self.assertFalse(a.check_private_view_key('0000000000000000000000000000000000000000000000000000000000000000')) + + def test_check_private_spend_key(self): + a = Address(self.addr) + self.assertTrue(a.check_private_spend_key(self.ssk)) + self.assertFalse(a.check_private_spend_key(self.svk)) + self.assertFalse(a.check_private_spend_key(self.psk)) + self.assertFalse(a.check_private_spend_key(self.pvk)) + self.assertFalse(a.check_private_spend_key('0000000000000000000000000000000000000000000000000000000000000000')) + def test_idempotence(self): a = Address(self.addr) a_idem = Address(a) @@ -130,46 +146,53 @@ class Tests(object): sa = SubAddress(self.subaddr) self.assertRaises(TypeError, sa.with_payment_id, self.pid) + class AddressTestCase(unittest.TestCase, Tests): - addr = '43aeKax1ts4BoEbSyzKVbbDRmc8nsnpZLUpQBYvhUxs3KVrodnaFaBEQMDp69u4VaiEG3LSQXA6M61mXPrztCLuh7PFUAmd' - psk = '33a7ceb933b793408d49e82c0a34664a4be7117243cb77a64ef280b866d8aa6e' - pvk = '96f70d63d9d3558b97a5dd200a170b4f45b3177a274aa90496ea683896ff6438' + addr = '47ewoP19TN7JEEnFKUJHAYhGxkeTRH82sf36giEp9AcNfDBfkAtRLX7A6rZz18bbNHPNV7ex6WYbMN3aKisFRJZ8Ebsmgef' + ssk = 'e0fe01d5794e240a26609250c0d7e01673219eececa3f499d5cfa20a75739b0a' + svk = '6d9056aa2c096bfcd2f272759555e5764ba204dd362604a983fa3e0aafd35901' + psk = '9f2a76d879aaf0670039dc8dbdca01f0ca26a2f6d93268e3674666bfdc5957e4' + pvk = '716cfc7da7e6ce366935c55747839a85be798037ab189c7dd0f10b7f1690cb78' pid = '4a6f686e47616c74' - subaddr = '83bK2pMxCQXdRyd6W1haNWYRsF6Qb3iGa8gxKEynm9U7cYoXrMHFwRrFFuxRSgnLtGe7LM8SmrPY6L3TVBa3UV3YLuVJ7Rw' - iaddr = '4DHKLPmWW8aBoEbSyzKVbbDRmc8nsnpZLUpQBYvhUxs3KVrodnaFaBEQMDp69u4VaiEG3LSQXA6M61mXPrztCLuhAR6GpL18QNwE8h3TuF' + iaddr = '4HMcpBpe4ddJEEnFKUJHAYhGxkeTRH82sf36giEp9AcNfDBfkAtRLX7A6rZz18bbNHPNV7ex6WYbMN3aKisFRJZ8M7yKhzQhKW3ECCLWQw' + subaddr = '84LooD7i35SFppgf4tQ453Vi3q5WexSUXaVgut69ro8MFnmHwuezAArEZTZyLr9fS6QotjqkSAxSF6d1aDgsPoX849izJ7m' mainnet = True testnet = False stagenet = False - addr_invalid = '43aeKax1ts4boEbSyzKVbbDRmc8nsnpZLUpQBYvhUxs3KVrodnaFaBEQMDp69u4VaiEG3LSQXA6M61mXPrztCLuh7PFUAmd' - iaddr_invalid = '4DHKLpmWW8aBoEbSyzKVbbDRmc8nsnpZLUpQBYvhUxs3KVrodnaFaBEQMDp69u4VaiEG3LSQXA6M61mXPrztCLuhAR6GpL18QNwE8h3TuF' + addr_invalid = '47ewoP19TN7JCEnFKUJHAYhGxkeTRH82sf36giEp9AcNfDBfkAtRLX7A6rZz18bbNHPNV7ex6WYbMN3aKisFRJZ8Ebsmgef' + iaddr_invalid = '4HMcpBpe4ddJEEnFKUJHAYhGxkyTRH82sf36giEp9AcNfDBfkAtRLX7A6rZz18bbNHPNV7ex6WYbMN3aKisFRJZ8M7yKhzQhKW3ECCLWQw' class TestnetAddressTestCase(AddressTestCase, Tests): - addr = '9vgV48wWAPTWik5QSUSoGYicdvvsbSNHrT9Arsx1XBTz6VrWPSgfmnUKSPZDMyX4Ms8R9TkhB4uFqK9s5LUBbV6YQN2Q9ag' - psk = '5cbcfbcea7cc62b1aeb76758ad8df5f8cbe0c63d40c8cd9c49377bbc9c9b9520' - pvk = 'de048ca310ff7d6e3b6714bccdebd62c56d680a10272846c875241fa2c5fc1cf' + addr = '9wuKTHsxGiwEsMp2fYzJiVahyhU2aZi1oZ6R6fK5U64uRa1Pxi8diZh2S1GJFqYXRRhcbfzfWiPD819zKEZkXTMwP7hMs5N' + ssk = '4f5b7af2c1942067ba33d34318b9735cb46ab5d50b75294844c82a9dd872c201' + svk = '60cf228f2bf7f6a70643afe9468fde254145dbd3aab4072ede14bf8bae914103' + psk = '7cf743dcfd23d452e9b2936caeb622c9849f1ff1ddfd62bfdfac64113c1a4e92' + pvk = 'e3924b14d99a9c088e5a45278d5218f2d053b1c03c480f00ed2ee3dce80806c4' pid = '4a6f686e47616c74' - iaddr = 'A6PA4wkzmeyWik5QSUSoGYicdvvsbSNHrT9Arsx1XBTz6VrWPSgfmnUKSPZDMyX4Ms8R9TkhB4uFqK9s5LUBbV6YbfyqvDecDn3E7cvp9b' - subaddr = 'BbBjyYoYNNwFfL8RRVRTMiZUofBLpjRxdNnd5E4LyGcAK5CEsnL3gmE5QkrDRta7RPficGHcFdR6rUwWcjnwZVvCE3tLxhJ' + subaddr = 'BaU3yLuDqdcETYzeF7vFSVEKNR4sSGxBV1Evrw5yNBf2VMiuAwfDmiF3RHqLHkaA5A6RGiNNRUqvtaqhMtdjA1SQ1tnQV8D' + iaddr = 'A7bzU6hSszTEsMp2fYzJiVahyhU2aZi1oZ6R6fK5U64uRa1Pxi8diZh2S1GJFqYXRRhcbfzfWiPD819zKEZkXTMwZqGSmLeBXqMEBnZVkh' mainnet = False testnet = True stagenet = False - addr_invalid = '9vgV48wWAPTWik5QSUSoGYicdvvsbSNHrT9Arsx1XBTz6VrWPSgfmnUKSPZDMyX4Ms8R9TkhB4uFqK9s5LUbbV6YQN2Q9ag' - iaddr_invalid = 'A6PA4wkzmeyWik5qSUSoGYicdvvsbSNHrT9Arsx1XBTz6VrWPSgfmnUKSPZDMyX4Ms8R9TkhB4uFqK9s5LUBbV6YbfyqvDecDn3E7cvp9b' + addr_invalid = '9wuKTHsxGiwEsMp3fYzJiVahyhU2aZi1oZ6R6fK5U64uRa1Pxi8diZh2S1GJFqYXRRhcbfzfWiPD819zKEZkXTMwP7hMs5N' + iaddr_invalid = 'A7bzU6hSszTEsMp2fYzJiVahyhU2aZi2oZ6R6fK5U64uRa1Pxi8diZh2S1GJFqYXRRhcbfzfWiPD819zKEZkXTMwZqGSmLeBXqMEBnZVkh' class StagenetAddressTestCase(AddressTestCase, Tests): - addr = '56cXYWG13YKaT9z1aEy2hb9TZNnxrW3zE9S4nTQVDux5Qq7UYsmjuux3Zstxkorj9HAufyWLU3FwHW4uERQF6tkeUVogGN3' - psk = '7e33891fe6ea30c7fd79d48e250906329104dc77407cf732699f41564df8ca8e' - pvk = '77a3720428f91f0f58a196bb374f703b3ca45fa55f0764adc81ff241c4c797f3' + addr = '52jzuBBUMty3xPL3JsQxGP74LDuV6E1LS8Zda1PbdqQjGzFmH6N9ep9McbFKMALujVT9S5mKpbEgC5VPhfoAiVj8LdAqbp6' + ssk = 'a8733c61797115db4ec8a5ce39fb811f81dd4ec163b880526683e059c7e62503' + svk = 'fd5c0d25f8f994268079a4f7844274dc870a7c2b88fbfc24ba318375e1d9430f' + psk = '180c1d7bbf7f2e11aa90d0f61bf49024370e01cd54f33f2d36bba0357c9c205f' + pvk = '94b66a81e646927b3da74392306f789c5024734b4ce6351ad74c4c7d7351b3ad' pid = '4a6f686e47616c74' - iaddr = '5GKCZK5VeoqaT9z1aEy2hb9TZNnxrW3zE9S4nTQVDux5Qq7UYsmjuux3Zstxkorj9HAufyWLU3FwHW4uERQF6tkehhE4RH8N7QfEAC8jMy' - subaddr = '7417qYoKBoYXCugU2KvJBZExmyjav4n1MVME74AeWNwxQ39wKtbWdyP6YGuMK6C7HkAjBuVcbUYmCWbxDLwk9GAX4qyb48U' + subaddr = '7AeQwvrLtPeYoXVPRkEu8oEL7N9wnqHjYKwSvTf6YKbHgYmw6AJMsjggzVLo21egMK9qcoV1mxCTfP4FbaGb7JEMDfpLetk' + iaddr = '5CSfuyzxyAV3xPL3JsQxGP74LDuV6E1LS8Zda1PbdqQjGzFmH6N9ep9McbFKMALujVT9S5mKpbEgC5VPhfoAiVj8Vz8ySmoqYgTE8dR1yS' mainnet = False testnet = False stagenet = True - addr_invalid ='7417qYoKBoYXCugU2KvJBZExmyjav4n1MVME74AeWNwxQ39wKtbWdyP6YGuMK6C7HkAjBuVcbUYmCWbyDLwk9GAX4qyb48U' - iaddr_invalid = '5GKCZK5VeuqaT9z1aEy2hb9TZNnxrW3zE9S4nTQVDux5Qq7UYsmjuux3Zstxkorj9HAufyWLU3FwHW4uERQF6tkehhE4RH8N7QfEAC8jMy' + addr_invalid = '52jzuBBUMty3xPL3JsQxGP74LDuV6H1LS8Zda1PbdqQjGzFmH6N9ep9McbFKMALujVT9S5mKpbEgC5VPhfoAiVj8LdAqbp6' + iaddr_invalid = '5CSfuyzxyAV3xPL3JsQxGP74LDuV6E1LS8Zda1PbdqQjGzFmH6N9ep9McbFKMALujVT9S5mKppEgC5VPhfoAiVj8Vz8ySmoqYgTE8dR1yS' class KnownBugsTest(unittest.TestCase): diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 9b90984..cca8cc9 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1,12 +1,13 @@ from datetime import datetime from decimal import Decimal import unittest +import warnings from monero.wallet import Wallet from monero.account import Account from monero.address import address from monero.numbers import PaymentID -from monero.transaction import IncomingPayment, OutgoingPayment, Transaction +from monero.transaction import IncomingPayment, Transaction class FiltersTestCase(unittest.TestCase): def setUp(self): @@ -146,43 +147,51 @@ class FiltersTestCase(unittest.TestCase): self.assertEqual(len(pmts), 3) def test_filter_mempool(self): - pmts = self.wallet.incoming() - self.assertEqual(len(pmts), 7) - for p in pmts: - self.assertGreater(self.wallet.confirmations(p.transaction), 0) - pmts = self.wallet.incoming(unconfirmed=True) - self.assertEqual(len(pmts), 8) - pmts = self.wallet.incoming(unconfirmed=True, confirmed=False) - self.assertEqual(len(pmts), 1) - self.assertEqual( - pmts[0].transaction.hash, - 'd29264ad317e8fdb55ea04484c00420430c35be7b3fe6dd663f99aebf41a786c') - self.assertEqual(self.wallet.confirmations(pmts[0]), 0) - self.assertEqual(self.wallet.confirmations(pmts[0].transaction), 0) - pmts = self.wallet.incoming(unconfirmed=True, confirmed=False, min_height=1) - self.assertEqual(len(pmts), 0) - pmts = self.wallet.incoming(unconfirmed=True, confirmed=False, max_height=99999999999999) - self.assertEqual(len(pmts), 0) - pmts = self.wallet.incoming(payment_id='03f6649304ea4cb2') - self.assertEqual(len(pmts), 0) - pmts = self.wallet.incoming(unconfirmed=True, payment_id='03f6649304ea4cb2') - self.assertEqual(len(pmts), 1) - pmts = self.wallet.incoming( - local_address='9tQoHWyZ4yXUgbz9nvMcFZUfDy5hxcdZabQCxmNCUukKYicXegsDL7nQpcUa3A1pF6K3fhq3scsyY88tdB1MqucULcKzWZC') - self.assertEqual(len(pmts), 4) - pmts = self.wallet.incoming( - unconfirmed=True, - local_address='9tQoHWyZ4yXUgbz9nvMcFZUfDy5hxcdZabQCxmNCUukKYicXegsDL7nQpcUa3A1pF6K3fhq3scsyY88tdB1MqucULcKzWZC') - self.assertEqual(len(pmts), 5) - pmts = self.wallet.incoming( - local_address='9tQoHWyZ4yXUgbz9nvMcFZUfDy5hxcdZabQCxmNCUukKYicXegsDL7nQpcUa3A1pF6K3fhq3scsyY88tdB1MqucULcKzWZC', - payment_id='03f6649304ea4cb2') - self.assertEqual(len(pmts), 0) - pmts = self.wallet.incoming( - unconfirmed=True, - local_address='9tQoHWyZ4yXUgbz9nvMcFZUfDy5hxcdZabQCxmNCUukKYicXegsDL7nQpcUa3A1pF6K3fhq3scsyY88tdB1MqucULcKzWZC', - payment_id='03f6649304ea4cb2') - self.assertEqual(len(pmts), 1) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + pmts = self.wallet.incoming() + self.assertEqual(len(pmts), 7) + for p in pmts: + self.assertGreater(self.wallet.confirmations(p.transaction), 0) + pmts = self.wallet.incoming(unconfirmed=True) + self.assertEqual(len(pmts), 8) + pmts = self.wallet.incoming(unconfirmed=True, confirmed=False) + self.assertEqual(len(pmts), 1) + self.assertEqual( + pmts[0].transaction.hash, + 'd29264ad317e8fdb55ea04484c00420430c35be7b3fe6dd663f99aebf41a786c') + self.assertEqual(self.wallet.confirmations(pmts[0]), 0) + self.assertEqual(self.wallet.confirmations(pmts[0].transaction), 0) + self.assertEqual(len(w), 0) + pmts = self.wallet.incoming(unconfirmed=True, confirmed=False, min_height=1) + self.assertEqual(len(pmts), 0) + self.assertEqual(len(w), 1) + self.assertIs(w[0].category, RuntimeWarning) + pmts = self.wallet.incoming(unconfirmed=True, confirmed=False, max_height=99999999999999) + self.assertEqual(len(pmts), 0) + self.assertEqual(len(w), 2) + self.assertIs(w[1].category, RuntimeWarning) + pmts = self.wallet.incoming(payment_id='03f6649304ea4cb2') + self.assertEqual(len(pmts), 0) + pmts = self.wallet.incoming(unconfirmed=True, payment_id='03f6649304ea4cb2') + self.assertEqual(len(pmts), 1) + pmts = self.wallet.incoming( + local_address='9tQoHWyZ4yXUgbz9nvMcFZUfDy5hxcdZabQCxmNCUukKYicXegsDL7nQpcUa3A1pF6K3fhq3scsyY88tdB1MqucULcKzWZC') + self.assertEqual(len(pmts), 4) + pmts = self.wallet.incoming( + unconfirmed=True, + local_address='9tQoHWyZ4yXUgbz9nvMcFZUfDy5hxcdZabQCxmNCUukKYicXegsDL7nQpcUa3A1pF6K3fhq3scsyY88tdB1MqucULcKzWZC') + self.assertEqual(len(pmts), 5) + pmts = self.wallet.incoming( + local_address='9tQoHWyZ4yXUgbz9nvMcFZUfDy5hxcdZabQCxmNCUukKYicXegsDL7nQpcUa3A1pF6K3fhq3scsyY88tdB1MqucULcKzWZC', + payment_id='03f6649304ea4cb2') + self.assertEqual(len(pmts), 0) + pmts = self.wallet.incoming( + unconfirmed=True, + local_address='9tQoHWyZ4yXUgbz9nvMcFZUfDy5hxcdZabQCxmNCUukKYicXegsDL7nQpcUa3A1pF6K3fhq3scsyY88tdB1MqucULcKzWZC', + payment_id='03f6649304ea4cb2') + self.assertEqual(len(pmts), 1) + self.assertEqual(len(w), 2) def test_filter_excessive(self): self.assertRaises(ValueError, self.wallet.incoming, excessive_argument='foo')