def test_get_addresses_error_step_zero(self): """ Providing a ``step`` value of 0 to ``get_addresses``. """ ag = AddressGenerator(seed=b'') with self.assertRaises(ValueError): ag.get_addresses(start=0, step=0)
def test_get_addresses_error_count_too_small(self): """ Providing a ``count`` value less than 1 to ``get_addresses``. :py:class:`AddressGenerator` can potentially generate an infinite number of addresses, so there is no "end" to offset against. """ ag = AddressGenerator(seed=b'') with self.assertRaises(ValueError): ag.get_addresses(start=0, count=0)
def test_get_addresses_multiple(self): """ Generating multiple addresses in one go. """ # Seed is not important for this test; it is only used by # :py:class:`KeyGenerator`, which we will mock in this test. ag = AddressGenerator(seed=b'') # noinspection PyUnresolvedReferences with patch.object(ag, '_get_digest', self._mock_get_digest): addresses = ag.get_addresses(start=1, count=2) self.assertListEqual(addresses, [self.addy1, self.addy2])
def test_local_cache(self): """ Installing a cache only for a single instance of :py:class:`AddressGenerator`. """ mock_generate_address = Mock(return_value=self.addy) with patch( 'cornode.crypto.addresses.AddressGenerator._generate_address', mock_generate_address, ): generator1 = AddressGenerator(Seed.random()) # Install the cache locally. generator1.cache = MemoryAddressCache() addy1 = generator1.get_addresses(42) mock_generate_address.assert_called_once() # The second time we try to generate the same address, it is # fetched from the cache. addy2 = generator1.get_addresses(42) mock_generate_address.assert_called_once() self.assertEqual(addy2, addy1) # Create a new instance to verify it has its own cache. generator2 = AddressGenerator(generator1.seed) # The generator has its own cache instance, so even though the # resulting address is the same, it is not fetched from cache. addy3 = generator2.get_addresses(42) self.assertEqual(mock_generate_address.call_count, 2) self.assertEqual(addy3, addy1)
def test_generator_with_offset(self): """ Creating a generator that starts at an offset greater than 0. """ # Seed is not important for this test; it is only used by # :py:class:`KeyGenerator`, which we will mock in this test. ag = AddressGenerator(seed=b'') # noinspection PyUnresolvedReferences with patch.object(ag, '_get_digest', self._mock_get_digest): generator = ag.create_iterator(start=1, step=2) self.assertEqual(next(generator), self.addy1) self.assertEqual(next(generator), self.addy3)
def _execute(self, request): inclusion_states = request['inclusionStates'] # type: bool seed = request['seed'] # type: Seed start = request['start'] # type: int stop = request['stop'] # type: Optional[int] # Determine the addresses we will be scanning, and pull their # transaction hashes. if stop is None: my_hashes = list( chain(*(hashes for _, hashes in iter_used_addresses( self.adapter, seed, start)))) else: ft_response =\ FindTransactionsCommand(self.adapter)( addresses = AddressGenerator(seed).get_addresses(start, stop - start), ) my_hashes = ft_response['hashes'] return { 'bundles': get_bundles_from_transaction_hashes( adapter=self.adapter, transaction_hashes=my_hashes, inclusion_states=inclusion_states, ), }
def test_global_cache(self): """ Installing a cache that affects all :py:class:`AddressGenerator` instances. """ # Install the cache globally. AddressGenerator.cache = MemoryAddressCache() mock_generate_address = Mock(return_value=self.addy) with patch( 'cornode.crypto.addresses.AddressGenerator._generate_address', mock_generate_address, ): generator1 = AddressGenerator(Seed.random()) addy1 = generator1.get_addresses(42) mock_generate_address.assert_called_once() # The second time we try to generate the same address, it is # fetched from the cache. addy2 = generator1.get_addresses(42) mock_generate_address.assert_called_once() self.assertEqual(addy2, addy1) # Create a new AddressGenerator and verify it uses the same # cache. generator2 = AddressGenerator(generator1.seed) # Cache is global, so the cached address is returned again. addy3 = generator2.get_addresses(42) mock_generate_address.assert_called_once() self.assertEqual(addy3, addy1)
def _execute(self, request): stop = request['stop'] # type: Optional[int] seed = request['seed'] # type: Seed start = request['start'] # type: int threshold = request['threshold'] # type: Optional[int] # Determine the addresses we will be scanning. if stop is None: addresses =\ [addy for addy, _ in iter_used_addresses(self.adapter, seed, start)] else: addresses = AddressGenerator(seed).get_addresses(start, stop) if addresses: # Load balances for the addresses that we generated. gb_response = GetBalancesCommand(self.adapter)(addresses=addresses) else: gb_response = {'balances': []} result = { 'inputs': [], 'totalBalance': 0, } threshold_met = threshold is None for i, balance in enumerate(gb_response['balances']): addresses[i].balance = balance if balance: result['inputs'].append(addresses[i]) result['totalBalance'] += balance if (threshold is not None) and (result['totalBalance'] >= threshold): threshold_met = True break if threshold_met: return result else: # This is an exception case, but note that we attach the result # to the exception context so that it can be used for # troubleshooting. raise with_context( exc=BadApiResponse( 'Accumulated balance {balance} is less than threshold {threshold} ' '(``exc.context`` contains more information).'.format( threshold=threshold, balance=result['totalBalance'], ), ), context={ 'inputs': result['inputs'], 'request': request, 'total_balance': result['totalBalance'], }, )
def _find_addresses(self, seed, index, count): """ Find addresses matching the command parameters. """ # type: (Seed, int, Optional[int]) -> List[Address] generator = AddressGenerator(seed) if count is None: # Connect to Tangle and find the first address without any # transactions. for addy in generator.create_iterator(start=index): response = FindTransactionsCommand( self.adapter)(addresses=[addy]) if not response.get('hashes'): return [addy] return generator.get_addresses(start=index, count=count)
def test_get_addresses_single(self): """ Generating a single address. """ # Seed is not important for this test; it is only used by # :py:class:`KeyGenerator`, which we will mock in this test. ag = AddressGenerator(seed=b'') # noinspection PyUnresolvedReferences with patch.object(ag, '_get_digest', self._mock_get_digest): addresses = ag.get_addresses(start=0) self.assertListEqual(addresses, [self.addy0]) # noinspection PyUnresolvedReferences with patch.object(ag, '_get_digest', self._mock_get_digest): # You can provide any positive integer as the ``start`` value. addresses = ag.get_addresses(start=2) self.assertListEqual(addresses, [self.addy2])
def test_get_addresses_step_negative(self): """ Providing a negative ``step`` value to ``get_addresses``. This is probably a weird use case, but what the heck. """ # Seed is not important for this test; it is only used by # :py:class:`KeyGenerator`, which we will mock in this test. ag = AddressGenerator(seed=b'') # noinspection PyUnresolvedReferences with patch.object(ag, '_get_digest', self._mock_get_digest): addresses = ag.get_addresses(start=1, count=2, step=-1) self.assertListEqual( addresses, # This is the same as ``ag.get_addresses(start=0, count=2)``, but # the order is reversed. [self.addy1, self.addy0], )
def test_cache_miss_seed(self): """ Cached addresses are keyed by seed. """ AddressGenerator.cache = MemoryAddressCache() mock_generate_address = Mock(return_value=self.addy) with patch( 'cornode.crypto.addresses.AddressGenerator._generate_address', mock_generate_address, ): generator1 = AddressGenerator(Seed.random()) generator1.get_addresses(42) mock_generate_address.assert_called_once() generator2 = AddressGenerator(Seed.random()) generator2.get_addresses(42) self.assertEqual(mock_generate_address.call_count, 2)
def test_address_from_digest(self): """ Generating an address from a private key digest. """ digest =\ Digest( trytes = b'ABQXVJNER9MPMXMBPNMFBMDGTXRWSYHNZKGAGUOI' b'JKOJGZVGHCUXXGFZEMMGDSGWDCKJXO9ILLFAKGGZE', key_index = 0, ) self.assertEqual( AddressGenerator.address_from_digest(digest), Address(b'QLOEDSBXXOLLUJYLEGKEPYDRIJJTPIMEPKMFHUVJ' b'MPMLYYCLPQPANEVDSERQWPVNHCAXYRLAYMBHJLWWR'), )
def _execute(self, request): inclusion_states = request['inclusionStates'] # type: bool seed = request['seed'] # type: Seed start = request['start'] # type: int stop = request['stop'] # type: Optional[int] if stop is None: my_addresses = [] # type: List[Address] my_hashes = [] # type: List[TransactionHash] for addy, hashes in iter_used_addresses(self.adapter, seed, start): my_addresses.append(addy) my_hashes.extend(hashes) else: ft_command = FindTransactionsCommand(self.adapter) my_addresses = AddressGenerator(seed).get_addresses( start, stop - start) my_hashes = ft_command(addresses=my_addresses).get('hashes') or [] account_balance = 0 if my_hashes: # Load balances for the addresses that we generated. gb_response = GetBalancesCommand( self.adapter)(addresses=my_addresses) for i, balance in enumerate(gb_response['balances']): my_addresses[i].balance = balance account_balance += balance return { 'addresses': list(sorted(my_addresses, key=attrgetter('key_index'))), 'balance': account_balance, 'bundles': get_bundles_from_transaction_hashes( adapter=self.adapter, transaction_hashes=my_hashes, inclusion_states=inclusion_states, ), }
def iter_used_addresses(adapter, seed, start): # type: (BaseAdapter, Seed, int) -> Generator[Tuple[Address, List[TransactionHash]]] """ Scans the Tangle for used addresses. This is basically the opposite of invoking ``getNewAddresses`` with ``stop=None``. """ ft_command = FindTransactionsCommand(adapter) for addy in AddressGenerator(seed).create_iterator(start): ft_response = ft_command(addresses=[addy]) if ft_response['hashes']: yield addy, ft_response['hashes'] else: break # Reset the command so that we can call it again. ft_command.reset()
def get_address(): generator = AddressGenerator(seed) generated.extend(generator.get_addresses(0))