diff --git a/monero/__init__.py b/monero/__init__.py index 4583f42..38cf7cc 100644 --- a/monero/__init__.py +++ b/monero/__init__.py @@ -1,3 +1,3 @@ -from . import address, account, daemon, wallet, numbers, prio, wordlists, seed +from . import address, account, const, daemon, wallet, numbers, wordlists, seed __version__ = '0.7' diff --git a/monero/account.py b/monero/account.py index 10ced7c..f8087b4 100644 --- a/monero/account.py +++ b/monero/account.py @@ -1,4 +1,4 @@ -from . import prio +from . import const from .transaction import PaymentManager @@ -70,7 +70,7 @@ class Account(object): return self._backend.new_address(account=self.index, label=label) def transfer(self, address, amount, - priority=prio.NORMAL, payment_id=None, unlock_time=0, + priority=const.PRIO_NORMAL, payment_id=None, unlock_time=0, relay=True): """ Sends a transfer. Returns a list of resulting transactions. @@ -79,7 +79,7 @@ class Account(object): :param amount: amount to send :param priority: transaction priority, implies fee. The priority can be a number from 1 to 4 (unimportant, normal, elevated, priority) or a constant - from `monero.prio`. + from `monero.const.PRIO_*`. :param payment_id: ID for the payment (must be None if :class:`IntegratedAddress ` is used as the destination) @@ -98,7 +98,7 @@ class Account(object): relay=relay) def transfer_multiple(self, destinations, - priority=prio.NORMAL, payment_id=None, unlock_time=0, + priority=const.PRIO_NORMAL, payment_id=None, unlock_time=0, relay=True): """ Sends a batch of transfers. Returns a list of resulting transactions. @@ -107,7 +107,7 @@ class Account(object): [(:class:`Address `, `Decimal`), ...] :param priority: transaction priority, implies fee. The priority can be a number from 1 to 4 (unimportant, normal, elevated, priority) or a constant - from `monero.prio`. + from `monero.const.PRIO_*`. :param payment_id: ID for the payment (must be None if :class:`IntegratedAddress ` is used as the destination) @@ -126,7 +126,7 @@ class Account(object): account=self.index, relay=relay) - def sweep_all(self, address, priority=prio.NORMAL, payment_id=None, + def sweep_all(self, address, priority=const.PRIO_NORMAL, payment_id=None, subaddr_indices=None, unlock_time=0, relay=True): """ Sends all unlocked balance to an address. Returns a list of resulting transactions. @@ -134,7 +134,7 @@ class Account(object): :param address: destination :class:`Address ` or subtype :param priority: transaction priority, implies fee. The priority can be a number from 1 to 4 (unimportant, normal, elevated, priority) or a constant - from `monero.prio`. + from `monero.const.PRIO_*`. :param payment_id: ID for the payment (must be None if :class:`IntegratedAddress ` is used as the destination) diff --git a/monero/address.py b/monero/address.py index 8379006..04a4096 100644 --- a/monero/address.py +++ b/monero/address.py @@ -3,8 +3,10 @@ import re from sha3 import keccak_256 import six import struct +import warnings from . import base58 +from . import const from . import ed25519 from . import numbers @@ -36,26 +38,42 @@ class BaseAddress(object): """ return hexlify(self._decoded[1:33]).decode() + @property + def net(self): + return const.NETS[self._valid_netbytes.index(self._decoded[0])] + def is_mainnet(self): """Returns `True` if the address belongs to mainnet. :rtype: bool """ - return self._decoded[0] == self._valid_netbytes[0] + warnings.warn(".is_mainnet(), .is_testnet() and .is_stagenet() methods are deprecated " + "and will be gone in 0.8; use Address.net property and constants form monero.const " + "instead", + DeprecationWarning) + return self.net == const.NET_MAIN def is_testnet(self): """Returns `True` if the address belongs to testnet. :rtype: bool """ - return self._decoded[0] == self._valid_netbytes[1] + warnings.warn(".is_mainnet(), .is_testnet() and .is_stagenet() methods are deprecated " + "and will be gone in 0.8; use Address.net property and constants form monero.const " + "instead", + DeprecationWarning) + return self.net == const.NET_TEST def is_stagenet(self): """Returns `True` if the address belongs to stagenet. :rtype: bool """ - return self._decoded[0] == self._valid_netbytes[2] + warnings.warn(".is_mainnet(), .is_testnet() and .is_stagenet() methods are deprecated " + "and will be gone in 0.8; use Address.net property and constants form monero.const " + "instead", + DeprecationWarning) + return self.net == const.NET_STAGE def _decode(self, address): self._decoded = bytearray(unhexlify(base58.decode(address))) @@ -92,8 +110,7 @@ class Address(BaseAddress): :param address: a Monero address as string-like object :param label: a label for the address (defaults to `None`) """ - _valid_netbytes = (18, 53, 24) - # NOTE: _valid_netbytes order is (mainnet, testnet, stagenet) + _valid_netbytes = const.MASTERADDR_NETBYTES def check_private_view_key(self, key): """Checks if private view key matches this address. @@ -121,7 +138,7 @@ class Address(BaseAddress): payment_id = numbers.PaymentID(payment_id) if not payment_id.is_short(): raise TypeError("Payment ID {0} has more than 64 bits and cannot be integrated".format(payment_id)) - prefix = 54 if self.is_testnet() else 25 if self.is_stagenet() else 19 + prefix = const.INTADDRR_NETBYTES[const.NETS.index(self.net)] data = bytearray([prefix]) + self._decoded[1:65] + struct.pack('>Q', int(payment_id)) checksum = bytearray(keccak_256(data).digest()[:4]) return IntegratedAddress(base58.encode(hexlify(data + checksum))) @@ -133,8 +150,7 @@ class SubAddress(BaseAddress): Any type of address which is not the master one for a wallet. """ - _valid_netbytes = (42, 63, 36) - # NOTE: _valid_netbytes order is (mainnet, testnet, stagenet) + _valid_netbytes = const.SUBADDR_NETBYTES def with_payment_id(self, _): raise TypeError("SubAddress cannot be integrated with payment ID") @@ -146,8 +162,7 @@ class IntegratedAddress(Address): A master address integrated with payment id (short one, max 64 bit). """ - _valid_netbytes = (19, 54, 25) - # NOTE: _valid_netbytes order is (mainnet, testnet, stagenet) + _valid_netbytes = const.INTADDRR_NETBYTES def __init__(self, address): address = address.decode() if isinstance(address, bytes) else str(address) @@ -167,7 +182,7 @@ class IntegratedAddress(Address): """Returns the base address without payment id. :rtype: :class:`Address` """ - prefix = 53 if self.is_testnet() else 24 if self.is_stagenet() else 18 + prefix = const.MASTERADDR_NETBYTES[const.NETS.index(self.net)] data = bytearray([prefix]) + self._decoded[1:65] checksum = keccak_256(data).digest()[:4] return Address(base58.encode(hexlify(data + checksum))) diff --git a/monero/const.py b/monero/const.py new file mode 100644 index 0000000..19358e1 --- /dev/null +++ b/monero/const.py @@ -0,0 +1,13 @@ +NET_MAIN = "main" +NET_STAGE = "stage" +NET_TEST = "test" + +NETS = (NET_MAIN, NET_TEST, NET_STAGE) +MASTERADDR_NETBYTES = (18, 53, 24) +SUBADDR_NETBYTES = (42, 63, 36) +INTADDRR_NETBYTES = (19, 54, 25) + +PRIO_UNIMPORTANT = 1 +PRIO_NORMAL = 2 +PRIO_ELEVATED = 3 +PRIO_PRIORITY = 4 diff --git a/monero/prio.py b/monero/prio.py index 8626d30..2848ad4 100644 --- a/monero/prio.py +++ b/monero/prio.py @@ -1,3 +1,8 @@ +import warnings +warnings.warn( + "monero.prio is deprecated and will be gone in 0.8; use monero.const.PRIO_* consts instead", + DeprecationWarning) + UNIMPORTANT=1 NORMAL=2 ELEVATED=3 diff --git a/monero/seed.py b/monero/seed.py index 07dd924..8509c9a 100644 --- a/monero/seed.py +++ b/monero/seed.py @@ -35,13 +35,12 @@ # + simplified interface, changed exceptions (assertions -> explicit raise) # + optimization -from monero import wordlists -from monero import ed25519 -from monero import base58 -from monero.address import address from binascii import hexlify, unhexlify from os import urandom from sha3 import keccak_256 +import warnings +from . import base58, const, ed25519, wordlists +from .address import address class Seed(object): """Creates a seed object either from local system randomness or an imported phrase. @@ -151,17 +150,25 @@ class Seed(object): self._ed_pub_view_key = ed25519.public_from_secret_hex(self.secret_view_key()) return self._ed_pub_view_key - def public_address(self, net='mainnet'): + def public_address(self, net=const.NET_MAIN): """Returns the master :class:`Address ` represented by the seed. - :param net: the network, one of 'mainnet', 'testnet', 'stagenet'. Default is 'mainnet'. + :param net: the network, one of `const.NET_*`; default is `const.NET_MAIN` :rtype: :class:`Address ` """ - if net not in ('mainnet', 'testnet', 'stagenet'): + # backward compatibility + _net = net[:-3] if net.endswith('net') else net + if _net != net: + warnings.warn( + "Argument '{:s}' is deprecated and will not be accepted in 0.8, " + "use one of monero.const.NET_*".format(net), + DeprecationWarning) + net = _net + if net not in const.NETS: raise ValueError( - "Invalid net argument. Must be one of ('mainnet', 'testnet', 'stagenet').") - netbyte = 18 if net == 'mainnet' else 53 if net == 'testnet' else 24 + "Invalid net argument '{:s}'. Must be one of monero.const.NET_*".format(net)) + netbyte = (18, 53, 24)[const.NETS.index(net)] data = "{:x}{:s}{:s}".format(netbyte, self.public_spend_key(), self.public_view_key()) h = keccak_256() h.update(unhexlify(data)) diff --git a/monero/wallet.py b/monero/wallet.py index 106908e..be6a8f3 100644 --- a/monero/wallet.py +++ b/monero/wallet.py @@ -4,9 +4,9 @@ import struct from . import address from . import base58 +from . import const from . import ed25519 from . import numbers -from . import prio from .transaction import Payment, PaymentManager @@ -224,15 +224,13 @@ class Wallet(object): ed25519.scalarmult_B(ed25519.decodeint(m))) # C = master_svk * D C = ed25519.scalarmult(D, ed25519.decodeint(master_svk)) - netbyte = bytearray([ - 42 if master_address.is_mainnet() else \ - 63 if master_address.is_testnet() else 36]) + netbyte = bytearray([const.SUBADDR_NETBYTES[const.NETS.index(master_address.net)]]) data = netbyte + ed25519.encodepoint(D) + ed25519.encodepoint(C) checksum = keccak_256(data).digest()[:4] return address.SubAddress(base58.encode(hexlify(data + checksum))) def transfer(self, address, amount, - priority=prio.NORMAL, payment_id=None, unlock_time=0, + priority=const.PRIO_NORMAL, payment_id=None, unlock_time=0, relay=True): """ Sends a transfer from the default account. Returns a list of resulting transactions. @@ -241,7 +239,7 @@ class Wallet(object): :param amount: amount to send :param priority: transaction priority, implies fee. The priority can be a number from 1 to 4 (unimportant, normal, elevated, priority) or a constant - from `monero.prio`. + from `monero.const.PRIO_*`. :param payment_id: ID for the payment (must be None if :class:`IntegratedAddress ` is used as the destination) @@ -260,7 +258,7 @@ class Wallet(object): relay=relay) def transfer_multiple(self, destinations, - priority=prio.NORMAL, payment_id=None, unlock_time=0, + priority=const.PRIO_NORMAL, payment_id=None, unlock_time=0, relay=True): """ Sends a batch of transfers from the default account. Returns a list of resulting @@ -269,7 +267,7 @@ class Wallet(object): :param destinations: a list of destination and amount pairs: [(address, amount), ...] :param priority: transaction priority, implies fee. The priority can be a number from 1 to 4 (unimportant, normal, elevated, priority) or a constant - from `monero.prio`. + from `monero.const.PRIO_*`. :param payment_id: ID for the payment (must be None if :class:`IntegratedAddress ` is used as a destination) @@ -287,7 +285,7 @@ class Wallet(object): unlock_time=unlock_time, relay=relay) - def sweep_all(self, address, priority=prio.NORMAL, payment_id=None, + def sweep_all(self, address, priority=const.PRIO_NORMAL, payment_id=None, subaddr_indices=None, unlock_time=0, relay=True): """ Sends all unlocked balance from the default account to an address. @@ -296,7 +294,7 @@ class Wallet(object): :param address: destination :class:`Address ` or subtype :param priority: transaction priority, implies fee. The priority can be a number from 1 to 4 (unimportant, normal, elevated, priority) or a constant - from `monero.prio`. + from `monero.const.PRIO_*`. :param payment_id: ID for the payment (must be None if :class:`IntegratedAddress ` is used as the destination) diff --git a/tests/test_address.py b/tests/test_address.py index 75ae76f..9d3ada0 100644 --- a/tests/test_address.py +++ b/tests/test_address.py @@ -1,7 +1,7 @@ -import json -import os +import pytest import unittest +from monero import const from monero.address import Address, SubAddress, IntegratedAddress, address from tests.utils import classproperty @@ -50,6 +50,7 @@ class Tests(object): self.assertEqual(ia.payment_id(), self.pid) self.assertEqual(str(ia), self.iaddr) + @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_recognition_and_comparisons(self): a = Address(self.addr) a2 = address(self.addr) @@ -58,12 +59,20 @@ class Tests(object): 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) - self.assertEqual(a2.is_mainnet(), self.mainnet) - self.assertEqual(a2.is_testnet(), self.testnet) - self.assertEqual(a2.is_stagenet(), self.stagenet) + self.assertEqual(a.net, self.net) + with pytest.deprecated_call(): + self.assertEqual(a.is_mainnet(), self.net == const.NET_MAIN) + with pytest.deprecated_call(): + self.assertEqual(a.is_testnet(), self.net == const.NET_TEST) + with pytest.deprecated_call(): + self.assertEqual(a.is_stagenet(), self.net == const.NET_STAGE) + self.assertEqual(a2.net, self.net) + with pytest.deprecated_call(): + self.assertEqual(a2.is_mainnet(), self.net == const.NET_MAIN) + with pytest.deprecated_call(): + self.assertEqual(a2.is_testnet(), self.net == const.NET_TEST) + with pytest.deprecated_call(): + self.assertEqual(a2.is_stagenet(), self.net == const.NET_STAGE) ia = IntegratedAddress(self.iaddr) ia2 = address(self.iaddr) @@ -72,12 +81,20 @@ class Tests(object): 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) - self.assertEqual(ia2.is_mainnet(), self.mainnet) - self.assertEqual(ia2.is_testnet(), self.testnet) - self.assertEqual(ia2.is_stagenet(), self.stagenet) + self.assertEqual(ia.net, self.net) + with pytest.deprecated_call(): + self.assertEqual(ia.is_mainnet(), self.net == const.NET_MAIN) + with pytest.deprecated_call(): + self.assertEqual(ia.is_testnet(), self.net == const.NET_TEST) + with pytest.deprecated_call(): + self.assertEqual(ia.is_stagenet(), self.net == const.NET_STAGE) + self.assertEqual(ia2.net, self.net) + with pytest.deprecated_call(): + self.assertEqual(ia2.is_mainnet(), self.net == const.NET_MAIN) + with pytest.deprecated_call(): + self.assertEqual(ia2.is_testnet(), self.net == const.NET_TEST) + with pytest.deprecated_call(): + self.assertEqual(ia2.is_stagenet(), self.net == const.NET_STAGE) self.assertEqual(ia2.base_address(), a) self.assertEqual(ia.view_key(), a.view_key()) @@ -90,12 +107,20 @@ class Tests(object): 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) - self.assertEqual(sa2.is_mainnet(), self.mainnet) - self.assertEqual(sa2.is_testnet(), self.testnet) - self.assertEqual(sa2.is_stagenet(), self.stagenet) + self.assertEqual(sa.net, self.net) + with pytest.deprecated_call(): + self.assertEqual(sa.is_mainnet(), self.net == const.NET_MAIN) + with pytest.deprecated_call(): + self.assertEqual(sa.is_testnet(), self.net == const.NET_TEST) + with pytest.deprecated_call(): + self.assertEqual(sa.is_stagenet(), self.net == const.NET_STAGE) + self.assertEqual(sa2.net, self.net) + with pytest.deprecated_call(): + self.assertEqual(sa2.is_mainnet(), self.net == const.NET_MAIN) + with pytest.deprecated_call(): + self.assertEqual(sa2.is_testnet(), self.net == const.NET_TEST) + with pytest.deprecated_call(): + self.assertEqual(sa2.is_stagenet(), self.net == const.NET_STAGE) self.assertNotEqual(a, 0) @@ -177,9 +202,7 @@ class AddressTestCase(Tests, unittest.TestCase): pid = '4a6f686e47616c74' iaddr = '4HMcpBpe4ddJEEnFKUJHAYhGxkeTRH82sf36giEp9AcNfDBfkAtRLX7A6rZz18bbNHPNV7ex6WYbMN3aKisFRJZ8M7yKhzQhKW3ECCLWQw' subaddr = '84LooD7i35SFppgf4tQ453Vi3q5WexSUXaVgut69ro8MFnmHwuezAArEZTZyLr9fS6QotjqkSAxSF6d1aDgsPoX849izJ7m' - mainnet = True - testnet = False - stagenet = False + net = const.NET_MAIN addr_invalid = '47ewoP19TN7JCEnFKUJHAYhGxkeTRH82sf36giEp9AcNfDBfkAtRLX7A6rZz18bbNHPNV7ex6WYbMN3aKisFRJZ8Ebsmgef' iaddr_invalid = '4HMcpBpe4ddJEEnFKUJHAYhGxkyTRH82sf36giEp9AcNfDBfkAtRLX7A6rZz18bbNHPNV7ex6WYbMN3aKisFRJZ8M7yKhzQhKW3ECCLWQw' @@ -193,9 +216,7 @@ class TestnetAddressTestCase(Tests, unittest.TestCase): pid = '4a6f686e47616c74' subaddr = 'BaU3yLuDqdcETYzeF7vFSVEKNR4sSGxBV1Evrw5yNBf2VMiuAwfDmiF3RHqLHkaA5A6RGiNNRUqvtaqhMtdjA1SQ1tnQV8D' iaddr = 'A7bzU6hSszTEsMp2fYzJiVahyhU2aZi1oZ6R6fK5U64uRa1Pxi8diZh2S1GJFqYXRRhcbfzfWiPD819zKEZkXTMwZqGSmLeBXqMEBnZVkh' - mainnet = False - testnet = True - stagenet = False + net = const.NET_TEST addr_invalid = '9wuKTHsxGiwEsMp3fYzJiVahyhU2aZi1oZ6R6fK5U64uRa1Pxi8diZh2S1GJFqYXRRhcbfzfWiPD819zKEZkXTMwP7hMs5N' iaddr_invalid = 'A7bzU6hSszTEsMp2fYzJiVahyhU2aZi2oZ6R6fK5U64uRa1Pxi8diZh2S1GJFqYXRRhcbfzfWiPD819zKEZkXTMwZqGSmLeBXqMEBnZVkh' @@ -209,9 +230,7 @@ class StagenetAddressTestCase(Tests, unittest.TestCase): pid = '4a6f686e47616c74' subaddr = '7AeQwvrLtPeYoXVPRkEu8oEL7N9wnqHjYKwSvTf6YKbHgYmw6AJMsjggzVLo21egMK9qcoV1mxCTfP4FbaGb7JEMDfpLetk' iaddr = '5CSfuyzxyAV3xPL3JsQxGP74LDuV6E1LS8Zda1PbdqQjGzFmH6N9ep9McbFKMALujVT9S5mKpbEgC5VPhfoAiVj8Vz8ySmoqYgTE8dR1yS' - mainnet = False - testnet = False - stagenet = True + net = const.NET_STAGE addr_invalid = '52jzuBBUMty3xPL3JsQxGP74LDuV6H1LS8Zda1PbdqQjGzFmH6N9ep9McbFKMALujVT9S5mKpbEgC5VPhfoAiVj8LdAqbp6' iaddr_invalid = '5CSfuyzxyAV3xPL3JsQxGP74LDuV6E1LS8Zda1PbdqQjGzFmH6N9ep9McbFKMALujVT9S5mKppEgC5VPhfoAiVj8Vz8ySmoqYgTE8dR1yS' diff --git a/tests/test_offline.py b/tests/test_offline.py index 5d2b82a..53d9284 100644 --- a/tests/test_offline.py +++ b/tests/test_offline.py @@ -1,6 +1,8 @@ +import pytest +import unittest + from monero.backends.offline import OfflineWallet, WalletIsOffline from monero.wallet import Wallet -import unittest from .base import JSONTestCase diff --git a/tests/test_seed.py b/tests/test_seed.py index 3008c02..fc4835d 100644 --- a/tests/test_seed.py +++ b/tests/test_seed.py @@ -78,9 +78,9 @@ class SeedTestCase(unittest.TestCase): '44cWztNFdAqNnycvZbUoj44vsbAEmKnx9aNgkjHdjtMsBrSeKiY8J4s2raH7EMawA2Fwo9utaRTV7Aw8EcTMNMxhH4YtKdH') self.assertIsInstance(seed.public_address(), Address) self.assertEqual( - seed.public_address(net='stagenet'), + seed.public_address(net='stage'), '54pZ5jHDGmwNnycvZbUoj44vsbAEmKnx9aNgkjHdjtMsBrSeKiY8J4s2raH7EMawA2Fwo9utaRTV7Aw8EcTMNMxhH6cuARW') - self.assertIsInstance(seed.public_address(net='stagenet'), Address) + self.assertIsInstance(seed.public_address(net='stage'), Address) seed = Seed("dwelt idols lopped blender haggled rabbits piloted value swagger taunts toolbox upgrade swagger") self.assertTrue(seed.is_mymonero()) @@ -100,7 +100,6 @@ class SeedTestCase(unittest.TestCase): def test_languages(self): for wordlist in list_wordlists(): - print("Language: {}".format(wordlist)) # Generate random seed seed = Seed(wordlist=wordlist) diff --git a/utils/transfer.py b/utils/transfer.py index 2d82ceb..9847e2c 100755 --- a/utils/transfer.py +++ b/utils/transfer.py @@ -42,7 +42,7 @@ argsparser.add_argument('--save', dest='outdir', nargs='?', default=None, const= argsparser.add_argument('destinations', metavar='address:amount', nargs='+', type=destpair, help="Destination address and amount (one or more pairs)") args = argsparser.parse_args() -prio = getattr(monero.prio, args.prio.upper()) +prio = getattr(monero.const, "PRIO_{:s}".format(args.prio.upper())) level = logging.WARNING if args.verbosity == 1: