diff --git a/monero/address.py b/monero/address.py index 1e377ce..7b2dc92 100644 --- a/monero/address.py +++ b/monero/address.py @@ -4,6 +4,9 @@ from sha3 import keccak_256 from . import base58 class Address(object): + _valid_netbytes = (18, 53) + # NOTE: _valid_netbytes order is (real, testnet) + def __init__(self, address): address = str(address) if len(address) != 95: @@ -15,9 +18,13 @@ class Address(object): checksum = self._decoded[-4:] if checksum != keccak_256(self._decoded[:-4]).digest()[:4]: raise ValueError("Invalid checksum") + if self._decoded[0] not in self._valid_netbytes: + raise ValueError("Invalid address netbyte {nb}. Allowed values are: {allowed}".format( + nb=hexlify(bytes(self._decoded[0])), + allowed=", ".join(map(lambda b: '%02x' % b, self._valid_netbytes)))) def is_testnet(self): - return self._decoded[0] in bytes([53, 54]) + return self._decoded[0] == self._valid_netbytes[1] def get_view_key(self): return hexlify(self._decoded[33:65]).decode() @@ -46,7 +53,16 @@ class Address(object): return super() +class SubAddress(Address): + _valid_netbytes = (42, 63) + + def with_payment_id(self): + raise TypeError("SubAddress cannot be merged with payment ID into IntegratedAddress") + + class IntegratedAddress(Address): + _valid_netbytes = (19, 54) + def __init__(self, address): address = str(address) if len(address) != 106: @@ -66,7 +82,16 @@ class IntegratedAddress(Address): def address(addr): addr = str(addr) if len(addr) == 95: - return Address(addr) + netbyte = unhexlify(base58.decode(addr))[0] + if netbyte in Address._valid_netbytes: + return Address(addr) + elif netbyte in SubAddress._valid_netbytes: + return SubAddress(addr) + raise ValueError("Invalid address netbyte {nb}. Allowed values are: {allowed}".format( + nb=hexlify(self._decoded[0]), + allowed=", ".join(map( + lambda b: '%02x' % b, + sorted(Address._valid_netbytes + SubAddress._valid_netbytes))))) elif len(addr) == 106: return IntegratedAddress(addr) raise ValueError("Address must be either 95 or 106 characters long") diff --git a/monero/backends/jsonrpc.py b/monero/backends/jsonrpc.py index c3502c1..80b0c50 100644 --- a/monero/backends/jsonrpc.py +++ b/monero/backends/jsonrpc.py @@ -7,7 +7,7 @@ import requests from .. import exceptions from ..account import Account -from ..address import Address +from ..address import address from ..numbers import from_atomic, to_atomic _log = logging.getLogger(__name__) @@ -79,7 +79,7 @@ class JSONRPC(object): data = { 'account_index': account, 'destinations': list(map( - lambda dst: {'address': str(Address(dst[0])), 'amount': to_atomic(dst[1])}, + lambda dst: {'address': str(address(dst[0])), 'amount': to_atomic(dst[1])}, destinations)), 'mixin': mixin, 'priority': priority, diff --git a/tests/address.py b/tests/address.py index 0fe7856..4103685 100644 --- a/tests/address.py +++ b/tests/address.py @@ -1,6 +1,6 @@ import unittest -from monero.address import Address, IntegratedAddress, address +from monero.address import Address, SubAddress, IntegratedAddress, address class Tests(object): def test_from_and_to_string(self): @@ -16,6 +16,9 @@ class Tests(object): self.assertEqual(ia.get_view_key(), self.pvk) self.assertEqual(ia.get_base_address(), a) + sa = SubAddress(self.subaddr) + self.assertEqual(str(sa), self.subaddr) + def test_payment_id(self): a = Address(self.addr) ia = a.with_payment_id(self.pid) @@ -43,6 +46,15 @@ class Tests(object): self.assertEqual(ia2.is_testnet(), self.testnet) self.assertEqual(ia2.get_base_address(), a) + sa = SubAddress(self.subaddr) + sa2 = address(self.subaddr) + self.assertIsInstance(sa2, SubAddress) + self.assertEqual(sa, sa2) + self.assertEqual(sa, self.subaddr) + self.assertEqual(self.subaddr, sa) + self.assertEqual(sa.is_testnet(), self.testnet) + self.assertEqual(sa2.is_testnet(), self.testnet) + def test_idempotence(self): a = Address(self.addr) a_idem = Address(a) @@ -60,19 +72,41 @@ class Tests(object): ia_idem = address(ia) self.assertEqual(ia, ia_idem) + def test_invalid(self): + self.assertRaises(ValueError, Address, self.addr_invalid) + self.assertRaises(ValueError, Address, self.iaddr_invalid) + + def test_type_mismatch(self): + self.assertRaises(ValueError, Address, self.iaddr) + self.assertRaises(ValueError, Address, self.subaddr) + self.assertRaises(ValueError, IntegratedAddress, self.addr) + self.assertRaises(ValueError, IntegratedAddress, self.subaddr) + self.assertRaises(ValueError, SubAddress, self.addr) + self.assertRaises(ValueError, SubAddress, self.iaddr) + + def test_subaddress_cannot_into_integrated(self): + sa = SubAddress(self.subaddr) + self.assertRaises(TypeError, sa.with_payment_id, self.pid) + class AddressTestCase(unittest.TestCase, Tests): addr = '43aeKax1ts4BoEbSyzKVbbDRmc8nsnpZLUpQBYvhUxs3KVrodnaFaBEQMDp69u4VaiEG3LSQXA6M61mXPrztCLuh7PFUAmd' psk = '33a7ceb933b793408d49e82c0a34664a4be7117243cb77a64ef280b866d8aa6e' pvk = '96f70d63d9d3558b97a5dd200a170b4f45b3177a274aa90496ea683896ff6438' pid = '4a6f686e47616c74' + subaddr = '83bK2pMxCQXdRyd6W1haNWYRsF6Qb3iGa8gxKEynm9U7cYoXrMHFwRrFFuxRSgnLtGe7LM8SmrPY6L3TVBa3UV3YLuVJ7Rw' iaddr = '4DHKLPmWW8aBoEbSyzKVbbDRmc8nsnpZLUpQBYvhUxs3KVrodnaFaBEQMDp69u4VaiEG3LSQXA6M61mXPrztCLuhAR6GpL18QNwE8h3TuF' testnet = False + addr_invalid = '43aeKax1ts4boEbSyzKVbbDRmc8nsnpZLUpQBYvhUxs3KVrodnaFaBEQMDp69u4VaiEG3LSQXA6M61mXPrztCLuh7PFUAmd' + iaddr_invalid = '4DHKLpmWW8aBoEbSyzKVbbDRmc8nsnpZLUpQBYvhUxs3KVrodnaFaBEQMDp69u4VaiEG3LSQXA6M61mXPrztCLuhAR6GpL18QNwE8h3TuF' class TestnetAddressTestCase(AddressTestCase, Tests): - addr = '9u9j6xG1GNu4ghrdUL35m5PQcJV69YF8731DSTDoh7pDgkBWz2LWNzncq7M5s1ARjPRhvGPX4dBUeC3xNj4wzfrjV6SY3e9' - psk = '345b201b8d1ba216074e3c45ca606c85f68563f60d0b8c0bfab5123f80692aed' - pvk = '9deb70cc7e1e23d635de2d5a3086a293b4580dc2b9133b4211bc09f22fadc4f9' + addr = '9vgV48wWAPTWik5QSUSoGYicdvvsbSNHrT9Arsx1XBTz6VrWPSgfmnUKSPZDMyX4Ms8R9TkhB4uFqK9s5LUBbV6YQN2Q9ag' + psk = '5cbcfbcea7cc62b1aeb76758ad8df5f8cbe0c63d40c8cd9c49377bbc9c9b9520' + pvk = 'de048ca310ff7d6e3b6714bccdebd62c56d680a10272846c875241fa2c5fc1cf' pid = '4a6f686e47616c74' - iaddr = 'A4rQ7m5VseR4ghrdUL35m5PQcJV69YF8731DSTDoh7pDgkBWz2LWNzncq7M5s1ARjPRhvGPX4dBUeC3xNj4wzfrjihS6W83Km1mE7W3kMa' + iaddr = 'A6PA4wkzmeyWik5QSUSoGYicdvvsbSNHrT9Arsx1XBTz6VrWPSgfmnUKSPZDMyX4Ms8R9TkhB4uFqK9s5LUBbV6YbfyqvDecDn3E7cvp9b' + subaddr = 'BbBjyYoYNNwFfL8RRVRTMiZUofBLpjRxdNnd5E4LyGcAK5CEsnL3gmE5QkrDRta7RPficGHcFdR6rUwWcjnwZVvCE3tLxhJ' testnet = True + addr_invalid = '9vgV48wWAPTWik5QSUSoGYicdvvsbSNHrT9Arsx1XBTz6VrWPSgfmnUKSPZDMyX4Ms8R9TkhB4uFqK9s5LUbbV6YQN2Q9ag' + iaddr_invalid = 'A6PA4wkzmeyWik5qSUSoGYicdvvsbSNHrT9Arsx1XBTz6VrWPSgfmnUKSPZDMyX4Ms8R9TkhB4uFqK9s5LUBbV6YbfyqvDecDn3E7cvp9b'