Skip to content
This repository was archived by the owner on Jan 13, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ This method returns a ``dict`` with the following items:
broadcast/stored. Should be the same as the value of the ``trytes``
parameter.

``broadcast_bundle``
-----------------------

Re-broadcasts all transactions in a bundle given the tail transaction hash.
It might be useful when transactions did not properly propagate,
particularly in the case of large bundles.

Parameters
~~~~~~~~~~

- ``tail_hash: TransactionHash``: Transaction hash of the tail transaction
of the bundle.

Return
~~~~~~

This method returns a ``dict`` with the following items:

- ``trytes: List[TransactionTrytes]``: Transaction trytes that were
broadcast.

``find_transaction_objects``
----------------------------
Expand Down
25 changes: 25 additions & 0 deletions iota/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,31 @@ def broadcast_and_store(self, trytes):
"""
return extended.BroadcastAndStoreCommand(self.adapter)(trytes=trytes)

def broadcast_bundle(self, tail_transaction_hash):
# type (TransactionHash) -> dict
"""
Re-broadcasts all transactions in a bundle given the tail transaction hash.
It might be useful when transactions did not properly propagate,
particularly in the case of large bundles.

:param tail_transaction_hash:
Tail transaction hash of the bundle.

:return:
Dict with the following structure::

{
'trytes': List[TransactionTrytes],
List of TransactionTrytes that were broadcast.
}

References:

- https://github.com/iotaledger/iota.js/blob/next/api_reference.md#module_core.broadcastBundle
"""

return extended.BroadcastBundleCommand(self.adapter)(tail_hash=tail_transaction_hash)

def find_transaction_objects(
self,
bundles=None, # type: Optional[Iterable[BundleHash]]
Expand Down
1 change: 1 addition & 0 deletions iota/commands/extended/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
unicode_literals

from .broadcast_and_store import *
from .broadcast_bundle import *
from .find_transaction_objects import *
from .get_account_data import *
from .get_bundles import *
Expand Down
49 changes: 49 additions & 0 deletions iota/commands/extended/broadcast_bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# coding=utf-8
from __future__ import absolute_import, division, print_function, \
unicode_literals

import filters as f
from iota.filters import Trytes

from iota import TransactionTrytes, TransactionHash
from iota.commands.core import \
BroadcastTransactionsCommand
from iota.commands.extended.get_bundles import GetBundlesCommand
from iota.commands import FilterCommand, RequestFilter

__all__ = [
'BroadcastBundleCommand',
]


class BroadcastBundleCommand(FilterCommand):
"""
Executes ``broadcastBundle`` extended API command.

See :py:meth:`iota.api.Iota.broadcast_bundle` for more info.
"""
command = 'broadcastBundle'

def get_request_filter(self):
return BroadcastBundleRequestFilter()

def get_response_filter(self):
# Return value is filtered before hitting us.
pass

def _execute(self, request):
# Given tail hash, fetches the bundle from the tangle
# and validates it.
# Returns List[List[TransactionTrytes]]
# (outer list has one item in current implementation)
bundle = GetBundlesCommand(self.adapter)(transaction=request['tail_hash'])
BroadcastTransactionsCommand(self.adapter)(trytes=bundle[0])
return {
'trytes': bundle[0],
}

class BroadcastBundleRequestFilter(RequestFilter):
def __init__(self):
super(BroadcastBundleRequestFilter, self).__init__({
'tail_hash': f.Required | Trytes(TransactionHash),
})
205 changes: 205 additions & 0 deletions test/commands/extended/broadcast_bundle_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# coding=utf-8
from __future__ import absolute_import, division, print_function, \
unicode_literals

from unittest import TestCase

import filters as f
from filters.test import BaseFilterTestCase

from iota import Address, BadApiResponse, Bundle, BundleHash, Fragment, Hash, \
Iota, Tag, Transaction, TransactionHash, TransactionTrytes, Nonce
from iota.adapter import MockAdapter
from iota.commands.extended.broadcast_bundle import BroadcastBundleCommand
from iota.filters import Trytes

from six import PY2

if PY2:
from mock import MagicMock, patch
else:
from unittest.mock import MagicMock, patch

# RequestFilterTestCase code reused from get_bundles_test.py
class BroadcastBundleRequestFilterTestCase(BaseFilterTestCase):
filter_type = BroadcastBundleCommand(MockAdapter()).get_request_filter
skip_value_check = True

def setUp(self):
super(BroadcastBundleRequestFilterTestCase, self).setUp()

# noinspection SpellCheckingInspection
self.transaction = (
'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR'
'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ'
)

def test_pass_happy_path(self):
"""
Request is valid.
"""
# Raw trytes are extracted to match the IRI's JSON protocol.
request = {
'tail_hash': self.transaction,
}

filter_ = self._filter(request)

self.assertFilterPasses(filter_)
self.assertDictEqual(filter_.cleaned_data, request)

def test_pass_compatible_types(self):
"""
Request contains values that can be converted to the expected
types.
"""
filter_ = self._filter({
# Any TrytesCompatible value will work here.
'tail_hash': TransactionHash(self.transaction),
})

self.assertFilterPasses(filter_)
self.assertDictEqual(
filter_.cleaned_data,

{
'tail_hash': self.transaction,
},
)

def test_fail_empty(self):
"""
Request is empty.
"""
self.assertFilterErrors(
{},

{
'tail_hash': [f.FilterMapper.CODE_MISSING_KEY],
},
)

def test_fail_unexpected_parameters(self):
"""
Request contains unexpected parameters.
"""
self.assertFilterErrors(
{
'tail_hash': TransactionHash(self.transaction),

# SAY "WHAT" AGAIN!
'what': 'augh!',
},

{
'what': [f.FilterMapper.CODE_EXTRA_KEY],
},
)

def test_fail_transaction_wrong_type(self):
"""
``tail_hash`` is not a TrytesCompatible value.
"""
self.assertFilterErrors(
{
'tail_hash': 42,
},

{
'tail_hash': [f.Type.CODE_WRONG_TYPE],
},
)

def test_fail_transaction_not_trytes(self):
"""
``tail_hash`` contains invalid characters.
"""
self.assertFilterErrors(
{
'tail_hash': b'not valid; must contain only uppercase and "9"',
},

{
'tail_hash': [Trytes.CODE_NOT_TRYTES],
},
)

class BroadcastBundleCommandTestCase(TestCase):
def setUp(self):
super(BroadcastBundleCommandTestCase, self).setUp()

self.adapter = MockAdapter()
self.command = BroadcastBundleCommand(self.adapter)

self.tail = (
'TESTVALUE9DONTUSEINPRODUCTION99999999999'
)

self.trytes = [
'TESTVALUE9DONTUSEINPRODUCTION99999TRYTESFORTRANSACTION1',
'TESTVALUE9DONTUSEINPRODUCTION99999TRYTESFORTRANSACTION2'
]

self.trytes_dummy = [
'TESTVALUE9DONTUSEINPRODUCTION99999TRYTESFORTRANSACTION3',
'TESTVALUE9DONTUSEINPRODUCTION99999TRYTESFORTRANSACTION4'
]

def test_wireup(self):
"""
Verify that the command is wired up correctly.
"""
self.assertIsInstance(
Iota(self.adapter).broadcastBundle,
BroadcastBundleCommand,
)

def test_happy_path(self):
"""
Test command flow executes as expected.
"""
# Call the command with a tail hash.
# Let's mock away GetBundlesCommand, and we don't do
# BroadcastTransactionsCommand either.
# We could seed a response to our MockAdapter, but then we shall provide
# valid values to pass GetBundlesRequestFilter. Instead we mock away the
# whole command, so no filter is applied. It is safe because it is tested
# elsewhere.
with patch('iota.commands.extended.get_bundles.GetBundlesCommand.__call__',
MagicMock(return_value=[self.trytes])) as mocked_get_bundles:
# We could seed a reponse to our MockAdapter, but then the returned value
# from `GetBundlesCommand` shall be valid to pass
# BroadcastTransactionRequestFilter.
# Anyway, nature loves symmetry and so do we.
with patch('iota.commands.core.BroadcastTransactionsCommand.__call__',
MagicMock(return_value=[])) as mocked_broadcast:

response = self.command(tail_hash=self.tail)

self.assertEqual(
response['trytes'],
self.trytes
)

def test_happy_path_multiple_bundle(self):
"""
Test if command returns the correct bundle if underlying `get_bundles`
returns multiple bundles.
"""
# Call the command with a tail hash.
# Let's mock away GetBundlesCommand, and we don't do
# BroadcastTransactionsCommand either.
# Note that GetBundlesCommand returns multiple bundles!
with patch('iota.commands.extended.get_bundles.GetBundlesCommand.__call__',
MagicMock(return_value=[self.trytes, self.trytes_dummy])
) as mocked_get_bundles:
with patch('iota.commands.core.BroadcastTransactionsCommand.__call__',
MagicMock(return_value=[])) as mocked_broadcast:

response = self.command(tail_hash=self.tail)

# Expect only the first bundle
self.assertEqual(
response['trytes'],
self.trytes
)