diff --git a/setup.py b/setup.py index 9a019a9..827233e 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ name = 'PyOTA', description = 'IOTA API library for Python', url = 'https://github.com/iotaledger/iota.lib.py', - version = '1.0.0b4', + version = '1.0.0b5', packages = find_packages('src'), include_package_data = True, diff --git a/src/iota/api.py b/src/iota/api.py index db2b540..1797473 100644 --- a/src/iota/api.py +++ b/src/iota/api.py @@ -461,22 +461,23 @@ def get_latest_inclusion(self, hashes): """ return self.getLatestInclusion(hashes=hashes) - def get_new_addresses(self, index=None, count=1): - # type: (Optional[int], Optional[int]) -> List[Address] + def get_new_addresses(self, index=0, count=1): + # type: (int, Optional[int]) -> List[Address] """ Generates one or more new addresses from the seed. :param index: Specify the index of the new address (must be >= 1). - If not provided, the address will be generated deterministically. - :param count: Number of addresses to generate (must be >= 1). Note: This is more efficient than calling ``get_new_address`` inside a loop. + If ``None``, this method will scan the Tangle to find the next + available unused address and return that. + :return: List of generated addresses. diff --git a/src/iota/commands/extended/get_new_addresses.py b/src/iota/commands/extended/get_new_addresses.py index 5a194b3..3a1b712 100644 --- a/src/iota/commands/extended/get_new_addresses.py +++ b/src/iota/commands/extended/get_new_addresses.py @@ -35,7 +35,7 @@ def _execute(self, request): index = request.get('index') # Required parameters. - seed = request['seed'] + seed = request['seed'] generator = AddressGenerator(seed) diff --git a/src/iota/crypto/signing.py b/src/iota/crypto/signing.py index c4c6247..f4b4946 100644 --- a/src/iota/crypto/signing.py +++ b/src/iota/crypto/signing.py @@ -8,7 +8,7 @@ from iota import TRITS_PER_TRYTE, TryteString, TrytesCompatible, Hash from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH -from iota.crypto.types import PrivateKey +from iota.crypto.types import PrivateKey, Seed from iota.exceptions import with_context __all__ = [ @@ -68,7 +68,7 @@ def __init__(self, seed): # type: (TrytesCompatible) -> None super(KeyGenerator, self).__init__() - self.seed = TryteString(seed) + self.seed = Seed(seed) def get_keys(self, start, count=1, step=1, iterations=1): # type: (int, int, int, int) -> List[PrivateKey] diff --git a/src/iota/crypto/types.py b/src/iota/crypto/types.py index 7e10dca..af78a56 100644 --- a/src/iota/crypto/types.py +++ b/src/iota/crypto/types.py @@ -87,14 +87,15 @@ def get_digest_trits(self): """ hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN - digest = [0] * HASH_LENGTH + key_chunks = self.iter_chunks(FRAGMENT_LENGTH) - for (i, fragment) in enumerate(self.iter_chunks(FRAGMENT_LENGTH)): # type: Tuple[int, TryteString] - fragment_start = i * FRAGMENT_LENGTH - fragment_end = fragment_start + FRAGMENT_LENGTH - fragment_trits = fragment[fragment_start:fragment_end].as_trits() + # The digest will contain one hash per key fragment. + digest = [0] * HASH_LENGTH * len(key_chunks) + + for (i, fragment) in enumerate(key_chunks): # type: Tuple[int, TryteString] + fragment_trits = fragment.as_trits() - key_fragment = [0] * len(fragment_trits) + key_fragment = [0] * FRAGMENT_LENGTH hash_trits = [] for j in range(hashes_per_fragment): @@ -113,6 +114,9 @@ def get_digest_trits(self): sponge.absorb(key_fragment) sponge.squeeze(hash_trits) + fragment_start = i * FRAGMENT_LENGTH + fragment_end = fragment_start + FRAGMENT_LENGTH + digest[fragment_start:fragment_end] = hash_trits return digest diff --git a/src/iota/transaction.py b/src/iota/transaction.py index 5e2e920..bddb5b6 100644 --- a/src/iota/transaction.py +++ b/src/iota/transaction.py @@ -15,6 +15,7 @@ validate_signature_fragments from iota.exceptions import with_context from iota.json import JsonSerializable +from six import PY2 __all__ = [ 'Bundle', @@ -127,43 +128,43 @@ def __init__( branch_transaction_hash, nonce, ): - # type: (Optional[TransactionHash], Optional[TryteString], Address, int, Tag, int, Optional[int], Optional[int], Optional[BundleHash], Optional[TransactionHash], Optional[TransactionHash], Optional[Hash]) -> None - self.hash = hash_ + # type: (Optional[TransactionHash], Optional[Fragment], Address, int, Optional[Tag], int, Optional[int], Optional[int], Optional[BundleHash], Optional[TransactionHash], Optional[TransactionHash], Optional[Hash]) -> None + self.hash = hash_ # type: Optional[TransactionHash] """ Transaction ID, generated by taking a hash of the transaction trits. """ - self.bundle_hash = bundle_hash + self.bundle_hash = bundle_hash # type: Optional[BundleHash] """ Bundle hash, generated by taking a hash of metadata from all the transactions in the bundle. """ - self.address = address + self.address = address # type: Address """ The address associated with this transaction. If ``value`` is != 0, the associated address' balance is adjusted as a result of this transaction. """ - self.value = value + self.value = value # type: int """ Amount to adjust the balance of ``address``. Can be negative (i.e., for spending inputs). """ - self.tag = tag + self.tag = tag # type: Optional[Tag] """ Optional classification tag applied to this transaction. """ - self.nonce = nonce + self.nonce = nonce # type: Optional[Hash] """ Unique value used to increase security of the transaction hash. """ - self.timestamp = timestamp + self.timestamp = timestamp # type: int """ Timestamp used to increase the security of the transaction hash. @@ -171,7 +172,7 @@ def __init__( Do not rely on it when resolving conflicts! """ - self.current_index = current_index + self.current_index = current_index # type: Optional[int] """ The position of the transaction inside the bundle. @@ -180,12 +181,12 @@ def __init__( last. """ - self.last_index = last_index + self.last_index = last_index # type: Optional[int] """ The position of the final transaction inside the bundle. """ - self.trunk_transaction_hash = trunk_transaction_hash + self.trunk_transaction_hash = trunk_transaction_hash # type: Optional[TransactionHash] """ In order to add a transaction to the Tangle, you must perform PoW to "approve" two existing transactions, called the "trunk" and @@ -195,7 +196,7 @@ def __init__( a bundle. """ - self.branch_transaction_hash = branch_transaction_hash + self.branch_transaction_hash = branch_transaction_hash # type: Optional[TransactionHash] """ In order to add a transaction to the Tangle, you must perform PoW to "approve" two existing transactions, called the "trunk" and @@ -204,7 +205,7 @@ def __init__( The branch transaction generally has no significance. """ - self.signature_message_fragment = signature_message_fragment + self.signature_message_fragment = signature_message_fragment # type: Optional[Fragment] """ "Signature/Message Fragment" (note the slash): @@ -676,12 +677,11 @@ class ProposedBundle(JsonSerializable, Sequence[ProposedTransaction]): A collection of proposed transactions, to be treated as an atomic unit when attached to the Tangle. """ - def __init__(self, transactions=None): - # type: (Optional[Iterable[ProposedTransaction]]) -> None + def __init__(self, transactions=None, inputs=None, change_address=None): + # type: (Optional[Iterable[ProposedTransaction]], Optional[Iterable[Address]], Optional[Address]) -> None super(ProposedBundle, self).__init__() self.hash = None # type: Optional[Hash] - self.tag = None # type: Optional[Tag] self._transactions = [] # type: List[ProposedTransaction] @@ -689,6 +689,22 @@ def __init__(self, transactions=None): for t in transactions: self.add_transaction(t) + if inputs: + self.add_inputs(inputs) + + self.change_address = change_address + + def __bool__(self): + # type: () -> bool + """ + Returns whether this bundle has any transactions. + """ + return bool(self._transactions) + + # :bc: Magic methods have different names in Python 2. + if PY2: + __nonzero__ = __bool__ + def __contains__(self, transaction): # type: (ProposedTransaction) -> bool return transaction in self._transactions @@ -730,6 +746,19 @@ def balance(self): """ return sum(t.value for t in self._transactions) + @property + def tag(self): + # type: () -> Tag + """ + Determines the most relevant tag for the bundle. + """ + for txn in reversed(self): # type: ProposedTransaction + if txn.tag: + # noinspection PyTypeChecker + return txn.tag + + return Tag(b'') + def as_json_compatible(self): # type: () -> List[dict] """ @@ -762,6 +791,9 @@ def add_transaction(self, transaction): if self.hash: raise RuntimeError('Bundle is already finalized.') + if transaction.value < 0: + raise ValueError('Use ``add_inputs`` to add inputs to the bundle.') + self._transactions.append(ProposedTransaction( address = transaction.address, value = transaction.value, @@ -770,9 +802,6 @@ def add_transaction(self, transaction): timestamp = transaction.timestamp, )) - # Last-added transaction determines the bundle tag. - self.tag = transaction.tag or self.tag - # If the message is too long to fit in a single transactions, # it must be split up into multiple transactions so that it will # fit. @@ -835,7 +864,7 @@ def add_inputs(self, inputs): ) # Add the input as a transaction. - self.add_transaction(ProposedTransaction( + self._transactions.append(ProposedTransaction( address = addy, tag = self.tag, @@ -848,7 +877,7 @@ def add_inputs(self, inputs): # transaction length limit. # Subtract 1 to account for the transaction we just added. for _ in range(AddressGenerator.DIGEST_ITERATIONS - 1): - self.add_transaction(ProposedTransaction( + self._transactions.append(ProposedTransaction( address = addy, tag = self.tag, @@ -867,16 +896,7 @@ def send_unspent_inputs_to(self, address): if self.hash: raise RuntimeError('Bundle is already finalized.') - # Negative balance means that there are unspent inputs. - # See :py:meth:`balance` for more info. - unspent_inputs = -self.balance - - if unspent_inputs > 0: - self.add_transaction(ProposedTransaction( - address = address, - value = unspent_inputs, - tag = self.tag, - )) + self.change_address = address def finalize(self): # type: () -> None @@ -886,21 +906,34 @@ def finalize(self): if self.hash: raise RuntimeError('Bundle is already finalized.') + if not self: + raise ValueError('Bundle has no transactions.') + # Quick validation. balance = self.balance - if balance > 0: + + if balance < 0: + if self.change_address: + self.add_transaction(ProposedTransaction( + address = self.change_address, + value = -balance, + tag = self.tag, + )) + else: + raise ValueError( + 'Bundle has unspent inputs (balance: {balance}); ' + 'use ``send_unspent_inputs_to`` to create ' + 'change transaction.'.format( + balance = balance, + ), + ) + elif balance > 0: raise ValueError( 'Inputs are insufficient to cover bundle spend ' '(balance: {balance}).'.format( balance = balance, ), ) - elif balance < 0: - raise ValueError( - 'Bundle has unspent inputs (balance: {balance}).'.format( - balance = balance, - ), - ) # Generate bundle hash. sponge = Curl() diff --git a/test/crypto/types_test.py b/test/crypto/types_test.py index 974e769..b52ece0 100644 --- a/test/crypto/types_test.py +++ b/test/crypto/types_test.py @@ -10,11 +10,12 @@ # noinspection SpellCheckingInspection class PrivateKeyTestCase(TestCase): - def test_get_digest_trits(self): + def test_get_digest_trits_single_fragment(self): """ - Generating digest trits from a valid PrivateKey. + Generating digest trits from a PrivateKey 1 fragment long. """ - key = PrivateKey( + key =\ + PrivateKey( b'BWFTZWBZVFOSQYHQFXOPYTZ9SWB9RYYHBOUA9NOYSWGALF9MSVNEDW9A9FLGBRWKED' b'MPEIPRKBMRXRLLFJCAGVIMXPISRGXIJQ9BOBHKJEUKDEUUWYXJGCGAWHYBQHBPMRTZ' b'FPBGNLMKPZYXZPXFSPFUWZNRWYXUEWMP9URKVVJOSWEPJKSMPLWZPIZGOTVAA9QQOC' @@ -59,3 +60,90 @@ def test_get_digest_trits(self): b'JKOJGZVGHCUXXGFZEMMGDSGWDCKJXO9ILLFAKGGZE' ), ) + + def test_get_digest_trits_multiple_fragments(self): + """ + Generating digest trits from a PrivateKey longer than 1 fragment. + """ + key =\ + PrivateKey( + b'LXZFBGXJCSEHJFVQOJBSBDWVPNHSTKGMNZQYVFKAJTFSIVMXQIRIQYHRCFVDKYCCVK' + b'VGPRZRZUXXUIV9ZPJSWXZ9FUHBVYFGNWTGMX9LXPTALCML9ASKROTEVORSSQZHWEMF' + b'UPNFQCEHXTEALKZVSHHALJXAPMGFASSHHCREWFCSYJFWLSKJZOFLLMTGWSSJZLJQOH' + b'GQAHICAMRHHWZAIAALSUDPWBSRILGBQILJQUGIIOCNDGSYUSXRVPAZFKARRXVBDQLW' + b'YZFR9FKIDUBODORNUXBOKDURIFHBRLTSIWOTUVQNMKXJGWYEVXCTKVQWDXJ9ZPLTIH' + b'DEW9ERC9EFNKCWBNSUXGYLPG9PYERCIHBWNGTHWRSNNYPADKIUWMJVNWALHCCVSMYZ' + b'GFIBFUXULLAJNZYQRLVYZHOQEPEURBITZIF9XWSOTEXEI9DBCM9VZPLURONGEIXMVU' + b'RPZRUSAOLZPYMIXKPV9SWSXSPQSRALB99IVDBFXJC9SPXPUTMKTEKKJMPNC9SFDGIR' + b'BVMRQKGG9FWVZMWSCLGANOQENXZZVHESO9TXRRZGGKVGIJYZKULWSBWYFALOXFWHFI' + b'TTQWIQGJSEYRH9CIYPTYOERARZQVRUTIBR9QTJUIQXZ9FFKGPBBWHUVIGGKSXVBCUP' + b'YGESTDRCVKDJLHNXJUTGUFPOTTTDYVMOOE9DDXAAJVSULTWCQOJBAWKVZSLKGAEYOV' + b'WXNULJZPPJGJJXMIEQWKPHBHWZEUJYOZ9FXMKFT9RMBTXTPAKAWMJTPZVPT9SKCRHU' + b'KOSFPQWLIFQYOVOBISWFKARPKR9JZMQOXHLNZVVUQGZVAWKMI9KZBGELPCCYENZUKX' + b'FVGZJTDZZIPTJESWSFJUSRU9IHGEJXSHBRRSJZAHCK9STAKIDYTVXEVHZFKYXUSTTM' + b'ERPQGBIINLMXHZHVWGFKBMQXULTONGQPLHMUACQSIVCYMHKGSVWEHYARSVECJMWBWW' + b'DLSGVTPZAKCRYXYLEEAHOUNVLNLFPXGNRKAZSSLVTVBDTBVCTEBW9FP9IROYSINOFH' + b'PZNYPHX9CVWDMTYXUFLHR9R9MCI9BGLIBHKCDENREOEPXCLMSYQZDIDES9KIURUSJQ' + b'JL9IEUAOLJYKJTLE9UYVBSPRAGMTQYKOHNWGHXUBYZAOMBBTMGOEXKJEYMWUSSIRDO' + b'IMHNTWWOKHYVDDKOYFTYFOGAZ9MHJVNKRYGFXYEUFBQPMYFCFBAIXXIFHVTPRPDOXS' + b'QVIKTSNONSNQKYIRULCQRMEOKSAXTDGJGSZKUPLWJITS9ZQOLEFPPSDSSA9DHGDJMT' + b'MAETSNDB9BSIVVPYIKD9ESAYOSYJSKJBRJOJOWBEJWSI9PJT9BYJDROWYHAZJBLUJX' + b'GAZGUYJKTRJJIRCRUWRRKVDAKMFGHJQKYDENHJYNLBUTVNKNFYYHEYGPLVGGPLBWZR' + b'HUMEHELSDRJDLRIYORNSLNOWCDBFKPESBQSNSDZUGCCJHLJJEHUWGOYBBAFJNL9QCF' + b'ROADVYXELOOO9OFRM9OMJYS9VZLVUONKTPXJBHKYMUDSYKHUJBJCNRZXQBYYVAHDJS' + b'WOVPGWQUUWKHFWI9PTCKFROSFNSZUKBBNJ9NZWWGXWEMSLTQ9YJFBPBFEACG9WLTK9' + b'MNBNEYDPCLQVRIHMCIBEHNTQOXQRUXKQFCJYHJREBTSYAHDCWJBDVKAKKHULI99BIS' + b'9IVUDVCDFWTUUNFAGEKFGTQBEIYYUCOLIBTUSCNTQCJXZWCHQATRPOTJP9LNCEMSVO' + b'BNGMYVXTJWWLFXPDVNOIVIMBNBPEIGJCONYPLVSHVVDNOPMQMINRXCWFCOSYMDV9S9' + b'ZA9YFXAAVRWXWSAQLYTKKZAZEZCTWVHMBG9HTTJXYJSGBUJZXRPDGHBQIHWQQVRPNN' + b'WAYISDZCLKWZLPLUEF9TKCCA9YBUYQBZFVLVDRZ9AHZAEQNAQ9IQWKKXCMVDDJRBKE' + b'LJHVTLDUUMQLDXYTOWYKPMCQW9YBGD9VUJLCJWQEN9RISZKTNNCDLXVFMNGNBCDWQG' + b'WRLPWKALPJDRDZFCLTONVQUZPUISJRVYOBVCXEICEQRNOTXMWTPTEHAYROEVQNPO9K' + b'IFCIBXHL9MJM9JYOXYDKZEF9DLURXGXPQGVUZQB9EKPLBQXQQYCXBYUBAQVCKUJDJA' + b'CCXQINZTZWMSEBNGBDORPZKMVGGZSPIUKLRAMXGKSIULOJBRBTGXWEVMWQEALSZVLC' + b'MXQIKZIIMLSVRDGIZWBVZUUIKGYXNTFNSLBXEQKBBUOYPLSDCGJPTVGYAGDD9JNVZY' + b'UUXWUQJ9DUPDGWCXOEEVVQPOZLRGYBMPAWGMKNUCMWDIKIHHAPPN9BIKCFHDEORXRX' + b'STBFGBCRDSPNBZDRZHFSMXNXHSKAZF9XYKOWFNTGUUHMUOWXHAROKHZOLWECDVRDXO' + b'RDHANPIYZFETYBAOPPHTMTZMLDLQXZVYGXYKUDUNFWVPESWBFRTOJRRCPMKIBTQHFG' + b'SPATWEXLWRFFQI9RKZBY9RZDVMVZCCWTS9PNRCTP9FRCGPOCVUJZIRZPU9WJPBHDTB' + b'DSZSGI9HYHRBEBRQECBJEIZDXZOGGYZUPSEQLMQBTRQANDWG9RWGXAUYPRBSJRULMS' + b'BJVPIFJWQPWEMY9YHDULM99VOIIDHRJQMHCZQAZCITZPKNSOBGSQDLOVWCZOLQEHBD' + b'VIMDRWWXY9LWHTRBWUASPTMA9UPRILCALBIUFUELQDAKSD9WADUZESCBNNNZCQLXBI' + b'FCNGGUBZTWSKQ9QHQLE9LBLWNKOQJYNSD9SRASRCGLICBCX9VNAGXAP9NFJUGWCZAI' + b'QLBCQKSLFGXDRMEXKAHLMOE9VKFRFKCBLRNZVIARBRKEZMLEKIMMMGEJRLWMJFCLHT' + b'LMXYYVKJFX9M9HIDHLBVFUKHMJRJOCWRHHTGLXDEWTTFGSMUHVOK9VDPOABLMZQGGP' + b'FYIDMQIELYYLOCIXYAASYUFGVQDCEESUEJWOJCNNRMFKHXFCYMLXIEFMAHJRZ9TBZF' + b'BSZOCDJPUPBLNBTNYHIAUYQKLPMMNNDEIPEFUAMTMYMXCENQCIJRWWZASXIHCMYLMB' + b'GYRUDEYCYJPYPOEOAWENQLKMGCFMLVRJYMCHEYWBJZMOLSATAVIORHIEAHVPDEKPZW' + b'KTPPGTTCRWJTDKABHHETSYLXJOQXJSYKMT9OJEEIOKAYWTLIWUTSAYZ9IDNWJLKKOY' + b'VMGCJQIMROINJVPRKUQJCTDLFHZQDAOMDIAJYY9NQ9WZZIKHBFLYXOOWRHDRGJYNCD' + b'BIEYCBAZMQNMNBTLGOBUTBMUDFOKTZ9BCBPOVNJXDHGMGITUMSPKAQ9R9PFCNCKPDP' + b'HZLVTQCSKVJOIEYXUEOOIMWVIKKKNRNENRDFUXP9WJNWXJHBJIXPJQBRNOHOBQGKSY' + b'UKCAMZKLWJETFNZGYIWBLARTNYHWLUBXRBHOO99RECDLSWBOOVJ9OEAWKPRBVWUUK9' + b'ASGWAIS9CIPFAJRXFEDCSSMOXFASWKDZZKRGMBLD9GU9CWWBXW9DEU9IKENDNYEWOP' + b'ZRUJUEK9YMNERGQOXUBGVNIGWDXXUVBKMSQWCGSDSIMKEFOBLIGPFCPNQ9QLNSLK9O' + b'NMGNJMRI9KUQNTSMFGXAGYJVUQO9JDLDBUIQQJIEZCTFJS9VIKIIIDSQFBBAGUGOTA' + b'VO9WZQXKBJSZBZDUTUSZ9GZAZQXTUHYTLCZQLRMURZLTYMNBPE9QFZOUVRRDRAEEAI' + b'9OVJXPEWOWEESPUKMBGGBAYLLTFLBPNM9VVYNWTCRCIHEKKGU9RGQWGKEREBUXTELB' + b'XTP9VQQMPGZHIJFBIWE9GSPTJHCMSKE9HXU9XISYRVCFXSQZO9CSXGAZUBINTCRKFH' + b'OPFXUHDTWAZVMQWRZQQVMUVACIIDETVJLUXY9TEJGMWLGMMJ9SWTFNUHRI9SFVHBSJ' + b'J9MRWCKFRJJJL9DECJL9XDHKWHYHLMSSXRIBBHC9PNWKIIQYITKUHENQWBMNALETH9' + b'F9KIWDJXHPGSGYIKLJNQEZGXZJDKWCNGPFKSATLYNNYRCKCBBCEH9ADIZICTXOUKSS' + b'WAPSHQKIBMOZKIPHLNHDSDVNRMWIFDYATUHFJSZFGYCSRKFERXBPVWUU9WKFPHAZJ9' + b'LQCDSMZJWQGMFTHZNYEFBTNJ9TCNWMEZWQVRYFBVIWNSWDJDVJMFCRRDYLBYECAFW9' + b'KXMX9LRLWLFIGAHBGF9YJODGYKULATHVYBCHUQTDZXKUKGVVJCDBJUJKRDN9EKCTBY' + b'NQMGSIPDCTJVCX9INTPUZ9BXITIPKLHO9ESBOFSWLYNYXPCWRQCEOZTM9UYAAZSDHB' + b'9BTFJNLZA9NTCKSAIZ' + ) + + self.assertEqual( + TryteString.from_trits(key.get_digest_trits()), + + # Note that the digest is 2 hashes long, because the key + # is 2 fragments long. + TryteString( + b'XCLECABPSBFYWJQNKXJAHB9QPJLAIJJBFUQNGUWDNVCVFQWXECVLYXUYHKW9XQECWC' + b'IVNEDMSJWL9PDEQGVKTYZQXPAMHIJGJXQIJJRSLPCVNAUUYJSIDOHYXUNDQYVBPYDD' + b'EZMFJQRAIMPVATMWLFHJXAISQTQYWX' + ), + ) diff --git a/test/transaction_test.py b/test/transaction_test.py index ee27ce4..9b49ec0 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -5,8 +5,13 @@ from typing import Tuple from unittest import TestCase -from iota import Address, Bundle, BundleHash, Fragment, Hash, Tag, \ - Transaction, TransactionHash +from mock import patch + +from iota import Address, Bundle, BundleHash, Fragment, Hash, ProposedBundle, \ + ProposedTransaction, Tag, Transaction, TransactionHash, TryteString, \ + trits_from_int +from iota.crypto.addresses import AddressGenerator +from iota.crypto.signing import KeyGenerator from iota.transaction import BundleValidator from six import binary_type @@ -519,30 +524,127 @@ def test_fail_multiple_errors(self): ) +# noinspection SpellCheckingInspection class ProposedBundleTestCase(TestCase): + def setUp(self): + super(ProposedBundleTestCase, self).setUp() + + self.bundle = ProposedBundle() + def test_add_transaction_short_message(self): """ Adding a transaction to a bundle, with a message short enough to fit inside a single transaction. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999AETEXB' + b'D9YBTH9EMFKF9CAHJIAIKDBEPAMH99DEN9DAJETGN' + ), + + message = TryteString.from_string('Hello, IOTA!'), + value = 42, + )) + + # We can fit the message inside a single fragment, so only one + # transaction is necessary. + self.assertEqual(len(self.bundle), 1) def test_add_transaction_long_message(self): """ Adding a transaction to a bundle, with a message so long that it has to be split into multiple transactions. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + address = Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999N9GIUF' + b'HCFIUGLBSCKELC9IYENFPHCEWHIDCHCGGEH9OFZBN' + ) + + tag = Tag.from_string('H2G2') + + self.bundle.add_transaction(ProposedTransaction( + address = address, + tag = tag, + + message = TryteString.from_string( + ''' +"Good morning," said Deep Thought at last. +"Er... Good morning, O Deep Thought," said Loonquawl nervously. + "Do you have... er, that is..." +"... an answer for you?" interrupted Deep Thought majestically. "Yes. I have." +The two men shivered with expectancy. Their waiting had not been in vain. +"There really is one?" breathed Phouchg. +"There really is one," confirmed Deep Thought. +"To Everything? To the great Question of Life, the Universe and Everything?" +"Yes." +Both of the men had been trained for this moment; their lives had been a + preparation for it; they had been selected at birth as those who would + witness the answer; but even so they found themselves gasping and squirming + like excited children. +"And you're ready to give it to us?" urged Loonquawl. +"I am." +"Now?" +"Now," said Deep Thought. +They both licked their dry lips. +"Though I don't think," added Deep Thought, "that you're going to like it." +"Doesn't matter," said Phouchg. "We must know it! Now!" +"Now?" enquired Deep Thought. +"Yes! Now!" +"All right," said the computer and settled into silence again. + The two men fidgeted. The tension was unbearable. +"You're really not going to like it," observed Deep Thought. +"Tell us!" +"All right," said Deep Thought. "The Answer to the Great Question..." +"Yes?" +"Of Life, the Universe and Everything..." said Deep Thought. +"Yes??" +"Is..." +"Yes?!" +"Forty-two," said Deep Thought, with infinite majesty and calm. + ''' + ), + + # Now you know... + value = 42, + )) + + # Because the message is too long to fit into a single fragment, + # the transaction is split into two parts. + self.assertEqual(len(self.bundle), 2) + + txn1 = self.bundle[0] + self.assertEqual(txn1.address, address) + self.assertEqual(txn1.tag, tag) + self.assertEqual(txn1.value, 42) + + txn2 = self.bundle[1] + self.assertEqual(txn2.address, address) + self.assertEqual(txn2.tag, tag) + # Supplementary transactions are assigned zero IOTA value. + self.assertEqual(txn2.value, 0) def test_add_transaction_error_already_finalized(self): """ Attempting to add a transaction to a bundle that is already finalized. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION999999DCBIE' + b'U9AIE9H9BCKGMCVCUGYDKDLCAEOHOHZGW9KGS9VGH' + ), + + value = 0, + )) + self.bundle.finalize() + + with self.assertRaises(RuntimeError): + self.bundle.add_transaction(ProposedTransaction( + address = Address(b''), + value = 0, + )) def test_add_transaction_error_negative_value(self): """ @@ -550,89 +652,364 @@ def test_add_transaction_error_negative_value(self): Use :py:meth:`ProposedBundle.add_inputs` to add inputs to a bundle. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + with self.assertRaises(ValueError): + self.bundle.add_transaction(ProposedTransaction( + address = Address(b''), + value = -1, + )) - def test_add_inputs_balanced(self): + def test_add_inputs_no_change(self): """ Adding inputs to cover the exact amount of the bundle spend. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999VELDTF' + b'QHDFTHIHFE9II9WFFDFHEATEI99GEDC9BAUH9EBGZ' + ), + + value = 29, + )) + + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999OGVEEF' + b'BCYAM9ZEAADBGBHH9BPBOHFEGCFAM9DESCCHODZ9Y' + ), + + value = 13, + )) + + self.bundle.add_inputs([ + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION99999VAFFMC' + b'X9AABIH9AEEGJHKFSHTGYHSFR9DEH9MEDAGGIGK9E', + + balance = 40, + key_index = 0, + ), + + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION99999VDR9AD' + b'OEH9YGGHGDVBCAREVBDHOFAGNDZCPBBAAIUCDGQ9Z', + + balance = 2, + key_index = 1, + ), + ]) + + # Because transaction signatures are so large, each input + # transaction must be split into multiple parts. + expected_length = 2 + (2 * AddressGenerator.DIGEST_ITERATIONS) + + self.assertEqual(len(self.bundle), expected_length) + + self.bundle.send_unspent_inputs_to( + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999FDCDFD' + b'VAF9NFLCSCSFFCLCW9KFL9TCAAO9IIHATCREAHGEA' + ), + ) + self.bundle.finalize() + + # Because the transaction is already balanced, no change + # transaction is necessary. + self.assertEqual(len(self.bundle), expected_length) + def test_add_inputs_with_change(self): """ Adding inputs to a bundle results in unspent inputs. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + tag = Tag(b'CHANGE9TXN') + + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999VELDTF' + b'QHDFTHIHFE9II9WFFDFHEATEI99GEDC9BAUH9EBGZ' + ), + + value = 29, + )) + + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999OGVEEF' + b'BCYAM9ZEAADBGBHH9BPBOHFEGCFAM9DESCCHODZ9Y' + ), + + tag = tag, + value = 13, + )) + + self.bundle.add_inputs([ + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION99999VAFFMC' + b'X9AABIH9AEEGJHKFSHTGYHSFR9DEH9MEDAGGIGK9E', + + balance = 40, + key_index = 0, + ), + + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION99999VDR9AD' + b'OEH9YGGHGDVBCAREVBDHOFAGNDZCPBBAAIUCDGQ9Z', + + balance = 20, + key_index = 1, + ), + ]) + + change_address =\ + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999KAFGVC' + b'IBLHS9JBZCEFDELEGFDCZGIEGCPFEIQEYGA9UFPAE' + ) + + self.bundle.send_unspent_inputs_to(change_address) + + # The change transaction is not created until the bundle is + # finalized. + expected_length = 2 + (2 * AddressGenerator.DIGEST_ITERATIONS) + + self.assertEqual(len(self.bundle), expected_length) + + self.bundle.finalize() + + self.assertEqual(len(self.bundle), expected_length + 1) + + change_txn = self.bundle[-1] + self.assertEqual(change_txn.address, change_address) + self.assertEqual(change_txn.value, 18) + self.assertEqual(change_txn.tag, tag) def test_add_inputs_error_already_finalized(self): """ Attempting to add inputs to a bundle that is already finalized. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999XE9IVG' + b'EFNDOCQCMERGUATCIEGGOHPHGFIAQEZGNHQ9W99CH' + ), - def test_send_unspent_inputs_to_unbalanced(self): - """ - Invoking ``send_unspent_inputs_to`` on an unbalanced bundle. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + value = 0, + )) - def test_send_unspent_inputs_to_balanced(self): - """ - Invoking ``send_unspent_inputs_to`` on a balanced bundle. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.finalize() + + with self.assertRaises(RuntimeError): + self.bundle.add_inputs([]) def test_send_unspent_inputs_to_error_already_finalized(self): """ Invoking ``send_unspent_inputs_to`` on a bundle that is already finalized. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999XE9IVG' + b'EFNDOCQCMERGUATCIEGGOHPHGFIAQEZGNHQ9W99CH' + ), - def test_finalize_happy_path(self): - """ - Finalizing a bundle. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + value = 0, + )) + + self.bundle.finalize() + + with self.assertRaises(RuntimeError): + self.bundle.send_unspent_inputs_to(Address(b'')) def test_finalize_error_already_finalized(self): """ Attempting to finalize a bundle that is already finalized. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999XE9IVG' + b'EFNDOCQCMERGUATCIEGGOHPHGFIAQEZGNHQ9W99CH' + ), - def test_finalize_error_unbalanced(self): + value = 0, + )) + + self.bundle.finalize() + + with self.assertRaises(RuntimeError): + self.bundle.finalize() + + def test_finalize_error_no_transactions(self): """ - Attempting to finalize an unbalanced bundle. + Attempting to finalize a bundle with no transactions. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + with self.assertRaises(ValueError): + self.bundle.finalize() + + def test_finalize_error_negative_balance(self): + """ + Attempting to finalize a bundle with unspent inputs. + """ + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999IGEFUG' + b'LIHIJGJGZ9CGRENCRHF9XFEAWD9ILFWEJFKDLITCC' + ), + + value = 42, + )) + + self.bundle.add_inputs([ + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION99999LAHFJ9' + b'Z9QEHGIHTAQFWFAHYEKFDBXHSBM9K9T9S9SBTF99W', + + balance = 43, + key_index = 0, + ), + ]) + + # Bundle spends 42 IOTAs, but inputs total 43 IOTAs. + self.assertEqual(self.bundle.balance, -1) + + # In order to finalize this bundle, we would need to specify a + # change address. + with self.assertRaises(ValueError): + self.bundle.finalize() + + def test_finalize_error_positive_balance(self): + """ + Attempting to finalize a bundle with insufficient inputs. + """ + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999IGEFUG' + b'LIHIJGJGZ9CGRENCRHF9XFEAWD9ILFWEJFKDLITCC' + ), + + value = 42, + )) + + self.bundle.add_inputs([ + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION99999LAHFJ9' + b'Z9QEHGIHTAQFWFAHYEKFDBXHSBM9K9T9S9SBTF99W', + + balance = 41, + key_index = 0, + ), + ]) + + # Bundle spends 42 IOTAs, but inputs total only 41 IOTAs. + self.assertEqual(self.bundle.balance, 1) + + # In order to finalize this bundle, we would need to specify + # additional inputs. + with self.assertRaises(ValueError): + self.bundle.finalize() def test_sign_inputs(self): """ Signing inputs in a finalized bundle. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999QARFLF' + b'TDVATBVFTFCGEHLFJBMHPBOBOHFBSGAGWCM9PG9GX' + ), + + value = 42, + )) + + self.bundle.add_inputs([ + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION99999UGYFU9' + b'TGMHNEN9S9CAIDUBGETHJHFHRAHGRGVF9GTDYHXCE', + + balance = 42, + key_index = 0, + ) + ]) + + self.bundle.finalize() + + # Mock the signature generator to improve test performance. + # We already have unit tests for signature generation; all we need + # to do here is verify that the method is invoked correctly. + # noinspection PyUnusedLocal + def mock_signature_generator(bundle, key_generator, txn): + for i in range(AddressGenerator.DIGEST_ITERATIONS): + yield Fragment.from_trits(trits_from_int(i)) + + with patch( + 'iota.transaction.ProposedBundle._create_signature_fragment_generator', + mock_signature_generator, + ): + self.bundle.sign_inputs(KeyGenerator(b'')) + + self.assertEqual( + len(self.bundle), + + # Spend txn + input fragments + 1 + AddressGenerator.DIGEST_ITERATIONS, + ) + + # The spending transaction does not have a signature. + self.assertEqual( + self.bundle[0].signature_message_fragment, + Fragment(b''), + ) + + for j in range(AddressGenerator.DIGEST_ITERATIONS): + self.assertEqual( + self.bundle[j+1].signature_message_fragment, + Fragment.from_trits(trits_from_int(j)), + ) def test_sign_inputs_error_not_finalized(self): """ Attempting to sign inputs in a bundle that hasn't been finalized yet. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.add_transaction(ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999QARFLF' + b'TDVATBVFTFCGEHLFJBMHPBOBOHFBSGAGWCM9PG9GX' + ), + + value = 42, + )) + + self.bundle.add_inputs([ + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION99999UGYFU9' + b'TGMHNEN9S9CAIDUBGETHJHFHRAHGRGVF9GTDYHXCE', + + balance = 42, + key_index = 0, + ) + ]) - # :todo: Validation tests. + with self.assertRaises(RuntimeError): + self.bundle.sign_inputs(KeyGenerator(b'')) # noinspection SpellCheckingInspection