diff --git a/.travis.yml b/.travis.yml index da1944c741..d83deac371 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,6 +45,10 @@ env: - TOX_ENV=py27-net - TOX_ENV=py34-net - TOX_ENV=py35-net + # shh + - TOX_ENV=py27-shh + - TOX_ENV=py34-shh + - TOX_ENV=py35-shh # txpool - TOX_ENV=py27-txpool - TOX_ENV=py34-txpool diff --git a/conftest.py b/conftest.py index bac2eabd3f..6c61cecafc 100644 --- a/conftest.py +++ b/conftest.py @@ -156,7 +156,7 @@ def setup_testing_geth(): geth_process = GethProcess( 'testing', base_dir=base_dir, - overrides={'verbosity': '3'}, + overrides={'verbosity': '3','shh': True}, ) with geth_process as running_geth_process: running_geth_process.wait_for_ipc(60) diff --git a/docs/filters.rst b/docs/filters.rst index 6ddd68ba33..b602d07aa0 100644 --- a/docs/filters.rst +++ b/docs/filters.rst @@ -102,7 +102,7 @@ will return a new :py:class::`BlockFilter` object. ... sys.stdout.write("New Block: {0}".format(transaction_hash)) ... >>> new_transaction_filter = web3.eth.filter('pending') - >>> new_transaction_filter.watch(new_transaction_filter) + >>> new_transaction_filter.watch(new_transaction_callback) # each time the client receieves a unmined transaction the # `new_transaction_filter` function will be called with the transaction # hash. @@ -143,3 +143,23 @@ event data from the event logs. The :py:class::`PastLogFilter` is a subclass of :py:class::`LogFilter` that is configured specially to return historical event logs. It conforms to the same API as the ``LogFilter`` class. + + +Shh Filter +---------- + +.. py:class:: ShhFilter(web3, filter_id) + +The :py:class:: `ShhFilter` class is used for filtering Shh messages. +You can setup a callback function for Whipser messages matching the topics subscribed using ``web3.shh.filter(filter_params)``,which +will return a :py:class::`ShhFilter` object + + .. code-block:: python + + >>>def filter_callback(new_message): + ... sys.stdout.write("New Shh Message: {0}".format(new_message)) + ... + >>>shh_filter = web3.shh.filter({"topics":[web3.fromAscii("topic_to_subscribe")]}) + >>>shh_filter.watch(filter_callback) + #each time client recieves a Shh messages matching the topics subscibed, + #filter_callback is called diff --git a/docs/web3.shh.rst b/docs/web3.shh.rst index c16a5155ce..cdb925e681 100644 --- a/docs/web3.shh.rst +++ b/docs/web3.shh.rst @@ -7,32 +7,120 @@ SHH API The ``web3.shh`` object exposes methods to interact with the RPC APIs under the ``shh_`` namespace. +Properties +---------- + +The following properties are available on the ``web.shh`` namespace. + +.. py:attribute:: Shh.version + + The version of Whisper protocol used by client + + .. code-block:: python + + >>>web3.shh.version + 2 + Methods ------- -The following methods are available on the ``web3.personal`` namespace. +The following methods are available on the ``web3.shh`` namespace. + + +.. py:method:: Shh.post(self, params) + + * Delegates to ``shh_post`` RPC method + + * ``params`` cannot be ``None`` and should contain ``topics`` and ``payload`` + + * Returns ``True`` if the message was succesfully sent,otherwise ``False`` + + .. code-block:: python + + >>>web3.shh.post({"topics":[web3.fromAscii("test_topic")],"payload":web3.fromAscii("test_payload")}) + True + +.. py:method:: Shh.newIdentity(self) + + * Delegates to ``shh_newIdentity`` RPC method + + * Returns ``address`` of newly created identity. + + .. code-block:: python + + >>>web3.shh.newIdentity() + u'0x045ed8042f436e1b546afd16e1f803888b896962484c0154fcc7c5fc43e276972af85f29a995a3beb232a4e9a0648858c0c8c0639d709f5d3230807d084b2d5030' + +.. py:method:: Shh.hasIdentity(self, identity) + + * Delegates to ``shh_hasIdentity`` RPC method + + * Returns ``True`` if the client holds the private key for the given identity,otherwise ``False`` + + .. code-block:: python + + >>>web3.shh.hasIdentity(u'0x045ed8042f436e1b546afd16e1f803888b896962484c0154fcc7c5fc43e276972af85f29a995a3beb232a4e9a0648858c0c8c0639d709f5d3230807d084b2d5030') + True + +.. py:method:: Shh.newGroup(self) + + * Delegates to ``shh_newGroup`` RPC method + + * Returns ``address`` of newly created group. + + .. note:: This method is not implemented yet in ``Geth``. `Open Issue `_ + +.. py:method:: Shh.addToGroup(self, identity) + + * Delegates to ``shh_addToGroup`` RPC Method + + * Returns ``True`` if the identity was succesfully added to the group,otherwise ``False`` + + .. note:: This method is not implemented yet in ``Geth``. `Open Issue `_ + +.. py:method:: Shh.filter(self, filter_params) + + * Delegates to ``shh_newFilter`` RPC Method + + * ``filter_params`` should contain the ``topics`` to subscribe + * Returns an instance of ``ShhFilter`` on succesful creation of filter,otherwise raises ``ValueError`` exception -.. py:method:: Shh.post(self, *args, **kwargs) + .. code-block:: python - .. note:: Not Implemented + >>>shh_filter = shh.filter({"topics":[web.fromAscii("topic_to_subscribe")]}) + >>>shh_filter.filter_id + u'0x0' +.. py:method:: Shh.uninstallFilter(self, filter_id) -.. py:method:: Shh.newIdentity(self, *args, **kwargs) + * Delegates to ``shh_uninstallFilter`` RPC Method - .. note:: Not Implemented + * Returns ``True`` if the filter was sucesfully uninstalled ,otherwise ``False`` + .. code-block:: python -.. py:method:: Shh.hasIdentity(self, *args, **kwargs) + >>>web3.shh.uninstallFilter("0x2") + True - .. note:: Not Implemented +.. py:method:: Shh.getFilterChanges(self, filter_id) + * Delegates to ``shh_getFilterChanges`` RPC Method -.. py:method:: Shh.newGroup(self, *args, **kwargs) + * Returns list of messages recieved since last poll + + .. code-block:: python + + >>>web3.shh.getFilterChanges(self,"0x2") + [{u'from': u'0x0', u'to': u'0x0', u'ttl': 50, u'hash': u'0xf84900b57d856a6ab1b41afc9784c31be48e841b9bcfc6accac14d05d7189f2f', u'payload': u'0x746573696e67', u'sent': 1476625149}] - .. note:: Not Implemented +.. py:method:: Shh.getMessages(self, filter_id) + * Delegates to ``shh_getMessages`` RPC Method -.. py:method:: Shh.addToGroup(self, *args, **kwargs) + * Returns a list of all messages - .. note:: Not Implemented + .. code-block:: python + + >>>web3.shh.getMessages("0x2") + [{u'from': u'0x0', u'to': u'0x0', u'ttl': 50, u'hash': u'0x808d74d003d1dcbed546cca29d7a4e839794c226296b613b0fa7a8c670f84146', u'payload': u'0x746573696e67617364', u'sent': 1476625342}, {u'from': u'0x0', u'to': u'0x0', u'ttl': 50, u'hash': u'0x62a2eb9a19968d59d8a85e6dc8d73deb9b4cd40c83d95b796262d6affe6397c6', u'payload': u'0x746573696e67617364617364', u'sent': 1476625369}] diff --git a/requirements-dev.txt b/requirements-dev.txt index 1686f685f6..af9b88a603 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,9 +3,10 @@ pytest-pythonpath>=0.3 tox>=1.8.0 eth-testrpc>=0.8.6 ethereum-tester-client>=1.2.3 -py-geth>=1.2.0 +py-geth>=1.4.0 ethereum>=1.5.2 secp256k1>=0.13.1 rlp>=0.4.6 hypothesis>=3.4.2 flaky>=3.3.0 +flake8==3.0.4 diff --git a/requirements-docs.txt b/requirements-docs.txt index c5bd02ad7b..cbdfbc717e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -10,7 +10,7 @@ contextlib2>=0.5.4 #eth-testrpc>=0.8.0 #ethereum-tester-client>=1.1.0 gevent>=1.1.2 -py-geth>=1.1.0 +py-geth>=1.4.0 py-solc>=0.4.0 #pysha3>=0.3 pytest>=2.7.2 diff --git a/tests/filtering/test_contract_on_event_filtering.py b/tests/filtering/test_contract_on_event_filtering.py index 1162e4d429..d5fde8a246 100644 --- a/tests/filtering/test_contract_on_event_filtering.py +++ b/tests/filtering/test_contract_on_event_filtering.py @@ -23,7 +23,7 @@ def test_on_filter_using_get_interface(web3_empty, txn_hash = emitter.transact().logNoArgs(emitter_event_ids.LogNoArguments) txn_receipt = wait_for_transaction(web3, txn_hash) - with gevent.Timeout(10): + with gevent.Timeout(30): while not filter.get(False): gevent.sleep(random.random()) @@ -54,11 +54,11 @@ def test_on_filter_with_only_event_name(web3_empty, txn_hash = emitter.transact().logNoArgs(emitter_event_ids.LogNoArguments) txn_receipt = wait_for_transaction(web3, txn_hash) - with gevent.Timeout(5): + with gevent.Timeout(30): while not seen_logs: gevent.sleep(random.random()) - filter.stop_watching(10) + filter.stop_watching(30) assert len(seen_logs) == 1 assert seen_logs[0]['transactionHash'] == txn_hash @@ -99,11 +99,11 @@ def test_on_filter_with_event_name_and_single_argument(web3_empty, for txn_hash in txn_hashes: wait_for_transaction(web3, txn_hash) - with gevent.Timeout(5): + with gevent.Timeout(30): while len(seen_logs) < 2: gevent.sleep(random.random()) - filter.stop_watching(10) + filter.stop_watching(30) assert len(seen_logs) == 2 assert {l['transactionHash'] for l in seen_logs} == set(txn_hashes[1:]) @@ -144,11 +144,11 @@ def test_on_filter_with_event_name_and_non_indexed_argument(web3_empty, for txn_hash in txn_hashes: wait_for_transaction(web3, txn_hash) - with gevent.Timeout(5): + with gevent.Timeout(30): while not seen_logs: gevent.sleep(random.random()) - filter.stop_watching(10) + filter.stop_watching(30) assert len(seen_logs) == 1 assert seen_logs[0]['transactionHash'] == txn_hashes[1] diff --git a/tests/filtering/test_contract_past_event_filtering.py b/tests/filtering/test_contract_past_event_filtering.py index 364f25cf1b..fe02bbe5c2 100644 --- a/tests/filtering/test_contract_past_event_filtering.py +++ b/tests/filtering/test_contract_past_event_filtering.py @@ -26,11 +26,11 @@ def test_past_events_filter_with_callback(web3_empty, else: filter = Emitter.pastEvents('LogNoArguments', {}, seen_logs.append) - with gevent.Timeout(5): + with gevent.Timeout(30): while not seen_logs: gevent.sleep(random.random()) - filter.stop_watching(10) + filter.stop_watching(30) assert len(seen_logs) == 1 event_data = seen_logs[0] @@ -62,7 +62,7 @@ def test_past_events_filter_using_get_api(web3_empty, else: filter = Emitter.pastEvents('LogNoArguments') - with gevent.Timeout(10): + with gevent.Timeout(30): while not filter.get(False): gevent.sleep(random.random()) diff --git a/tests/shh-module/test_shh_filter.py b/tests/shh-module/test_shh_filter.py new file mode 100644 index 0000000000..d53d0ff19f --- /dev/null +++ b/tests/shh-module/test_shh_filter.py @@ -0,0 +1,22 @@ +import gevent + +def test_shh_filter(web3, skip_if_testrpc): + skip_if_testrpc(web3) + recieved_messages = [] + shh_filter = web3.shh.filter({"topics":[web3.fromAscii("test")]}) + shh_filter.watch(recieved_messages.append) + gevent.sleep(1) + + payloads = [] + payloads.append(str.encode("payload1")) + web3.shh.post({"topics":[web3.fromAscii("test")], "payload":web3.fromAscii(payloads[len(payloads)-1])}) + gevent.sleep(1) + + payloads.append(str.encode("payload2")) + web3.shh.post({"topics":[web3.fromAscii("test")], "payload":web3.fromAscii(payloads[len(payloads)-1])}) + gevent.sleep(1) + + assert len(recieved_messages) > 1 + + for message in recieved_messages: + assert web3.toAscii(message["payload"]) in payloads diff --git a/tests/shh-module/test_shh_has_identity.py b/tests/shh-module/test_shh_has_identity.py new file mode 100644 index 0000000000..b1d46d980c --- /dev/null +++ b/tests/shh-module/test_shh_has_identity.py @@ -0,0 +1,5 @@ +def test_shh_has_identity(web3, skip_if_testrpc): + skip_if_testrpc(web3) + new_identity = web3.shh.newIdentity() + assert len(new_identity) == 132 + assert web3.shh.hasIdentity(new_identity) diff --git a/tests/shh-module/test_shh_new_identity.py b/tests/shh-module/test_shh_new_identity.py new file mode 100644 index 0000000000..bdfe1b513c --- /dev/null +++ b/tests/shh-module/test_shh_new_identity.py @@ -0,0 +1,4 @@ +def test_shh_new_identity(web3, skip_if_testrpc): + skip_if_testrpc(web3) + new_identity = web3.shh.newIdentity() + assert len(new_identity) == 132 diff --git a/tests/shh-module/test_shh_post.py b/tests/shh-module/test_shh_post.py new file mode 100644 index 0000000000..6a66089323 --- /dev/null +++ b/tests/shh-module/test_shh_post.py @@ -0,0 +1,4 @@ +def test_shh_post(web3, skip_if_testrpc): + skip_if_testrpc(web3) + random_topic = "testing" + assert web3.shh.post({"topics":[web3.fromAscii(random_topic)], "payload":web3.fromAscii("testing shh on web3.py")}) diff --git a/tests/shh-module/test_shh_properties.py b/tests/shh-module/test_shh_properties.py new file mode 100644 index 0000000000..6c5af8e35f --- /dev/null +++ b/tests/shh-module/test_shh_properties.py @@ -0,0 +1,3 @@ +def test_shh_version(web3, skip_if_testrpc): + skip_if_testrpc(web3) + assert web3.shh.version == 2 diff --git a/tox.ini b/tox.ini index f18dd91708..bd4b22fd7f 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,7 @@ commands= contracts: py.test {posargs:tests/contracts} filtering: py.test {posargs:tests/filtering} net: py.test {posargs:tests/net-module} + shh: py.test {posargs:tests/shh-module} txpool: py.test {posargs:tests/txpool-module} db: py.test {posargs:tests/db-module} managers: py.test {posargs:tests/managers} diff --git a/web3/shh.py b/web3/shh.py index 7d21d53b29..e0040a9abf 100644 --- a/web3/shh.py +++ b/web3/shh.py @@ -1,3 +1,15 @@ +from web3 import formatters +from web3.utils.encoding import ( + to_decimal, +) +from web3.utils.functional import ( + apply_formatters_to_return, +) +from web3.utils.filters import ( + ShhFilter, +) + + class Shh(object): """ TODO: flesh this out. @@ -5,17 +17,47 @@ class Shh(object): def __init__(self, web3): self.web3 = web3 - def post(self, *args, **kwargs): - raise NotImplementedError("Not Implemented") + @property + def request_manager(self): + return self.web3._requestManager + + @property + @apply_formatters_to_return(to_decimal) + def version(self): + return self.request_manager.request_blocking("shh_version", []) + + def post(self, params): + if params and ("topics" in params) and ("payload" in params): + return self.request_manager.request_blocking("shh_post", [params]) + else: + raise ValueError("params cannot be None or doesnot contain fields 'topic' or 'payload'") + + def newIdentity(self): + return self.request_manager.request_blocking("shh_newIdentity", []) + + def hasIdentity(self, identity): + return self.request_manager.request_blocking("shh_hasIdentity", [identity]) + + def newGroup(self): + return self.request_manager.request_blocking("shh_newGroup", []) + + def addToGroup(self, params): + return self.request_manager.request_blocking("shh_addToGroup", params) - def newIdentity(self, *args, **kwargs): - raise NotImplementedError("Not Implemented") + def filter(self, filter_params): + if "topics" in filter_params: + filter_id = self.request_manager.request_blocking("shh_newFilter", [filter_params]) + return ShhFilter(self.web3, filter_id) + else: + raise ValueError("filter params doesnot contain 'topics' to subsrcibe") - def hasIdentity(self, *args, **kwargs): - raise NotImplementedError("Not Implemented") + def uninstallFilter(self, filter_id): + return self.request_manager.request_blocking("shh_uninstallFilter", [filter_id]) - def newGroup(self, *args, **kwargs): - raise NotImplementedError("Not Implemented") + @apply_formatters_to_return(formatters.log_array_formatter) + def getMessages(self, filter_id): + return self.request_manager.request_blocking("shh_getMessages", [filter_id]) - def addToGroup(self, *args, **kwargs): - raise NotImplementedError("Not Implemented") + @apply_formatters_to_return(formatters.log_array_formatter) + def getFilterChanges(self, filter_id): + return self.request_manager.request_blocking("shh_getFilterChanges", [filter_id]) diff --git a/web3/utils/filters.py b/web3/utils/filters.py index c9ee95811e..14741eb3f1 100644 --- a/web3/utils/filters.py +++ b/web3/utils/filters.py @@ -207,3 +207,30 @@ def _run(self): callback_fn(self.format_entry(entry)) self.running = False + + +class ShhFilter(Filter): + def _run(self): + if self.stopped: + raise ValueError("Cannot restart a filter") + self.running = True + + while self.running: + changes = self.web3.shh.getFilterChanges(self.filter_id) + if changes: + for entry in changes: + for callback_fn in self.callbacks: + if self.is_valid_entry(entry): + callback_fn(self.format_entry(entry)) + if self.poll_interval is None: + gevent.sleep(random.random()) + else: + gevent.sleep(self.poll_interval) + + def stop_watching(self, timeout=0): + self.running = False + self.stopped = True + self.web3.shh.uninstallFilter(self.filter_id) + self.join(timeout) + + stopWatching = stop_watching