Skip to content
This repository has been archived by the owner on Jan 13, 2023. It is now read-only.

Commit

Permalink
Merge pull request #249 from lzpap/broadcast_bundle
Browse files Browse the repository at this point in the history
Implement broadcast_bundle Api Command
  • Loading branch information
lzpap committed Oct 23, 2019
2 parents 370cc6d + 35a274c commit 4ae8a61
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 0 deletions.
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
)

0 comments on commit 4ae8a61

Please sign in to comment.