diff --git a/tonlib/tonlib/tonlib-cli.cpp b/tonlib/tonlib/tonlib-cli.cpp new file mode 100644 index 00000000..a12b0d53 --- /dev/null +++ b/tonlib/tonlib/tonlib-cli.cpp @@ -0,0 +1,593 @@ +#include "td/actor/actor.h" + +#include "td/utils/filesystem.h" +#include "td/utils/OptionsParser.h" +#include "td/utils/Parser.h" +#include "td/utils/port/signals.h" +#include "td/utils/port/path.h" + +#include "terminal/terminal.h" + +#include "tonlib/TonlibClient.h" +#include "tonlib/TonlibCallback.h" + +#include +#include + +class TonlibCli : public td::actor::Actor { + public: + struct Options { + bool enable_readline{true}; + std::string config; + std::string key_dir{"."}; + }; + TonlibCli(Options options) : options_(std::move(options)) { + } + + private: + Options options_; + td::actor::ActorOwn io_; + td::actor::ActorOwn client_; + std::uint64_t next_query_id_{1}; + td::Promise cont_; + + struct KeyInfo { + std::string public_key; + td::SecureString secret; + }; + std::vector keys_; + + std::map>> query_handlers_; + + bool is_closing_{false}; + td::uint32 ref_cnt_{1}; + + void start_up() override { + class Cb : public td::TerminalIO::Callback { + public: + void line_cb(td::BufferSlice line) override { + td::actor::send_closure(id_, &TonlibCli::parse_line, std::move(line)); + } + Cb(td::actor::ActorShared id) : id_(std::move(id)) { + } + + private: + td::actor::ActorShared id_; + }; + ref_cnt_++; + io_ = td::TerminalIO::create("> ", options_.enable_readline, std::make_unique(actor_shared(this))); + td::actor::send_closure(io_, &td::TerminalIO::set_log_interface); + + class TonlibCb : public tonlib::TonlibCallback { + public: + TonlibCb(td::actor::ActorShared id) : id_(std::move(id)) { + } + void on_result(std::uint64_t id, tonlib_api::object_ptr result) override { + send_closure(id_, &TonlibCli::on_tonlib_result, id, std::move(result)); + } + void on_error(std::uint64_t id, tonlib_api::object_ptr error) override { + send_closure(id_, &TonlibCli::on_tonlib_error, id, std::move(error)); + } + + private: + td::actor::ActorShared id_; + }; + ref_cnt_++; + client_ = td::actor::create_actor("Tonlib", td::make_unique(actor_shared(this))); + + td::mkdir(options_.key_dir).ignore(); + + load_keys(); + + using tonlib_api::make_object; + send_query(make_object(make_object(options_.config, options_.key_dir)), + [](auto r_ok) { + LOG_IF(ERROR, r_ok.is_error()) << r_ok.error(); + td::TerminalIO::out() << "Tonlib is inited\n"; + }); + } + void hangup_shared() override { + CHECK(ref_cnt_ > 0); + ref_cnt_--; + try_stop(); + } + void try_stop() { + if (is_closing_ && ref_cnt_ == 0) { + stop(); + } + } + void tear_down() override { + td::actor::SchedulerContext::get()->stop(); + } + + void parse_line(td::BufferSlice line) { + if (is_closing_) { + return; + } + if (cont_) { + auto cont = std::move(cont_); + cont.set_value(line.as_slice()); + return; + } + td::ConstParser parser(line.as_slice()); + auto cmd = parser.read_word(); + if (cmd.empty()) { + return; + } + if (cmd == "help") { + td::TerminalIO::out() << "help - show this help\n"; + td::TerminalIO::out() << "genkey - generate new secret key\n"; + td::TerminalIO::out() << "keys - show all stored keys\n"; + td::TerminalIO::out() << "exportkey [key_id] - export key\n"; + td::TerminalIO::out() << "setconfig - set lite server config\n"; + td::TerminalIO::out() << "getstate - get state of simple wallet with requested key\n"; + td::TerminalIO::out() << "init - init simple wallet with requested key\n"; + td::TerminalIO::out() << "transfer - transfer of grams from " + " to .\n" + << "\t could also be 'giver'\n" + << "\t could also be 'giver' or smartcontract address\n"; + td::TerminalIO::out() << "exit - exit from this programm\n"; + } else if (cmd == "genkey") { + generate_key(); + } else if (cmd == "exit") { + is_closing_ = true; + io_.reset(); + client_.reset(); + ref_cnt_--; + try_stop(); + } else if (cmd == "keys") { + dump_keys(); + } else if (cmd == "exportkey") { + export_key(parser.read_word()); + } else if (cmd == "importkey") { + import_key(parser.read_all()); + } else if (cmd == "setconfig") { + set_config(parser.read_word()); + } else if (cmd == "getstate") { + get_state(parser.read_word()); + } else if (cmd == "init") { + init_simple_wallet(parser.read_word()); + } else if (cmd == "transfer") { + auto from = parser.read_word(); + auto to = parser.read_word(); + auto grams = parser.read_word(); + transfer(from, to, grams); + } + } + + void on_tonlib_result(std::uint64_t id, tonlib_api::object_ptr result) { + auto it = query_handlers_.find(id); + if (it == query_handlers_.end()) { + return; + } + auto promise = std::move(it->second); + query_handlers_.erase(it); + promise.set_value(std::move(result)); + } + + void on_tonlib_error(std::uint64_t id, tonlib_api::object_ptr error) { + auto it = query_handlers_.find(id); + if (it == query_handlers_.end()) { + return; + } + auto promise = std::move(it->second); + query_handlers_.erase(it); + promise.set_error(td::Status::Error(error->code_, error->message_)); + } + + template + void send_query(tonlib_api::object_ptr query, td::Promise promise) { + if (is_closing_) { + return; + } + auto query_id = next_query_id_++; + td::actor::send_closure(client_, &tonlib::TonlibClient::request, query_id, std::move(query)); + query_handlers_[query_id] = + [promise = std::move(promise)](td::Result> r_obj) mutable { + if (r_obj.is_error()) { + return promise.set_error(r_obj.move_as_error()); + } + promise.set_value(ton::move_tl_object_as(r_obj.move_as_ok())); + }; + } + + void generate_key(std::string entropy = "") { + if (entropy.size() < 20) { + td::TerminalIO::out() << "Enter some entropy"; + cont_ = [this, entropy](td::Slice new_entropy) { generate_key(entropy + new_entropy.str()); }; + return; + } + td::TerminalIO::out() << "Enter password (could be empty)"; + cont_ = [this, entropy](td::Slice password) { generate_key(std::move(entropy), td::SecureString(password)); }; + } + + void generate_key(std::string entropy, td::SecureString password) { + //TODO: use entropy + auto password_copy = password.copy(); + send_query(tonlib_api::make_object(std::move(password_copy), + td::SecureString() /*mnemonic password*/), + [this, password = std::move(password)](auto r_key) mutable { + if (r_key.is_error()) { + LOG(ERROR) << "Failed to create new key: " << r_key.error(); + } + auto key = r_key.move_as_ok(); + LOG(ERROR) << to_string(key); + KeyInfo info; + info.public_key = key->public_key_; + info.secret = std::move(key->secret_); + keys_.push_back(std::move(info)); + export_key(info.public_key, keys_.size() - 1, std::move(password)); + store_keys(); + }); + } + + void store_keys() { + td::SecureString buf(10000); + td::StringBuilder sb(buf.as_mutable_slice()); + for (auto& info : keys_) { + sb << td::base64_encode(info.public_key) << " " << td::base64_encode(info.secret) << "\n"; + } + LOG_IF(FATAL, sb.is_error()) << "StringBuilder overflow"; + td::atomic_write_file(key_db_path(), sb.as_cslice()); + } + + void load_keys() { + auto r_db = td::read_file_secure(key_db_path()); + if (r_db.is_error()) { + return; + } + auto db = r_db.move_as_ok(); + td::ConstParser parser(db.as_slice()); + while (true) { + auto public_key_b64 = parser.read_word(); + auto secret_b64 = parser.read_word(); + if (secret_b64.empty()) { + break; + } + auto r_public_key = td::base64_decode(public_key_b64); + auto r_secret = td::base64_decode_secure(secret_b64); + if (r_public_key.is_error() || r_secret.is_error()) { + LOG(ERROR) << "Invalid key database at " << key_db_path(); + } + + KeyInfo info; + info.public_key = r_public_key.move_as_ok(); + info.secret = r_secret.move_as_ok(); + LOG(INFO) << td::buffer_to_hex(info.public_key); + + keys_.push_back(std::move(info)); + } + } + + void dump_keys() { + td::TerminalIO::out() << "Got " << keys_.size() << " keys" + << "\n"; + for (size_t i = 0; i < keys_.size(); i++) { + td::TerminalIO::out() << " #" << i << ": " << td::buffer_to_hex(keys_[i].public_key) << "\n"; + } + } + + std::string key_db_path() { + return options_.key_dir + TD_DIR_SLASH + "key_db"; + } + + td::Result to_key_i(td::Slice key) { + if (key.empty()) { + return td::Status::Error("Empty key id"); + } + if (key[0] == '#') { + TRY_RESULT(res, td::to_integer_safe(key.substr(1))); + if (res < keys_.size()) { + return res; + } + return td::Status::Error("Invalid key id"); + } + auto r_res = td::to_integer_safe(key); + if (r_res.is_ok() && r_res.ok() < keys_.size()) { + return r_res.ok(); + } + if (key.size() < 3) { + return td::Status::Error("Too short key id"); + } + + auto prefix = td::to_lower(key); + size_t res = 0; + size_t cnt = 0; + for (size_t i = 0; i < keys_.size(); i++) { + auto full_key = td::to_lower(td::buffer_to_hex(keys_[i].public_key)); + if (td::begins_with(full_key, prefix)) { + res = i; + cnt++; + } + } + if (cnt == 0) { + return td::Status::Error("Unknown key prefix"); + } + if (cnt > 1) { + return td::Status::Error("Non unique key prefix"); + } + return res; + } + + struct Address { + tonlib_api::object_ptr address; + std::string public_key; + td::SecureString secret; + }; + + td::Result
to_account_address(td::Slice key, bool need_private_key) { + if (key.empty()) { + return td::Status::Error("account address is empty"); + } + auto r_key_i = to_key_i(key); + using tonlib_api::make_object; + if (r_key_i.is_ok()) { + auto obj = tonlib::TonlibClient::static_request(make_object( + make_object(keys_[r_key_i.ok()].public_key))); + if (obj->get_id() != tonlib_api::error::ID) { + Address res; + res.address = ton::move_tl_object_as(obj); + res.public_key = keys_[r_key_i.ok()].public_key; + res.secret = keys_[r_key_i.ok()].secret.copy(); + return std::move(res); + } + } + if (key == "giver") { + auto obj = tonlib::TonlibClient::static_request(make_object()); + if (obj->get_id() != tonlib_api::error::ID) { + Address res; + res.address = ton::move_tl_object_as(obj); + return std::move(res); + } else { + LOG(ERROR) << "Unexpected error during testGiver_getAccountAddress: " << to_string(obj); + } + } + if (need_private_key) { + return td::Status::Error("Don't have a private key for this address"); + } + //TODO: validate address + Address res; + res.address = make_object(key.str()); + return std::move(res); + } + + void export_key(td::Slice key) { + if (key.empty()) { + dump_keys(); + td::TerminalIO::out() << "Choose public key (hex prefix or #N)"; + cont_ = [this](td::Slice key) { this->export_key(key); }; + return; + } + auto r_key_i = to_key_i(key); + if (r_key_i.is_error()) { + td::TerminalIO::out() << "Unknown key id: [" << key << "]\n"; + return; + } + auto key_i = r_key_i.move_as_ok(); + + td::TerminalIO::out() << "Key #" << key_i << "\n" + << "public key: " << td::buffer_to_hex(keys_[key_i].public_key) << "\n"; + + td::TerminalIO::out() << "Enter password (could be empty)"; + cont_ = [this, key = key.str(), key_i](td::Slice password) { this->export_key(key, key_i, password); }; + } + + void export_key(std::string key, size_t key_i, td::Slice password) { + using tonlib_api::make_object; + send_query(make_object(make_object( + make_object(keys_[key_i].public_key, keys_[key_i].secret.copy()), + td::SecureString(password))), + [key = std::move(key)](auto r_res) { + if (r_res.is_error()) { + td::TerminalIO::out() << "Can't export key id: [" << key << "] " << r_res.error() << "\n"; + return; + } + td::TerminalIO::out() << to_string(r_res.ok()); + }); + } + + void import_key(td::Slice slice, std::vector words = {}) { + td::ConstParser parser(slice); + while (true) { + auto word = parser.read_word(); + if (word.empty()) { + break; + } + words.push_back(td::SecureString(word)); + } + if (words.size() < 24) { + td::TerminalIO::out() << "Enter mnemonic words (got " << words.size() << " out of 24)"; + cont_ = [this, words = std::move(words)](td::Slice slice) mutable { this->import_key(slice, std::move(words)); }; + return; + } + td::TerminalIO::out() << "Enter password (could be empty)"; + cont_ = [this, words = std::move(words)](td::Slice password) mutable { + this->import_key(std::move(words), password); + }; + } + + void import_key(std::vector words, td::Slice password) { + using tonlib_api::make_object; + send_query(make_object(td::SecureString(password), td::SecureString(), + make_object(std::move(words))), + [](auto r_res) { + if (r_res.is_error()) { + td::TerminalIO::out() << "Can't import key " << r_res.error() << "\n"; + return; + } + td::TerminalIO::out() << to_string(r_res.ok()); + }); + } + + void set_config(td::Slice path) { + auto r_data = td::read_file_str(path.str()); + if (r_data.is_error()) { + td::TerminalIO::out() << "Can't read file [" << path << "] : " << r_data.error() << "\n"; + return; + } + + auto data = r_data.move_as_ok(); + using tonlib_api::make_object; + send_query(make_object(data), [](auto r_res) { + if (r_res.is_error()) { + td::TerminalIO::out() << "Can't set config: " << r_res.error() << "\n"; + return; + } + td::TerminalIO::out() << to_string(r_res.ok()); + }); + } + + void get_state(td::Slice key) { + if (key.empty()) { + dump_keys(); + td::TerminalIO::out() << "Choose public key (hex prefix or #N)"; + cont_ = [this](td::Slice key) { this->get_state(key); }; + return; + } + auto r_key_i = to_key_i(key); + if (r_key_i.is_error()) { + td::TerminalIO::out() << "Unknown key id: [" << key << "]\n"; + return; + } + auto key_i = r_key_i.move_as_ok(); + using tonlib_api::make_object; + auto obj = tonlib::TonlibClient::static_request(make_object( + make_object(keys_[key_i].public_key))); + if (obj->get_id() == tonlib_api::error::ID) { + td::TerminalIO::out() << "Can't get state of [" << key << "] : " << to_string(obj); + } + send_query( + make_object(ton::move_tl_object_as(obj)), + [](auto r_res) { + if (r_res.is_error()) { + td::TerminalIO::out() << "Can't get state: " << r_res.error() << "\n"; + return; + } + td::TerminalIO::out() << to_string(r_res.ok()); + }); + } + + void transfer(td::Slice from, td::Slice to, td::Slice grams) { + auto r_from_address = to_account_address(from, true); + if (r_from_address.is_error()) { + td::TerminalIO::out() << "Unknown key id: [" << from << "] : " << r_from_address.error() << "\n"; + return; + } + auto r_to_address = to_account_address(to, false); + if (r_to_address.is_error()) { + td::TerminalIO::out() << "Unknown key id: [" << to << "] : " << r_to_address.error() << "\n"; + return; + } + auto r_grams = td::to_integer_safe(grams); + if (r_grams.is_error()) { + td::TerminalIO::out() << "Invalid grams amount: [" << grams << "]\n"; + return; + } + if (from != "giver") { + td::TerminalIO::out() << "Enter password (could be empty)"; + cont_ = [this, from = r_from_address.move_as_ok(), to = r_to_address.move_as_ok(), grams = r_grams.move_as_ok()]( + td::Slice password) mutable { this->transfer(std::move(from), std::move(to), grams, password); }; + return; + } + } + + void transfer(Address from, Address to, td::uint64 grams, td::Slice password) { + using tonlib_api::make_object; + auto key = !from.secret.empty() + ? make_object( + make_object(from.public_key, from.secret.copy()), td::SecureString(password)) + : nullptr; + send_query(make_object(std::move(key), std::move(from.address), + std::move(to.address), grams), + [](auto r_res) { + if (r_res.is_error()) { + td::TerminalIO::out() << "Can't get state: " << r_res.error() << "\n"; + return; + } + td::TerminalIO::out() << to_string(r_res.ok()); + }); + } + + void init_simple_wallet(td::Slice key) { + if (key.empty()) { + dump_keys(); + td::TerminalIO::out() << "Choose public key (hex prefix or #N)"; + cont_ = [this](td::Slice key) { this->init_simple_wallet(key); }; + return; + } + auto r_key_i = to_key_i(key); + if (r_key_i.is_error()) { + td::TerminalIO::out() << "Unknown key id: [" << key << "]\n"; + return; + } + auto key_i = r_key_i.move_as_ok(); + + td::TerminalIO::out() << "Key #" << key_i << "\n" + << "public key: " << td::buffer_to_hex(keys_[key_i].public_key) << "\n"; + + td::TerminalIO::out() << "Enter password (could be empty)"; + cont_ = [this, key = key.str(), key_i](td::Slice password) { this->init_simple_wallet(key, key_i, password); }; + } + + void init_simple_wallet(std::string key, size_t key_i, td::Slice password) { + using tonlib_api::make_object; + send_query(make_object(make_object( + make_object(keys_[key_i].public_key, keys_[key_i].secret.copy()), + td::SecureString(password))), + [key = std::move(key)](auto r_res) { + if (r_res.is_error()) { + td::TerminalIO::out() << "Can't init wallet with key: [" << key << "] " << r_res.error() << "\n"; + return; + } + td::TerminalIO::out() << to_string(r_res.ok()); + }); + } +}; + +int main(int argc, char* argv[]) { + using tonlib_api::make_object; + SET_VERBOSITY_LEVEL(verbosity_INFO); + td::set_default_failure_signal_handler(); + + td::OptionsParser p; + TonlibCli::Options options; + p.set_description("console for validator for TON Blockchain"); + p.add_option('h', "help", "prints_help", [&]() { + std::cout << (PSLICE() << p).c_str(); + std::exit(2); + return td::Status::OK(); + }); + p.add_option('r', "disable-readline", "disable readline", [&]() { + options.enable_readline = false; + return td::Status::OK(); + }); + p.add_option('R', "enable-readline", "enable readline", [&]() { + options.enable_readline = true; + return td::Status::OK(); + }); + p.add_option('D', "directory", "set keys directory", [&](td::Slice arg) { + options.key_dir = arg.str(); + return td::Status::OK(); + }); + p.add_option('v', "verbosity", "set verbosity level", [&](td::Slice arg) { + auto verbosity = td::to_integer(arg); + SET_VERBOSITY_LEVEL(VERBOSITY_NAME(FATAL) + verbosity); + return (verbosity >= 0 && verbosity <= 20) ? td::Status::OK() : td::Status::Error("verbosity must be 0..20"); + }); + p.add_option('C', "config", "set lite server config", [&](td::Slice arg) { + TRY_RESULT(data, td::read_file_str(arg.str())); + options.config = std::move(data); + return td::Status::OK(); + }); + + auto S = p.run(argc, argv); + if (S.is_error()) { + std::cerr << S.move_as_error().message().str() << std::endl; + std::_Exit(2); + } + + td::actor::Scheduler scheduler({2}); + scheduler.run_in_context([&] { td::actor::create_actor("console", options).release(); }); + scheduler.run(); + return 0; +}