-
Notifications
You must be signed in to change notification settings - Fork 648
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add cli wallet test framework #675
Changes from 9 commits
5b3ee10
8f2d468
bc6c470
c544f5a
4f6b0f8
f545941
218a073
b6a6dc3
11fae0a
9bd3e5b
aaee5ad
adde3fa
f8edc28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,388 @@ | ||
/* | ||
* Copyright (c) 2015 Cryptonomex, Inc., and contributors. | ||
* | ||
* 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 <graphene/app/application.hpp> | ||
#include <graphene/app/plugin.hpp> | ||
|
||
#include <graphene/utilities/tempdir.hpp> | ||
|
||
#include <graphene/account_history/account_history_plugin.hpp> | ||
#include <graphene/witness/witness.hpp> | ||
#include <graphene/market_history/market_history_plugin.hpp> | ||
#include <graphene/egenesis/egenesis.hpp> | ||
#include <graphene/wallet/wallet.hpp> | ||
|
||
#include <fc/thread/thread.hpp> | ||
#include <fc/smart_ref_impl.hpp> | ||
#include <fc/network/http/websocket.hpp> | ||
#include <fc/rpc/websocket_api.hpp> | ||
#include <fc/rpc/cli.hpp> | ||
|
||
#include <boost/filesystem/path.hpp> | ||
|
||
#define BOOST_TEST_MODULE Test Application | ||
#include <boost/test/included/unit_test.hpp> | ||
|
||
/********************* | ||
* Helper Methods | ||
*********************/ | ||
|
||
// hack: import create_example_genesis() even though it's a way, way | ||
// specific internal detail | ||
namespace graphene { namespace app { namespace detail { | ||
graphene::chain::genesis_state_type create_example_genesis(); | ||
} } } // graphene::app::detail | ||
|
||
boost::filesystem::path create_genesis_file(fc::temp_directory& directory) { | ||
boost::filesystem::path genesis_path = boost::filesystem::path{directory.path().generic_string()} / "genesis.json"; | ||
fc::path genesis_out = genesis_path; | ||
graphene::chain::genesis_state_type genesis_state = graphene::app::detail::create_example_genesis(); | ||
std::cerr << "Creating example genesis state in file " << genesis_out.generic_string() << "\n"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why output to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed |
||
fc::json::save_to_file(genesis_state, genesis_out); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After generated the genesis file, please replace all Public keys with a new random key, then use it in later tests. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am unfamiliar with the PKI architecture in Bitshares. I am working on familiarizing myself with this now. |
||
return genesis_path; | ||
} | ||
|
||
/*** | ||
* @brief Start the application, listening on port 8090 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please replace the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
* @param app_dir the temporary directory to use | ||
* @returns the application object | ||
*/ | ||
std::shared_ptr<graphene::app::application> start_application(fc::temp_directory& app_dir) { | ||
std::shared_ptr<graphene::app::application> app1(new graphene::app::application{}); | ||
|
||
app1->register_plugin<graphene::account_history::account_history_plugin>(); | ||
app1->register_plugin< graphene::market_history::market_history_plugin >(); | ||
app1->register_plugin< graphene::witness_plugin::witness_plugin >(); | ||
app1->startup_plugins(); | ||
boost::program_options::variables_map cfg; | ||
cfg.emplace("rpc-endpoint", boost::program_options::variable_value(string("127.0.0.1:8090"), false)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please use an available random port. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
cfg.emplace("genesis-json", boost::program_options::variable_value(create_genesis_file(app_dir), false)); | ||
cfg.emplace("seed-nodes", boost::program_options::variable_value(string("[]"), false)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No longer attempting to connect to seed nodes during test There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good, thanks. |
||
app1->initialize(app_dir.path(), cfg); | ||
|
||
app1->startup(); | ||
fc::usleep(fc::milliseconds(500)); | ||
return app1; | ||
} | ||
|
||
/**** | ||
* Send a block to the db | ||
* @param app the application | ||
* @returns true on success | ||
*/ | ||
bool generate_block(std::shared_ptr<graphene::app::application> app) { | ||
try { | ||
fc::ecc::private_key committee_key = fc::ecc::private_key::regenerate(fc::sha256::hash(string("nathan"))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can use the same method to generate a new key and replace the keys generated in genesis file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still working on this... |
||
auto db = app->chain_database(); | ||
auto block_1 = db->generate_block( | ||
db->get_slot_time(1), | ||
db->get_scheduled_witness(1), | ||
committee_key, | ||
database::skip_nothing); | ||
return true; | ||
} catch (exception &e) { | ||
return false; | ||
} | ||
} | ||
|
||
/**** | ||
* @brief Skip intermediate blocks, and generate a maintenance block | ||
* @param app the application | ||
* @returns true on success | ||
*/ | ||
bool generate_maintenance_block(std::shared_ptr<graphene::app::application> app) { | ||
try { | ||
fc::ecc::private_key committee_key = fc::ecc::private_key::regenerate(fc::sha256::hash(string("nathan"))); | ||
uint32_t skip = ~0; | ||
auto db = app->chain_database(); | ||
auto maint_time = db->get_dynamic_global_properties().next_maintenance_time; | ||
auto slots_to_miss = db->get_slot_at_time(maint_time); | ||
db->generate_block(db->get_slot_time(slots_to_miss), | ||
db->get_scheduled_witness(slots_to_miss), | ||
committee_key, | ||
skip); | ||
return true; | ||
} catch (exception& e) { | ||
return false; | ||
} | ||
} | ||
|
||
/**************************** | ||
* Tests | ||
****************************/ | ||
|
||
/** | ||
* Start a server and connect using the same calls as the CLI | ||
*/ | ||
BOOST_AUTO_TEST_CASE( cli_connect ) | ||
{ | ||
using namespace graphene::chain; | ||
using namespace graphene::app; | ||
std::shared_ptr<graphene::app::application> app1; | ||
try { | ||
fc::temp_directory app_dir ( graphene::utilities::temp_directory_path() ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please replace tabs with spaces. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3 spaces for indentation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again!?!??!! Cleaned up. |
||
|
||
app1 = start_application(app_dir); | ||
|
||
// connect to the server | ||
graphene::wallet::wallet_data wdata; | ||
wdata.chain_id = app1->chain_database()->get_chain_id(); | ||
wdata.ws_server = "ws://127.0.0.1:8090"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace the port with the chosen random port. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
wdata.ws_user = ""; | ||
wdata.ws_password = ""; | ||
fc::http::websocket_client client; | ||
|
||
auto apic = std::make_shared<fc::rpc::websocket_api_connection>(*(client.connect(wdata.ws_server))); | ||
|
||
auto remote_api = apic->get_remote_api< login_api >(1); | ||
BOOST_CHECK(remote_api->login( wdata.ws_user, wdata.ws_password ) ); | ||
|
||
} catch( fc::exception& e ) { | ||
edump((e.to_detail_string())); | ||
throw; | ||
} | ||
app1->shutdown(); | ||
} | ||
|
||
/** | ||
* Start a server and connect using the same calls as the CLI | ||
* Vote for two witnesses, and make sure they both stay there | ||
* after a maintenance block | ||
*/ | ||
BOOST_AUTO_TEST_CASE( cli_vote_for_2_witnesses ) | ||
{ | ||
using namespace graphene::chain; | ||
using namespace graphene::app; | ||
std::shared_ptr<graphene::app::application> app1; | ||
try { | ||
|
||
fc::temp_directory app_dir( graphene::utilities::temp_directory_path() ); | ||
|
||
app1 = start_application(app_dir); | ||
|
||
// connect to the server | ||
graphene::wallet::wallet_data wdata; | ||
wdata.chain_id = app1->chain_database()->get_chain_id(); | ||
wdata.ws_server = "ws://127.0.0.1:8090"; | ||
wdata.ws_user = ""; | ||
wdata.ws_password = ""; | ||
fc::http::websocket_client client; | ||
auto con = client.connect( wdata.ws_server ); | ||
|
||
auto apic = std::make_shared<fc::rpc::websocket_api_connection>(*con); | ||
|
||
auto remote_api = apic->get_remote_api< login_api >(1); | ||
BOOST_CHECK(remote_api->login( wdata.ws_user, wdata.ws_password ) ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please reuse the code. Almost every test case need to connect first. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created a class that connects and provides all connectivity objects |
||
|
||
auto wapiptr = std::make_shared<graphene::wallet::wallet_api>(wdata, remote_api); | ||
std::stringstream wallet_filename; | ||
wallet_filename << app_dir.path().generic_string() << "/wallet.json"; | ||
wapiptr->set_wallet_filename(wallet_filename.str()); | ||
|
||
fc::api<graphene::wallet::wallet_api> wapi(wapiptr); | ||
|
||
auto wallet_cli = std::make_shared<fc::rpc::cli>(); | ||
for( auto& name_formatter : wapiptr->get_result_formatters() ) | ||
wallet_cli->format_result( name_formatter.first, name_formatter.second ); | ||
|
||
boost::signals2::scoped_connection closed_connection(con->closed.connect([=]{ | ||
cerr << "Server has disconnected us.\n"; | ||
wallet_cli->stop(); | ||
})); | ||
(void)(closed_connection); | ||
|
||
BOOST_TEST_MESSAGE("Setting wallet password"); | ||
wapiptr->set_password("supersecret"); | ||
wapiptr->unlock("supersecret"); | ||
|
||
// import Nathan account | ||
BOOST_TEST_MESSAGE("Importing nathan key"); | ||
std::vector<std::string> nathan_keys{"5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"}; | ||
BOOST_CHECK_EQUAL(nathan_keys[0], "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"); | ||
BOOST_CHECK(wapiptr->import_key("nathan", nathan_keys[0])); | ||
|
||
BOOST_TEST_MESSAGE("Importing nathan's balance"); | ||
std::vector<signed_transaction> import_txs = wapiptr->import_balance("nathan", nathan_keys, true); | ||
account_object nathan_acct_before_upgrade = wapiptr->get_account("nathan"); | ||
|
||
// upgrade nathan | ||
BOOST_TEST_MESSAGE("Upgrading Nathan to LTM"); | ||
signed_transaction upgrade_tx = wapiptr->upgrade_account("nathan", true); | ||
account_object nathan_acct_after_upgrade = wapiptr->get_account("nathan"); | ||
|
||
// verify that the upgrade was successful | ||
BOOST_CHECK_PREDICATE( std::not_equal_to<uint32_t>(), (nathan_acct_before_upgrade.membership_expiration_date.sec_since_epoch())(nathan_acct_after_upgrade.membership_expiration_date.sec_since_epoch()) ); | ||
BOOST_CHECK(nathan_acct_after_upgrade.is_lifetime_member()); | ||
|
||
// create a new account | ||
graphene::wallet::brain_key_info bki = wapiptr->suggest_brain_key(); | ||
BOOST_CHECK(!bki.brain_priv_key.empty()); | ||
signed_transaction create_acct_tx = wapiptr->create_account_with_brain_key(bki.brain_priv_key, "jmjatlanta", "nathan", "nathan", true); | ||
// save the private key for this new account in the wallet file | ||
BOOST_CHECK(wapiptr->import_key("jmjatlanta", bki.wif_priv_key)); | ||
wapiptr->save_wallet_file(wallet_filename.str()); | ||
|
||
// attempt to give jmjatlanta some bitsahres | ||
BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to jmjatlanta"); | ||
signed_transaction transfer_tx = wapiptr->transfer("nathan", "jmjatlanta", "10000", "BTS", "Here are some BTS for your new account", true); | ||
|
||
// get the details for init1 | ||
witness_object init1_obj = wapiptr->get_witness("init1"); | ||
int init1_start_votes = init1_obj.total_votes; | ||
// Vote for a witness | ||
signed_transaction vote_witness1_tx = wapiptr->vote_for_witness("jmjatlanta", "init1", true, true); | ||
|
||
// generate a block to get things started | ||
BOOST_CHECK(generate_block(app1)); | ||
// wait for a maintenance interval | ||
BOOST_CHECK(generate_maintenance_block(app1)); | ||
|
||
// Verify that the vote is there | ||
init1_obj = wapiptr->get_witness("init1"); | ||
witness_object init2_obj = wapiptr->get_witness("init2"); | ||
int init1_middle_votes = init1_obj.total_votes; | ||
BOOST_CHECK(init1_middle_votes > init1_start_votes); | ||
|
||
// Vote for a 2nd witness | ||
int init2_start_votes = init2_obj.total_votes; | ||
signed_transaction vote_witness2_tx = wapiptr->vote_for_witness("jmjatlanta", "init2", true, true); | ||
|
||
// send another block to trigger maintenance interval | ||
BOOST_CHECK(generate_maintenance_block(app1)); | ||
|
||
// Verify that both the first vote and the 2nd are there | ||
init2_obj = wapiptr->get_witness("init2"); | ||
init1_obj = wapiptr->get_witness("init1"); | ||
|
||
int init2_middle_votes = init2_obj.total_votes; | ||
BOOST_CHECK(init2_middle_votes > init2_start_votes); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this test is failing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a check to make sure the maintenance interval is set to a low value for this test. Is there a way to adjust the maintenance interval at run time? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this can be done by another way, check for example: in there, block production advances until a hardfork time, you can play with that in order to advance the chain to the time you need. it will be something like: vote for witness looking good :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a modified version of what is in database_fixture. No more playing with the default maintenance interval |
||
int init1_last_votes = init1_obj.total_votes; | ||
BOOST_CHECK(init1_last_votes > init1_start_votes); | ||
|
||
// wait for everything to finish up | ||
fc::usleep(fc::seconds(1)); | ||
} catch( fc::exception& e ) { | ||
edump((e.to_detail_string())); | ||
throw; | ||
} | ||
app1->shutdown(); | ||
} | ||
|
||
/** | ||
* Start a server and connect using the same calls as the CLI | ||
* Set a voting proxy and be assured that it sticks | ||
*/ | ||
BOOST_AUTO_TEST_CASE( cli_set_voting_proxy ) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Separated the proxy into its own test |
||
{ | ||
using namespace graphene::chain; | ||
using namespace graphene::app; | ||
std::shared_ptr<graphene::app::application> app1; | ||
try { | ||
|
||
fc::temp_directory app_dir( graphene::utilities::temp_directory_path() ); | ||
|
||
app1 = start_application(app_dir); | ||
|
||
// connect to the server | ||
graphene::wallet::wallet_data wdata; | ||
wdata.chain_id = app1->chain_database()->get_chain_id(); | ||
wdata.ws_server = "ws://127.0.0.1:8090"; | ||
wdata.ws_user = ""; | ||
wdata.ws_password = ""; | ||
fc::http::websocket_client client; | ||
auto con = client.connect( wdata.ws_server ); | ||
|
||
auto apic = std::make_shared<fc::rpc::websocket_api_connection>(*con); | ||
|
||
auto remote_api = apic->get_remote_api< login_api >(1); | ||
BOOST_CHECK(remote_api->login( wdata.ws_user, wdata.ws_password ) ); | ||
|
||
auto wapiptr = std::make_shared<graphene::wallet::wallet_api>(wdata, remote_api); | ||
std::stringstream wallet_filename; | ||
wallet_filename << app_dir.path().generic_string() << "/wallet.json"; | ||
wapiptr->set_wallet_filename(wallet_filename.str()); | ||
|
||
fc::api<graphene::wallet::wallet_api> wapi(wapiptr); | ||
|
||
auto wallet_cli = std::make_shared<fc::rpc::cli>(); | ||
for( auto& name_formatter : wapiptr->get_result_formatters() ) | ||
wallet_cli->format_result( name_formatter.first, name_formatter.second ); | ||
|
||
boost::signals2::scoped_connection closed_connection(con->closed.connect([=]{ | ||
cerr << "Server has disconnected us.\n"; | ||
wallet_cli->stop(); | ||
})); | ||
(void)(closed_connection); | ||
|
||
BOOST_TEST_MESSAGE("Setting wallet password"); | ||
wapiptr->set_password("supersecret"); | ||
wapiptr->unlock("supersecret"); | ||
|
||
// import Nathan account | ||
BOOST_TEST_MESSAGE("Importing nathan key"); | ||
std::vector<std::string> nathan_keys{"5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"}; | ||
BOOST_CHECK_EQUAL(nathan_keys[0], "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"); | ||
BOOST_CHECK(wapiptr->import_key("nathan", nathan_keys[0])); | ||
|
||
BOOST_TEST_MESSAGE("Importing nathan's balance"); | ||
std::vector<signed_transaction> import_txs = wapiptr->import_balance("nathan", nathan_keys, true); | ||
account_object nathan_acct_before_upgrade = wapiptr->get_account("nathan"); | ||
|
||
// upgrade nathan | ||
BOOST_TEST_MESSAGE("Upgrading Nathan to LTM"); | ||
signed_transaction upgrade_tx = wapiptr->upgrade_account("nathan", true); | ||
account_object nathan_acct_after_upgrade = wapiptr->get_account("nathan"); | ||
|
||
// verify that the upgrade was successful | ||
BOOST_CHECK_PREDICATE( std::not_equal_to<uint32_t>(), (nathan_acct_before_upgrade.membership_expiration_date.sec_since_epoch())(nathan_acct_after_upgrade.membership_expiration_date.sec_since_epoch()) ); | ||
BOOST_CHECK(nathan_acct_after_upgrade.is_lifetime_member()); | ||
|
||
// create a new account | ||
graphene::wallet::brain_key_info bki = wapiptr->suggest_brain_key(); | ||
BOOST_CHECK(!bki.brain_priv_key.empty()); | ||
signed_transaction create_acct_tx = wapiptr->create_account_with_brain_key(bki.brain_priv_key, "jmjatlanta", "nathan", "nathan", true); | ||
// save the private key for this new account in the wallet file | ||
BOOST_CHECK(wapiptr->import_key("jmjatlanta", bki.wif_priv_key)); | ||
wapiptr->save_wallet_file(wallet_filename.str()); | ||
|
||
// attempt to give jmjatlanta some bitsahres | ||
BOOST_TEST_MESSAGE("Transferring bitshares from Nathan to jmjatlanta"); | ||
signed_transaction transfer_tx = wapiptr->transfer("nathan", "jmjatlanta", "10000", "BTS", "Here are some BTS for your new account", true); | ||
|
||
// grab account for comparison | ||
account_object prior_voting_account = wapiptr->get_account("jmjatlanta"); | ||
// set the voting proxy to nathan | ||
BOOST_TEST_MESSAGE("About to set voting proxy."); | ||
signed_transaction voting_tx = wapiptr->set_voting_proxy("jmjatlanta", "nathan", true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make test to check if the proxy changed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually, i think they should be separated test than the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created a new test for testing voting proxy |
||
account_object after_voting_account = wapiptr->get_account("jmjatlanta"); | ||
// see if it changed | ||
BOOST_CHECK(prior_voting_account.options.voting_account != after_voting_account.options.voting_account); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please confirm the proxy is now |
||
// wait for everything to finish up | ||
fc::usleep(fc::seconds(1)); | ||
} catch( fc::exception& e ) { | ||
edump((e.to_detail_string())); | ||
throw; | ||
} | ||
app1->shutdown(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO better replace Cryptonomex with your own name and replace year to 2018.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done