예제 #1
0
  def test_pass_changeAddress_multisig_address(self):
    """
    ``changeAddress`` is allowed to be a MultisigAddress.
    """
    change_addy =\
      MultisigAddress(
        digests = [self.digest_1, self.digest_2],
        trytes  = self.trytes_1
      )

    filter_ = self._filter({
      'changeAddress': change_addy,

      'multisigInput':
        MultisigAddress(
          digests = [self.digest_1, self.digest_2],
          trytes  = self.trytes_2,
        ),

      'transfers':
        [
          ProposedTransaction(
            address = Address(self.trytes_3),
            value   = 42,
          ),
        ],
    })

    self.assertFilterPasses(filter_)
    self.assertIs(filter_.cleaned_data['changeAddress'], change_addy)
예제 #2
0
  def test_pass_happy_path(self):
    """
    Request is valid.
    """
    request = {
      'changeAddress':
        Address(self.trytes_1),

      'multisigInput':
        MultisigAddress(
          digests = [self.digest_1, self.digest_2],
          trytes  = self.trytes_2,
        ),

      'transfers':
        [
          ProposedTransaction(
            address = Address(self.trytes_3),
            value   = 42,
          ),
        ],
    }

    filter_ = self._filter(request)

    self.assertFilterPasses(filter_)
    self.assertDictEqual(filter_.cleaned_data, request)
예제 #3
0
  def test_fail_transfers_wrong_type(self):
    """
    ``transfers`` is not an array.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        # ``transfers`` must be an array, even if there's only one
        # transaction.
        'transfers':
          ProposedTransaction(
            address = Address(self.trytes_3),
            value   = 42,
          ),
      },

      {
        'transfers': [f.Array.CODE_WRONG_TYPE],
      },
    )
예제 #4
0
  def test_error_zero_iotas_transferred(self):
    """
    The bundle doesn't spend any IOTAs.

    This is considered an error case because
    :py:meth:`MultisigIota.prepare_multisig_transfer` is specialized
    for generating bundles that require multisig inputs.  Any bundle
    that doesn't require multisig functionality should be generated
    using :py:meth:`iota_async.api.Iota.prepare_transfer` instead.
    """
    with self.assertRaises(ValueError):
      self.command(
        transfers = [
          ProposedTransaction(
            address = Address(self.trytes_1),
            value   = 0,
          ),
        ],

        multisigInput =
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),
      )
예제 #5
0
  def test_fail_unexpected_parameters(self):
    """
    Request contains unexpected parameters.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers':
          [
            ProposedTransaction(
              address = Address(self.trytes_3),
              value   = 42,
            ),
          ],

        # Oh come on!
        'foo': 'bar',
      },

      {
        'foo': [f.FilterMapper.CODE_EXTRA_KEY],
      },
    )
예제 #6
0
  def test_error_insufficient_inputs(self):
    """
    The multisig input does not contain sufficient IOTAs to cover the
    spends.
    """
    self.adapter.seed_response(
      command = GetBalancesCommand.command,

      response = {
        'balances': [42],
        'duration': 86,
      },
    )

    with self.assertRaises(ValueError):
      self.command(
        transfers = [
          ProposedTransaction(
            address = Address(self.trytes_1),
            value   = 101,
          ),
        ],

        multisigInput =
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),
      )
예제 #7
0
  def test_fail_changeAddress_wrong_type(self):
    """
    ``changeAddress`` is not a TrytesCompatible value.
    """
    self.assertFilterErrors(
      {
        'changeAddress': 42,

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers':
          [
            ProposedTransaction(
              address = Address(self.trytes_3),
              value   = 42,
            ),
          ],
      },

      {
        'changeAddress': [f.Type.CODE_WRONG_TYPE],
      },
    )
예제 #8
0
  def test_pass_compatible_types(self):
    """
    Request contains values that can be converted to the expected
    types.
    """
    txn =\
      ProposedTransaction(
        address = Address(self.trytes_3),
        value   = 42,
      )

    filter_ =\
      self._filter({
        # ``changeAddress`` can be any value that resolves to an
        # :py:class:`Address`.
        'changeAddress': self.trytes_1,

        # ``multisigInput`` must be a :py:class:`MultisigInput` object.
        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        # ``transfers`` must contain an array of
        # :py:class:`ProposedTransaction` objects.
        'transfers': [txn],
      })

    self.assertFilterPasses(filter_)

    self.assertDictEqual(
      filter_.cleaned_data,

      {
        'changeAddress': Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers': [txn],
      },
    )
예제 #9
0
  def test_pass_optional_parameters_excluded(self):
    """
    Request omits optional parameters.
    """
    txn =\
      ProposedTransaction(
        address = Address(self.trytes_3),
        value   = 42,
      )

    filter_ =\
      self._filter({
        # ``changeAddress`` is optional.
        # Technically, it's required if there are unspent inputs, but
        # the filter has no way to know whether this is the case.
        # 'changeAddress': self.trytes_1,

        # These parameters are required.
        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers': [txn],
      })

    self.assertFilterPasses(filter_)

    self.assertDictEqual(
      filter_.cleaned_data,

      {
        'changeAddress': None,

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers': [txn],
      },
    )
예제 #10
0
 def test_add_inputs_error_digests_empty(self):
   """
   Adding a multisig input with unknown digests.
   """
   with self.assertRaises(ValueError):
     self.bundle.add_inputs([
       MultisigAddress(
         trytes  = self.trytes_1,
         digests = [],
       ),
     ])
예제 #11
0
  def test_add_inputs_error_multiple(self):
    """
    Attempting to add multiple multisig inputs.

    This is not currently supported.
    """
    with self.assertRaises(ValueError):
      # noinspection SpellCheckingInspection
      self.bundle.add_inputs([
        MultisigAddress(
          trytes  = self.trytes_1,
          digests = [self.digest_1, self.digest_2],
          balance = 42,
        ),

        MultisigAddress(
          trytes  = self.trytes_2,
          digests = [self.digest_2, self.digest_1],
        ),
      ])
예제 #12
0
 def test_add_inputs_error_balance_null(self):
   """
   Adding a multisig input with null balance.
   """
   with self.assertRaises(ValueError):
     self.bundle.add_inputs([
       MultisigAddress(
         trytes  = self.trytes_1,
         digests = [self.digest_1, self.digest_2],
         # balance = 42,
       ),
     ])
예제 #13
0
  def test_fail_multisigInput_multiple(self):
    """
    ``multisigInput`` is an array.

    This is not valid; a bundle may only contain a single multisig
    input.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          [
            MultisigAddress(
              digests = [self.digest_1, self.digest_2],
              trytes  = self.trytes_2,
            ),

            MultisigAddress(
              digests = [self.digest_1, self.digest_2],
              trytes  = self.trytes_2,
            )
          ],

        'transfers':
          [
            ProposedTransaction(
              address = Address(self.trytes_3),
              value   = 42,
            ),
          ],
      },

      {
        'multisigInput': [f.Type.CODE_WRONG_TYPE],
      },
    )
예제 #14
0
    def test_happy_path(self):
        """
    Generating a multisig address.
    """
        result = self.command(digests=[self.digest_1, self.digest_2])

        # noinspection SpellCheckingInspection
        self.assertDictEqual(
            result,
            {
                'address':
                MultisigAddress(
                    trytes=b'ZYKDKGXTMGINTQLUMVNBBI9XCEI9BWYF9YOPCBFT'
                    b'UUJZWM9YIWHNYZEWOPEVRVLKZCPRKLCQD9BR9FVLC',
                    digests=[self.digest_1, self.digest_2],
                ),
            },
        )
예제 #15
0
  def test_error_unspent_inputs_no_change_address(self):
    """
    The bundle has unspent inputs, but no change address was specified.

    Unlike :py:meth:`iota_async.api.Iota.prepare_transfer` where all of the
    inputs are owned by the same seed, creating a multisig transfer
    usually involves multiple people.

    It would be unfair to the participants of the transaction if we
    were to automatically generate a change address using the seed of
    whoever happened to invoke the
    :py:meth:`MultisigIota.prepare_multisig_transfer` method!
    """
    self.adapter.seed_response(
      command = GetBalancesCommand.command,

      response = {
        'balances': [101],
        'duration': 86,
      },
    )

    with self.assertRaises(ValueError):
      self.command(
        transfers = [
          ProposedTransaction(
            address = Address(self.trytes_1),
            value   = 42,
          ),
        ],

        multisigInput =
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        # changeAddress = Address(self.trytes_3),
      )
예제 #16
0
  def test_fail_transfers_null(self):
    """
    ``transfers`` is null.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        # On second thought, I changed my mind.
        'transfers': None,
      },

      {
        'transfers': [f.Required.CODE_EMPTY],
      },
    )
예제 #17
0
  def test_fail_transfers_empty(self):
    """
    ``transfers`` is an array, but it's empty.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        # This is a variation on the ``null`` test.
        'transfers': [],
      },

      {
        'transfers': [f.Required.CODE_EMPTY],
      },
    )
예제 #18
0
  def test_fail_transfers_contents_invalid(self):
    """
    ``transfers`` is an array, but it contains invalid values.
    """
    self.assertFilterErrors(
      {
        'changeAddress':
          Address(self.trytes_1),

        'multisigInput':
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        'transfers':
          [
            None,
            42,

            # This one's valid, actually; just making sure that the
            # filter doesn't cheat.
            ProposedTransaction(
              address = Address(self.trytes_3),
              value   = 42,
            ),

            Address(self.trytes_3),
          ],
      },

      {
        'transfers.0': [f.Required.CODE_EMPTY],
        'transfers.1': [f.Type.CODE_WRONG_TYPE],
        'transfers.3': [f.Type.CODE_WRONG_TYPE],
      },
    )
예제 #19
0
    def get_address(self):
        # type: () -> MultisigAddress
        """
        Returns the new multisig address.

        Note that you can continue to add digests after extracting an
        address; the next address will use *all* of the digests that
        have been added so far.
        """
        if not self._digests:
            raise ValueError(
                'Must call ``add_digest`` at least once '
                'before calling ``get_address``.', )

        if not self._address:
            address_trits = [0] * HASH_LENGTH
            self._sponge.squeeze(address_trits)

            self._address = MultisigAddress.from_trits(
                address_trits,
                digests=self._digests[:],
            )

        return self._address
예제 #20
0
  def test_add_inputs_happy_path(self):
    """
    Adding a multisig input to a bundle.
    """
    # noinspection SpellCheckingInspection
    self.bundle.add_transaction(
      ProposedTransaction(
        address = Address(self.trytes_1),
        value   = 42,
      ),
    )

    self.bundle.add_inputs([
      MultisigAddress(
        trytes  = self.trytes_2,
        digests = [self.digest_1, self.digest_2],
        balance = 42,
      )
    ])

    # The multisig input requires a total of 4 transactions to store
    # all the signatures.  Including the spend, that makes 5
    # transactions in total.
    self.assertEqual(len(self.bundle), 5)
예제 #21
0
  def test_happy_path(self):
    """
    Preparing a bundle with a multisig input.
    """
    self.adapter.seed_response(
      command = GetBalancesCommand.command,

      response = {
        'balances': [42],

        # Would it be cheeky to put "7½ million years" here?
        'duration': 86,
      },
    )

    pmt_result =\
      self.command(
        transfers = [
          ProposedTransaction(
            address = Address(self.trytes_1),
            value   = 42,
          ),
        ],

        multisigInput =
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),
      )

    # The command returns the raw trytes.  This is useful in a
    # real-world scenario because trytes are easier to transfer between
    # each entity that needs to apply their signature.
    #
    # However, for purposes of this test, we will convert the trytes
    # back into a bundle so that we can inspect the end result more
    # easily.
    bundle = Bundle.from_tryte_strings(pmt_result['trytes'])

    #
    # This bundle looks almost identical to what you would expect from
    # :py:meth:`iota_async.api.Iota.prepare_transfer`, except:
    # - There are 4 inputs (to hold all of the signature fragments).
    # - The inputs are unsigned.
    #
    self.assertEqual(len(bundle), 5)

    # Spend Transaction
    txn_1 = bundle[0]
    self.assertEqual(txn_1.address, self.trytes_1)
    self.assertEqual(txn_1.value, 42)

    # Input 1, Part 1 of 4
    txn_2 = bundle[1]
    self.assertEqual(txn_2.address, self.trytes_2)
    self.assertEqual(txn_2.value, -42)
    self.assertEqual(txn_2.signature_message_fragment, Fragment(b''))

    # Input 1, Part 2 of 4
    txn_3 = bundle[2]
    self.assertEqual(txn_3.address, self.trytes_2)
    self.assertEqual(txn_3.value, 0)
    self.assertEqual(txn_3.signature_message_fragment, Fragment(b''))

    # Input 1, Part 3 of 4
    txn_4 = bundle[3]
    self.assertEqual(txn_4.address, self.trytes_2)
    self.assertEqual(txn_4.value, 0)
    self.assertEqual(txn_4.signature_message_fragment, Fragment(b''))

    # Input 1, Part 4 of 4
    txn_5 = bundle[4]
    self.assertEqual(txn_5.address, self.trytes_2)
    self.assertEqual(txn_5.value, 0)
    self.assertEqual(txn_5.signature_message_fragment, Fragment(b''))
예제 #22
0
  def test_unspent_inputs_with_change_address(self):
    """
    The bundle has unspent inputs, so it uses the provided change
    address.
    """
    self.adapter.seed_response(
      command = GetBalancesCommand.command,

      response = {
        'balances': [101],
        'duration': 86,
      },
    )

    pmt_result =\
      self.command(
        transfers = [
          ProposedTransaction(
            address = Address(self.trytes_1),
            value   = 42,
          ),
        ],

        multisigInput =
          MultisigAddress(
            digests = [self.digest_1, self.digest_2],
            trytes  = self.trytes_2,
          ),

        changeAddress = Address(self.trytes_3),
      )

    bundle = Bundle.from_tryte_strings(pmt_result['trytes'])

    self.assertEqual(len(bundle), 6)

    # Spend Transaction
    txn_1 = bundle[0]
    self.assertEqual(txn_1.address, self.trytes_1)
    self.assertEqual(txn_1.value, 42)

    # Input 1, Part 1 of 4
    txn_2 = bundle[1]
    self.assertEqual(txn_2.address, self.trytes_2)
    self.assertEqual(txn_2.value, -101)
    self.assertEqual(txn_2.signature_message_fragment, Fragment(b''))

    # Input 1, Part 2 of 4
    txn_3 = bundle[2]
    self.assertEqual(txn_3.address, self.trytes_2)
    self.assertEqual(txn_3.value, 0)
    self.assertEqual(txn_3.signature_message_fragment, Fragment(b''))

    # Input 1, Part 3 of 4
    txn_4 = bundle[3]
    self.assertEqual(txn_4.address, self.trytes_2)
    self.assertEqual(txn_4.value, 0)
    self.assertEqual(txn_4.signature_message_fragment, Fragment(b''))

    # Input 1, Part 4 of 4
    txn_5 = bundle[4]
    self.assertEqual(txn_5.address, self.trytes_2)
    self.assertEqual(txn_5.value, 0)
    self.assertEqual(txn_5.signature_message_fragment, Fragment(b''))

    # Change
    txn_6 = bundle[5]
    self.assertEqual(txn_6.address, self.trytes_3)
    self.assertEqual(txn_6.value, 59)