diff --git a/libraries/app/api.cpp b/libraries/app/api.cpp index 0190868a..d46eab07 100644 --- a/libraries/app/api.cpp +++ b/libraries/app/api.cpp @@ -202,6 +202,45 @@ namespace graphene { namespace app { network_node_api::network_node_api( application& a ) : _app( a ) { + _pending_trx_connection = _app.chain_database()->on_pending_transaction.connect([this]( const signed_transaction& transaction ){ + + auto transaction_it = _pending_transactions.find(transaction.id()); + if (_pending_transactions.end() == transaction_it) + { + _pending_transactions[transaction.id()] = transaction; + } + + if (_on_pending_transaction) + { + _on_pending_transaction(fc::variant(transaction)); + } + }); + + _applied_block_connection = _app.chain_database()->applied_block.connect([this]( const signed_block& block ){ + for (const auto& transaction: block.transactions) + { + auto transaction_it = _pending_transactions.find(transaction.id()); + if (_pending_transactions.end() != transaction_it) + { + _pending_transactions.erase(transaction_it); + } + } + + /* + * Remove expired transactions from pending_transactions + */ + for (const auto& transaction: _pending_transactions) + { + if (transaction.second.expiration < block.timestamp) + { + auto transaction_it = _pending_transactions.find(transaction.second.id()); + if (_pending_transactions.end() != transaction_it) + { + _pending_transactions.erase(transaction_it); + } + } + } + }); } fc::variant_object network_node_api::get_info() const @@ -236,6 +275,21 @@ namespace graphene { namespace app { return _app.p2p_node()->set_advanced_node_parameters(params); } + map network_node_api::list_pending_transactions() const + { + return _pending_transactions; + } + + void network_node_api::subscribe_to_pending_transactions( std::function callback ) + { + _on_pending_transaction = callback; + } + + void network_node_api::unsubscribe_from_pending_transactions() + { + _on_pending_transaction = std::function(); + } + fc::api login_api::network_broadcast()const { FC_ASSERT(_network_broadcast_api); diff --git a/libraries/app/include/graphene/app/api.hpp b/libraries/app/include/graphene/app/api.hpp index 2fe0c938..44ce7b68 100644 --- a/libraries/app/include/graphene/app/api.hpp +++ b/libraries/app/include/graphene/app/api.hpp @@ -265,8 +265,28 @@ namespace graphene { namespace app { */ std::vector get_potential_peers() const; + /** + * @brief Return list of pending transactions. + */ + map list_pending_transactions() const; + + /** + * @brief Subscribes caller for notifications about pending transactions. + * @param callback a functional object which will be called when new transaction is created. + */ + void subscribe_to_pending_transactions(std::function callback); + + /** + * @brief Unsubscribes caller from notifications about pending transactions. + */ + void unsubscribe_from_pending_transactions(); + private: application& _app; + map _pending_transactions; + boost::signals2::scoped_connection _pending_trx_connection; + boost::signals2::scoped_connection _applied_block_connection; + std::function _on_pending_transaction; }; class crypto_api @@ -422,6 +442,9 @@ FC_API(graphene::app::network_node_api, (get_potential_peers) (get_advanced_node_parameters) (set_advanced_node_parameters) + (list_pending_transactions) + (subscribe_to_pending_transactions) + (unsubscribe_from_pending_transactions) ) FC_API(graphene::app::crypto_api, (blind_sign) diff --git a/tests/tests/network_node_api_tests.cpp b/tests/tests/network_node_api_tests.cpp new file mode 100644 index 00000000..b857cdfe --- /dev/null +++ b/tests/tests/network_node_api_tests.cpp @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2017 Blockchain BV + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include + +#include "../common/database_fixture.hpp" + +using namespace graphene::chain; +using namespace graphene::chain::test; + +struct network_node_api_tests_fixture : public database_fixture +{ + processed_transaction push_transaction_for_account_creation(const std::string& account_name) + { + auto account_key = generate_private_key(account_name); + signed_transaction trx; + set_expiration(db, trx); + trx.operations.push_back(make_account(account_name, account_key.get_public_key())); + trx.validate(); + return db.push_transaction(trx, ~0); + } + + void trigger_transactions_applying() + { + db.generate_block(db.get_slot_time(1), + db.get_scheduled_witness(1), + generate_private_key("null_key"), + ~0 | database::skip_undo_history_check); + } + + void check_transaction_in_list(const map& left, const transaction& right) + { + BOOST_CHECK(left.find(right.id()) != left.end()); + } +}; + +BOOST_FIXTURE_TEST_SUITE(network_node_api_tests, network_node_api_tests_fixture) + +BOOST_AUTO_TEST_CASE(list_pending_proposals_empty) { + try { + graphene::app::network_node_api network_node_api(app); + + auto pending_transactions = network_node_api.list_pending_transactions(); + BOOST_CHECK(pending_transactions.empty()); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(list_pending_proposals_one) { + try { + graphene::app::network_node_api network_node_api(app); + + auto sam_transaction = push_transaction_for_account_creation("sam"); + + auto pending_transactions = network_node_api.list_pending_transactions(); + + BOOST_REQUIRE_EQUAL(pending_transactions.size(), 1); + check_transaction_in_list(pending_transactions, sam_transaction); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(list_pending_proposals_several) { + try { + graphene::app::network_node_api network_node_api(app); + + auto sam_transaction = push_transaction_for_account_creation("sam"); + auto dan_transaction = push_transaction_for_account_creation("dan"); + + auto pending_transactions = network_node_api.list_pending_transactions(); + + BOOST_REQUIRE_EQUAL(pending_transactions.size(), 2); + check_transaction_in_list(pending_transactions, sam_transaction); + check_transaction_in_list(pending_transactions, dan_transaction); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(list_pending_proposals_one_after_applying) { + try { + graphene::app::network_node_api network_node_api(app); + + auto sam_transaction = push_transaction_for_account_creation("sam"); + + auto pending_transactions = network_node_api.list_pending_transactions(); + BOOST_REQUIRE_EQUAL(pending_transactions.size(), 1); + check_transaction_in_list(pending_transactions, sam_transaction); + + trigger_transactions_applying(); + + pending_transactions = network_node_api.list_pending_transactions(); + BOOST_CHECK(pending_transactions.empty()); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(list_pending_proposals_several_after_applying) { + try { + graphene::app::network_node_api network_node_api(app); + + auto sam_transaction = push_transaction_for_account_creation("sam"); + auto dan_transaction = push_transaction_for_account_creation("dan"); + + auto pending_transactions = network_node_api.list_pending_transactions(); + BOOST_REQUIRE_EQUAL(pending_transactions.size(), 2); + check_transaction_in_list(pending_transactions, sam_transaction); + check_transaction_in_list(pending_transactions, dan_transaction); + + trigger_transactions_applying(); + + pending_transactions = network_node_api.list_pending_transactions(); + BOOST_CHECK(pending_transactions.empty()); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(list_pending_proposals_postponed) { + try { + graphene::app::network_node_api network_node_api(app); + + db.modify(db.get_global_properties(), [](global_property_object& properties) { + //Size in bytes. Empiricaly found to limit block size for two test transactions + properties.parameters.maximum_block_size = 650; + }); + + auto sam_transaction = push_transaction_for_account_creation("sam"); + auto dan_transaction = push_transaction_for_account_creation("dan"); + auto jon_transaction = push_transaction_for_account_creation("jon"); + + auto pending_transactions = network_node_api.list_pending_transactions(); + BOOST_REQUIRE_EQUAL(pending_transactions.size(), 3); + check_transaction_in_list(pending_transactions, sam_transaction); + check_transaction_in_list(pending_transactions, dan_transaction); + check_transaction_in_list(pending_transactions, jon_transaction); + + trigger_transactions_applying(); + + pending_transactions = network_node_api.list_pending_transactions(); + BOOST_REQUIRE_EQUAL(pending_transactions.size(), 1); + check_transaction_in_list(pending_transactions, jon_transaction); + + trigger_transactions_applying(); + + pending_transactions = network_node_api.list_pending_transactions(); + BOOST_CHECK(pending_transactions.empty()); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(subscribe_to_pending_transactions) { + try { + graphene::app::network_node_api network_node_api(app); + + signed_transaction transaction_in_notification; + network_node_api.subscribe_to_pending_transactions([&]( const variant& signed_transaction_object ){ + transaction_in_notification = signed_transaction_object.as(); + }); + + auto sam_transaction = push_transaction_for_account_creation("sam"); + BOOST_CHECK(sam_transaction.id() == transaction_in_notification.id()); + + auto dan_transaction = push_transaction_for_account_creation("dan"); + BOOST_CHECK(dan_transaction.id() == transaction_in_notification.id()); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_CASE(unsubscribe_from_pending_transactions) { + try { + graphene::app::network_node_api network_node_api(app); + + network_node_api.subscribe_to_pending_transactions([&]( const variant& signed_transaction_object ){ + BOOST_FAIL("This callback should not be called, because subscription was canceled."); + }); + + network_node_api.unsubscribe_from_pending_transactions(); + + push_transaction_for_account_creation("sam"); + + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END()