Meta Update:

* Completed Account Handling
  * Added QR Code Generation support
  * Added Tx Send Support WITHOUT address book
  * Bump version of lwsf
This commit is contained in:
Lee Clagett
2025-05-16 21:00:21 -04:00
parent 50ff86c56e
commit fbd694f55b
15 changed files with 814 additions and 136 deletions
+1 -1
+31 -17
View File
@@ -41,7 +41,7 @@ namespace lwcli { namespace component
class table_ final : public ftxui::ComponentBase
{
const table_generator generator_;
const table_enter enter_;
const table_on_key key_;
const std::vector<std::vector<std::string>> title_;
std::ptrdiff_t rows_;
std::ptrdiff_t selected_;
@@ -62,10 +62,10 @@ namespace lwcli { namespace component
}
public:
explicit table_(std::vector<std::string>&& title, table_generator&& generator, table_enter&& enter)
explicit table_(std::vector<std::string>&& title, table_generator&& generator, table_on_key&& key)
: ftxui::ComponentBase(),
generator_(std::move(generator)),
enter_(std::move(enter)),
key_(std::move(key)),
title_(get_title_bar(std::move(title))),
rows_(0),
selected_(-1)
@@ -80,18 +80,31 @@ namespace lwcli { namespace component
rows_ = rows;
}
bool can_increment() const noexcept
{ return selected_ + min_row() < rows_; }
bool can_decrement() const noexcept
{ return min_row() <= selected_; }
bool OnEvent(ftxui::Event event) override final
{
if (event == ftxui::Event::ArrowDown && selected_ + min_row() < rows_)
if (event.is_mouse())
{
if (event.mouse().button == ftxui::Mouse::WheelDown && can_increment())
++selected_;
else if (event.mouse().button == ftxui::Mouse::WheelUp && can_decrement())
--selected_;// = std::max(min_row(), selected_ - 1);
}
else if (event == ftxui::Event::ArrowDown && can_increment())
++selected_;
else if (event == ftxui::Event::ArrowUp && min_row() <= selected_)
else if (event == ftxui::Event::ArrowUp && can_decrement())
--selected_;
else if (event == ftxui::Event::PageDown)
selected_ = std::min(rows_ + min_row(), selected_ + 15);
else if (event == ftxui::Event::PageUp)
selected_ = std::max(min_row(), selected_ - 15);
else if (event == ftxui::Event::Return && min_row() <= selected_ && selected_ < rows_)
return enter_(std::size_t(selected_));
else if (min_row() <= selected_ && selected_ < rows_)
return key_(std::move(event), std::size_t(selected_));
return min_row() <= selected_ && selected_ - min_row() < rows_;
}
@@ -119,23 +132,24 @@ namespace lwcli { namespace component
selected_ = min_row();
else if (rows_ <= selected_ - min_row())
selected_ = rows_ - 1 + min_row();
if (min_row() <= selected_ && selected_ < rows_)
{
auto row = table.SelectRow(std::size_t(selected_) + title_.size());
row.Decorate(ftxui::inverted);
row.Decorate(ftxui::focus);
}
}
if (min_row() <= selected_ && selected_ < rows_)
{
auto row = table.SelectRow(std::size_t(selected_) + title_.size());
row.Decorate(ftxui::inverted);
row.Decorate(ftxui::focus);
}
return table.Render();
}
};
} // anonymous
ftxui::Component table(std::vector<std::string> title, table_generator generator, table_enter enter)
ftxui::Component table(std::vector<std::string> title, table_generator generator, table_on_key key)
{
if (!generator || !enter)
if (!generator || !key)
throw std::invalid_argument{"lwcli::components::table was given nullptr"};
return std::make_shared<table_>(std::move(title), std::move(generator), std::move(enter));
return std::make_shared<table_>(std::move(title), std::move(generator), std::move(key));
}
}} // lwcli // view
+2 -2
View File
@@ -39,8 +39,8 @@ namespace lwcli { namespace component
{
//! Shows Transaction History
using table_generator = std::function<std::vector<std::vector<std::string>>()>;
using table_enter = std::function<bool(std::size_t)>;
ftxui::Component table(std::vector<std::string> title, table_generator generator, table_enter enter);
using table_on_key = std::function<bool(ftxui::Event, std::size_t)>;
ftxui::Component table(std::vector<std::string> title, table_generator generator, table_on_key key);
}} // lwscli // view
+1
View File
@@ -34,4 +34,5 @@ namespace lwcli { namespace event
string optmization. */
const ftxui::Event lock_wallet = ftxui::Event::Special("lwcli.lockw");
const ftxui::Event refresh_wallet = ftxui::Event::Special("lwcli.refresh");
const ftxui::Event send_async = ftxui::Event::Special("lwcli.sendasync");
}}
+1 -1
View File
@@ -46,5 +46,5 @@ namespace lwcli { namespace event
extern const ftxui::Event lock_wallet;
extern const ftxui::Event refresh_wallet;
extern const ftxui::Event send_async;
}} // lwcli // event
-1
View File
@@ -51,7 +51,6 @@ namespace lwcli { namespace config
constexpr const std::string_view default_language{"English"};
constexpr const std::string_view default_account_name{"Untitled Account"};
constexpr const std::string_view default_subaddress_name{"Untitled Address"};
//! Timeout interval for inactivity with open wallet
constexpr const std::chrono::minutes wallet_timeout{2};
+2 -2
View File
@@ -26,8 +26,8 @@
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
set(lwcli-views_sources accounts.cpp history.cpp keys.cpp manager.cpp settings.cpp wallet.cpp)
set(lwscli-views_headers accounts.h history.h keys.h manager.h settings.h wallet.h)
set(lwcli-views_sources accounts.cpp history.cpp keys.cpp manager.cpp send.cpp settings.cpp wallet.cpp)
set(lwscli-views_headers accounts.h history.h keys.h manager.h send.h settings.h wallet.h)
add_library(lwcli-views ${lwcli-views_sources} ${lwcli-views_headers})
target_link_libraries(lwcli-views PRIVATE component dom lwcli-components lwcli-decorate lwsf-api)
+143 -21
View File
@@ -54,6 +54,106 @@ namespace lwcli { namespace view
ftxui::ButtonOption ascii() { return ftxui::ButtonOption::Ascii(); }
ftxui::Canvas make_qr_code(std::vector<std::vector<std::uint8_t>> raw)
{
const std::size_t size = raw.size();
if (std::numeric_limits<std::size_t>::max() / 4 < size)
throw std::runtime_error{"qrcode too large"};
if (std::numeric_limits<int>::max() / 4 < size)
throw std::runtime_error{"qrcode too large"};
const std::size_t canvas_size = (size * 2) + ((size % 2) * 2);
ftxui::Canvas out{int(canvas_size), int(canvas_size)};
for (std::size_t y = 0; y < canvas_size; ++y)
for (std::size_t x = 0; x < canvas_size; ++x)
out.DrawBlockOff(x, y);
for (std::size_t y = 0; y < size; ++y)
{
const auto& row = raw.at(y);
for (std::size_t x = 0; x < size; ++x)
{
if (row.at(x))
{
const int real_x = x * 2;
const int real_y = y * 2;
out.DrawBlockOn(real_x, real_y);
out.DrawBlockOn(real_x + 1, real_y);
}
}
}
return out;
}
class subaccount_ final : public ftxui::ComponentBase
{
std::string subaccount_name_;
const std::shared_ptr<Monero::Wallet> wal_;
const ftxui::Element title_;
const ftxui::Element desc_;
const ftxui::Canvas qr_code_raw_;
const ftxui::Element qr_code_;
ftxui::Component buttons_;
const ftxui::Component name_;
ftxui::Component ui_;
const std::uint32_t major_;
const std::uint32_t minor_;
bool Focusable() const override final { return true; }
ftxui::Component ActiveChild() override final { return ui_; }
public:
explicit subaccount_(std::shared_ptr<Monero::Wallet>&& wal, std::uint32_t major, std::uint32_t minor)
: ftxui::ComponentBase(),
subaccount_name_(wal->getSubaddressLabel(major, minor)),
wal_(std::move(wal)),
title_(ftxui::text(wal_->address(major, minor))),
desc_(ftxui::text(_("Name: "))),
qr_code_raw_(make_qr_code(lwsf::qrcode(wal_.get(), major, minor))),
qr_code_(ftxui::canvas(&qr_code_raw_)),
buttons_(),
name_(last_input(&subaccount_name_)),
ui_(),
major_(major),
minor_(minor)
{
buttons_ = ftxui::Container::Horizontal({
ftxui::Button(_("Cancel"), [] () { throw event::close{}; }, ascii()),
ftxui::Button(_("Save"), [this] () {
wal_->setSubaddressLabel(major_, minor_, subaccount_name_);
throw event::close{};
}, ascii())
});
ui_ = ftxui::Container::Vertical({buttons_, name_});
Add(ui_);
}
bool OnEvent(ftxui::Event event) override final
{
if (event == ftxui::Event::CtrlQ)
throw event::close{};
ui_->OnEvent(std::move(event));
return true;
}
ftxui::Element OnRender() override final
{
return ftxui::window(title_, ftxui::vbox({
buttons_->Render() | ftxui::hcenter,
ftxui::separator(),
ftxui::hbox({desc_, name_->Render()}),
ftxui::separator(),
qr_code_ | ftxui::hcenter
}));
}
};
ftxui::Component subaccount(std::shared_ptr<Monero::Wallet> wal, std::uint32_t major, std::uint32_t minor)
{
return std::make_shared<subaccount_>(std::move(wal), major, minor);
}
class account_detail final : public ftxui::ComponentBase
{
std::string account_name_;
@@ -67,6 +167,7 @@ namespace lwcli { namespace view
ftxui::Component buttons_;
const ftxui::Component name_;
ftxui::Component ui_;
ftxui::Element cached_;
std::unordered_map<std::size_t, std::size_t> row_map_;
const std::uint32_t id_;
@@ -85,13 +186,15 @@ namespace lwcli { namespace view
wal_(std::move(wal)),
acct_(acct),
title_(ftxui::text(_("Account #") + std::to_string(id))),
address_({ftxui::text("Address: "), ftxui::text(wal_->address(id, 0))}),
address_({ftxui::text("Primary: "), ftxui::text(wal_->address(id, 0).substr(0, 30) + "...")}),
desc_(ftxui::text(_("Name: "))),
details_(),
table_(),
buttons_(),
name_(last_input(&account_name_)),
ui_(),
cached_(),
row_map_(),
id_(id)
{
if (!acct_)
@@ -105,25 +208,29 @@ namespace lwcli { namespace view
wal_->setSubaddressLabel(id_, 0, account_name_);
throw event::close{};
}, ascii()),
ftxui::Button(_("Add Subaddress"), [this] () {
acct_->addRow(id_, std::string{config::default_subaddress_name});
}, ascii())
ftxui::Button(_("Add Subaddress"), [this] () { acct_->addRow(id_, std::string{}); }, ascii())
});
table_ = component::table(
{_("#"), _("Label"), _("Address")},
[this] () { return subaddress_list(); },
[this] (std::size_t i) { return display_details(i); }
[this] (ftxui::Event e, std::size_t i) { return display_details(e, i); }
);
ui_ = ftxui::Container::Vertical({buttons_, name_, table_});
Add(ui_);
}
bool display_details(const std::size_t index)
bool display_details(const ftxui::Event& e, const std::size_t index)
{
if (row_map_.find(index) == row_map_.end())
throw std::logic_error{"DEBUG"};
if (e != ftxui::Event::Return)
return true;
const std::size_t minor = row_map_.at(index);
if (std::numeric_limits<std::uint32_t>::max() < minor)
throw std::runtime_error{"account::display_details invalid minor"};
details_ = subaccount(wal_, id_, std::uint32_t(minor));
Add(details_);
return true;
}
@@ -143,6 +250,7 @@ namespace lwcli { namespace view
throw;
details_->Detach();
details_.reset();
account_name_ = wal_->getSubaddressLabel(id_, 0);
}
return true;
}
@@ -177,13 +285,18 @@ namespace lwcli { namespace view
ftxui::Element OnRender() override final
{
return ftxui::window(title_, ftxui::vbox({
buttons_->Render() | ftxui::hcenter,
ftxui::separator(),
ftxui::gridbox({address_, {desc_, name_->Render()}}),
ftxui::separator(),
table_->Render() | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center
}));
if (!details_)
{
cached_ = ftxui::window(title_, ftxui::vbox({
buttons_->Render() | ftxui::hcenter,
ftxui::separator(),
ftxui::gridbox({address_, {desc_, name_->Render()}}),
ftxui::separator(),
table_->Render() | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center
}));
return cached_;
}
return ftxui::dbox(cached_, decorate::overlay(details_->Render()));
}
};
@@ -194,6 +307,7 @@ namespace lwcli { namespace view
Monero::SubaddressAccount* const wal_accounts_;
std::uint32_t* const account_;
const ftxui::Element title_;
const ftxui::Element instructions_;
ftxui::Component details_;
ftxui::Component table_;
ftxui::Element table_cached_;
@@ -216,6 +330,7 @@ namespace lwcli { namespace view
wal_accounts_(wal_->subaddressAccount()),
account_(account),
title_(ftxui::text(_("Accounts"))),
instructions_(decorate::banner(ftxui::text("[l]oad account")) | ftxui::inverted),
details_(),
table_(),
table_cached_(),
@@ -233,18 +348,24 @@ namespace lwcli { namespace view
table_ = component::table(
{_("#"), _("Balance"), _("Label"), _("Address")},
[this] () { return accounts_list(); },
[this] (std::size_t i) { return display_details(i); }
[this] (ftxui::Event e, std::size_t i) { return display_details(e, i); }
);
ui_ = ftxui::Container::Vertical({buttons_, table_});
Add(ui_);
}
bool display_details(const std::size_t index)
bool display_details(const ftxui::Event& e, const std::size_t index)
{
if (details_)
details_->Detach();
details_ = std::make_shared<account_detail>(wal_, wal_->subaddress(), row_map_.at(index));
if (e == ftxui::Event::Return)
{
if (details_)
details_->Detach();
details_ = std::make_shared<account_detail>(wal_, wal_->subaddress(), row_map_.at(index));
Add(details_);
}
else if (e == ftxui::Event::l || e == ftxui::Event::L)
*account_ = row_map_.at(index);
return true;
}
@@ -305,7 +426,8 @@ namespace lwcli { namespace view
table_cached_ = ftxui::window(title_, ftxui::vbox({
buttons_->Render() | ftxui::hcenter,
ftxui::separator(),
table_->Render() //| ftxui::vscroll_indicator | ftxui::yframe | ftxui::center
table_->Render() | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center,
instructions_
}));
return table_cached_;
}
+34 -73
View File
@@ -44,61 +44,10 @@ namespace lwcli { namespace view
{
namespace
{
// Notice for next two functions
// Copyright (c) 2014-2024, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers
constexpr const unsigned int default_decimal_point = 12;
void insert_money_decimal_point(std::string &s, unsigned int decimal_point)
{
if (decimal_point == (unsigned int)-1)
decimal_point = default_decimal_point;
if(s.size() < decimal_point+1)
{
s.insert(0, decimal_point+1 - s.size(), '0');
}
if (decimal_point > 0)
s.insert(s.size() - decimal_point, ".");
}
std::string print_money(const std::uint64_t amount)
{
std::string s = std::to_string(amount);
insert_money_decimal_point(s, default_decimal_point);
return s;
}
std::string print_amount(std::uint64_t amount, int direction)
{
const bool negate = direction == Monero::TransactionInfo::Direction_Out;
return (negate ? "-" : "") + print_money(amount);
return (negate ? "-" : "") + lwsf::displayAmount(amount);
}
std::string print_amount(const Monero::TransactionInfo& tx)
@@ -154,7 +103,7 @@ namespace lwcli { namespace view
text = _("Transfers: ");
grid.push_back({
ftxui::text(std::move(text)),
ftxui::text(print_money(transfer.amount) + address)
ftxui::text(lwsf::displayAmount(transfer.amount) + address)
});
}
@@ -212,7 +161,7 @@ namespace lwcli { namespace view
}
payment_id_ = {ftxui::text(_("Payment ID: ")), ftxui::text(info_->paymentId())};
amount_ = {ftxui::text(_("Amount: ")), ftxui::text(print_amount(*info_))};
fee_ = {ftxui::text(_("Fee: ")), ftxui::text(print_money(info_->fee()))};
fee_ = {ftxui::text(_("Fee: ")), ftxui::text(lwsf::displayAmount(info_->fee()))};
{
std::string minors;
for (const std::uint32_t minor : info_->subaddrIndex())
@@ -272,34 +221,35 @@ namespace lwcli { namespace view
row_map_(),
account_(account)
{
Monero::Subaddress* subaddress = wallet_->subaddress();
if (!subaddress)
throw std::runtime_error{"unexpected subaddress nullptr"};
subaddress->refresh(account_);
if (!wallet_)
throw std::invalid_argument{"lwcli::view::history given nullptr"};
std::string address = _("Account ") + std::to_string(account_);
const auto subaddresses = subaddress->getAll();
if (!subaddresses.empty() && *subaddresses.begin())
address += ": " + (*subaddresses.begin())->getAddress();
title_ = ftxui::text(std::move(address));
{
std::string address = _("Account #") + std::to_string(account_);
address.append(" / ").append(wallet_->getSubaddressLabel(account_, 0)).append(" / ");
address.append(wallet_->address(account_, 0).substr(0, 20)).append("...");
title_ = ftxui::text(std::move(address));
}
// perform transalation lookup once
table_ = component::table(
{_("Date"), _("Amount"), _("Payment ID"), _("Desription"), _("Block"), _("Fee"), _("Hash")},
{_("Date"), _("Amount"), _("Payment ID"), _("Label"), _("Desription"), _("Block"), _("Fee"), _("Hash")},
[this] () { return transaction_list(); },
[this] (std::size_t i) { return add_overlay(i); }
[this] (ftxui::Event e, std::size_t i) { return add_overlay(e, i); }
);
Add(table_);
}
bool add_overlay(const std::size_t i)
bool add_overlay(const ftxui::Event& e, const std::size_t i)
{
if (overlay_)
overlay_->Detach();
overlay_ = std::make_shared<tx_details>(wallet_->history(), row_map_.at(i));
Add(overlay_);
if (e == ftxui::Event::Return)
{
if (overlay_)
overlay_->Detach();
overlay_ = std::make_shared<tx_details>(wallet_->history(), row_map_.at(i));
Add(overlay_);
}
return true;
}
@@ -390,13 +340,24 @@ namespace lwcli { namespace view
char const* const extended_payment_id = 16 < payment_id.size() ?
"..." : "";
std::string label;
const std::set<std::uint32_t> subaddrs = tx->subaddrIndex();
if (!subaddrs.empty())
{
const std::uint32_t index = *subaddrs.begin();
if (index)
label = wallet_->getSubaddressLabel(account_, index);
}
rows.push_back({
std::string{date},
print_amount(amount, direction),
payment_id.substr(0, 16) + extended_payment_id,
std::move(label),
tx->description(),
std::to_string(tx->blockHeight()),
print_money(tx->fee()),
lwsf::displayAmount(tx->fee()),
tx->hash().substr(0, 16) + "..."
});
@@ -415,7 +376,7 @@ namespace lwcli { namespace view
auto table = table_->Render(); // compute balance first in callback
table_cached_ = ftxui::vbox({
title_,
ftxui::text(_("Balance: ") + print_money(balance_)),
ftxui::text(_("Balance: ") + lwsf::displayAmount(balance_)),
std::move(table) | ftxui::vscroll_indicator | ftxui::yframe | ftxui::center | ftxui::flex
});
return table_cached_;
+5 -3
View File
@@ -470,7 +470,7 @@ namespace lwcli { namespace view
}
public:
explicit start(std::shared_ptr<Monero::WalletManager>&& wm, std::string&& file, std::shared_ptr<Monero::Wallet>* out)
explicit start(std::shared_ptr<Monero::WalletManager> wm, std::string&& file, std::shared_ptr<Monero::Wallet>* out)
: out_(out),
title_(ftxui::text("wmcli")),
help_(decorate::banner(ftxui::text(_("Ctrl-Q to close active window, Ctrl-C close app immediately")))),
@@ -598,6 +598,7 @@ namespace lwcli { namespace view
class manager_ final : public ftxui::ComponentBase
{
const std::shared_ptr<Monero::WalletManager> wm_;
std::shared_ptr<Monero::Wallet> data_;
const ftxui::Component start_;
ftxui::Component wallet_;
@@ -613,8 +614,9 @@ namespace lwcli { namespace view
public:
explicit manager_(std::shared_ptr<Monero::WalletManager>&& wm, std::string&& file)
: ftxui::ComponentBase(),
wm_(std::move(wm)),
data_(nullptr),
start_(std::make_shared<start>(std::move(wm), std::move(file), &data_)),
start_(std::make_shared<start>(wm_, std::move(file), &data_)),
wallet_(nullptr)
{}
@@ -633,7 +635,7 @@ namespace lwcli { namespace view
return wallet_->OnEvent(std::move(event));
}
else if (event != event::lock_wallet && start_->OnEvent(std::move(event)) && data_)
wallet_ = view::wallet(std::move(data_));
wallet_ = view::wallet(wm_, std::move(data_));
data_.reset();
}
catch (const event::close&)
+532
View File
@@ -0,0 +1,532 @@
// Copyright (c) 2025, Cifro Codes LLC
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "wallet.h"
#include <array>
#include <ftxui/component/animation.hpp>
#include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/table.hpp>
#include <future>
#include <lws_frontend.h>
#include "components/table.h"
#include "decorate/overlay.h"
#include "events.h"
#include "lwcli_config.h"
#include "translate.h"
#include "util.h"
namespace lwcli { namespace view
{
namespace
{
using dest_pair = std::pair<std::string, std::string>;
using dest_group = std::pair<std::vector<std::string>, std::vector<std::uint64_t>>;
constexpr const std::array<char, 4> spinner{{'|', '/', '-', '\\'}};
ftxui::Component last_input(std::string* str)
{
auto opt = ftxui::InputOption::Default();
opt.cursor_position = str->size();
opt.multiline = false;
return ftxui::Input(str, std::move(opt));
}
ftxui::ButtonOption ascii() { return ftxui::ButtonOption::Ascii(); }
struct confirmed final : public std::exception
{
confirmed() noexcept
: std::exception()
{}
virtual ~confirmed() noexcept override = default;
virtual const char* what() const noexcept override final { return "confirmed send"; }
};
class confirm_ final : public ftxui::ComponentBase
{
const std::shared_ptr<Monero::PendingTransaction> tx_;
const ftxui::Element title_;
ftxui::Element info_;
ftxui::Element error_;
ftxui::Component buttons_;
std::future<bool> sending_;
unsigned animation_;
bool sent_;
bool closing_;
bool Focusable() const override final { return true; }
ftxui::Component ActiveChild() override final { return buttons_; }
public:
explicit confirm_(std::shared_ptr<Monero::PendingTransaction>&& tx, dest_group&& dests)
: ftxui::ComponentBase(),
tx_(std::move(tx)),
title_(ftxui::text(_("Sending Tx(es)"))),
info_(),
error_(),
buttons_(),
sending_(),
animation_(0),
sent_(false),
closing_(false)
{
{
std::vector<std::vector<ftxui::Element>> grid;
grid.reserve(dests.first.size() + 3);
grid.push_back({ftxui::text(_("Sending: ")), ftxui::text(lwsf::displayAmount(tx_->amount()) + " XMR")});
grid.push_back({ftxui::text(_("Fee: ")), ftxui::text(lwsf::displayAmount(tx_->fee()) + " XMR")});
{
std::vector<std::string> ids = tx_->txid();
ftxui::Elements rows;
rows.reserve(ids.size());
for (std::string& id : ids)
rows.push_back(ftxui::text(std::move(id)));
grid.push_back({ftxui::text(_("TX IDs:")), ftxui::vbox(std::move(rows))});
}
for (std::size_t i = 0; i < dests.first.size(); ++i)
grid.push_back({ftxui::text(lwsf::displayAmount(dests.second.at(i)) + " XMR to "), ftxui::text(std::move(dests.first.at(i)))});
info_ = ftxui::gridbox(std::move(grid));
}
buttons_ = ftxui::Container::Horizontal({
ftxui::Button(_("Cancel"), [] () { throw event::close{}; }, ascii()),
ftxui::Button(_("Send/Confirm"), [this] () { send_tx(); }, ascii())
});
Add(buttons_);
}
void send_tx()
{
const auto tx_commit = [] (std::shared_ptr<Monero::PendingTransaction> tx)
{
return tx->commit();
};
sending_ = std::async(std::launch::async, tx_commit, tx_);
}
bool OnEvent(ftxui::Event event) override final
{
const bool send_async = event == event::send_async;
if (!event.is_mouse() && !send_async)
error_.reset();
try
{
if (event == ftxui::Event::CtrlQ)
throw event::close{};
else if (sent_ && send_async)
throw confirmed{};
else if (closing_ && send_async)
throw event::close{};
else if (!closing_)
buttons_->OnEvent(std::move(event));
}
catch (const event::close&)
{
if (!sending_.valid())
throw;
closing_ = true;
}
return true;
}
ftxui::Element OnRender() override final
{
ftxui::Elements rows;
rows.reserve(4);
bool animate = false;
if (sending_.valid())
{
if (sending_.wait_for(std::chrono::seconds{0}) == std::future_status::ready)
{
sent_ = sending_.get();
if (!sent_)
error_ = ftxui::text(tx_->errorString());
else
error_.reset();
ftxui::ScreenInteractive* const active = ftxui::ScreenInteractive::Active();
if (active)
active->PostEvent(event::send_async);
}
else
{
animate = true;
animation_ = (animation_ + 1) % spinner.size();
error_ = ftxui::text(std::string{spinner[animation_]} + _(" Sending ") + spinner[animation_]);
ftxui::animation::RequestAnimationFrame();
}
}
if (!closing_ && !animate)
rows.push_back(buttons_->Render() | ftxui::hcenter);
if (error_)
rows.push_back(decorate::banner(error_) | ftxui::inverted);
else
rows.push_back(ftxui::separator());
if (closing_)
rows.push_back(ftxui::text(_("...Waiting for Tx Send...")));
rows.push_back(info_);
return ftxui::window(title_, ftxui::vbox(std::move(rows)));
}
};
ftxui::Component confirm(std::shared_ptr<Monero::PendingTransaction> tx, dest_group dests)
{
return std::make_shared<confirm_>(std::move(tx), std::move(dests));
}
ftxui::Component book(std::shared_ptr<Monero::WalletManager> wm, std::shared_ptr<Monero::Wallet> wal, std::shared_ptr<dest_pair> dest)
{
return nullptr; //return std::make_shared<book_>(std::move(wm), std::move(wal), std::move(dest));
}
class send_ final : public ftxui::ComponentBase
{
using buttons_tuple =
std::tuple<ftxui::Component, ftxui::Component, ftxui::Component, ftxui::Component>;
const std::shared_ptr<Monero::WalletManager> wm_;
const std::shared_ptr<Monero::Wallet> wal_;
const ftxui::Element title_;
std::vector<std::shared_ptr<dest_pair>> dests_;
std::vector<buttons_tuple> dests_ui_;
ftxui::Component overlay_;
ftxui::Component buttons_;
ftxui::Element error_;
ftxui::Component ui_;
ftxui::Element cached_;
std::future<std::optional<std::pair<std::string, std::shared_ptr<dest_pair>>>> oa_;
std::future<std::tuple<std::shared_ptr<Monero::PendingTransaction>, dest_group, std::string>> tx_;
const std::uint32_t account_;
unsigned animation_;
bool closing_;
bool Focusable() const override final { return true; }
ftxui::Component ActiveChild() override final
{
if (overlay_)
return overlay_;
return ui_;
}
public:
explicit send_(std::shared_ptr<Monero::WalletManager>&& wm, std::shared_ptr<Monero::Wallet>&& wal, const std::uint32_t account)
: ftxui::ComponentBase(),
wm_(std::move(wm)),
wal_(std::move(wal)),
title_(ftxui::text(_("Send from account #") + std::to_string(account) + " (" + lwsf::displayAmount(wal_->unlockedBalance(account)) + " XMR available)")),
dests_(),
dests_ui_(),
overlay_(),
buttons_(),
error_(),
ui_(),
cached_(),
oa_(),
tx_(),
account_(account),
animation_(0),
closing_(false)
{
buttons_ = ftxui::Container::Horizontal({
ftxui::Button(_("Cancel"), [] () { throw event::close{}; }, ascii()),
ftxui::Button(_("Add Dest"), [this] () { add_dest(); }, ascii()),
ftxui::Button(_("Construct Tx"), [this] () { try_construct(); }, ascii())
});
add_dest();
}
void add_dest()
{
dests_.push_back(std::make_shared<std::pair<std::string, std::string>>());
const std::shared_ptr<dest_pair> dest = dests_.back();
const std::size_t elem = dests_ui_.size();
dests_ui_.emplace_back(
last_input(&dests_.back()->first),
last_input(&dests_.back()->second),
ftxui::Button(_("Book"), [this, dest] () { overlay_ = book(wm_, wal_, dest); }, ascii()),
ftxui::Button(_("Remove"), [this, elem] () { remove_dest(elem); }, ascii())
);
update_ui();
}
void remove_dest(const std::size_t elem)
{
dests_ui_.erase(dests_ui_.begin() + elem);
dests_.erase(dests_.begin() + elem);
for (std::size_t i = 0; i < dests_ui_.size(); ++i)
std::get<3>(dests_ui_.at(i)) = ftxui::Button(_("Remove"), [this, i] () { remove_dest(i); }, ascii());
update_ui();
}
void update_ui()
{
ftxui::Components ui;
ui.reserve(dests_ui_.size() + 1);
ui.push_back(buttons_);
for (const auto& e : dests_ui_)
ui.push_back(ftxui::Container::Horizontal({std::get<0>(e), std::get<1>(e), std::get<2>(e), std::get<3>(e)}));
if (ui_)
ui_->Detach();
ui_ = ftxui::Container::Vertical(std::move(ui));
Add(ui_);
}
void try_construct()
{
if (oa_.valid() || tx_.valid())
return;
if (dests_.empty())
{
error_ = ftxui::text("Must have one destination");
return;
}
dest_group dests;
dests.first.reserve(dests_.size());
dests.second.reserve(dests_.size());
for (const auto& dest : dests_)
{
const std::uint64_t amount = lwsf::amountFromString(dest->first);
if (!amount)
{
error_ = ftxui::text("Invalid amount");
return;
}
dests.second.push_back(amount);
if (!lwsf::addressValid(dest->second, wal_->nettype()))
{
if (dest->second.find_first_of(u8'.') == std::string::npos)
{
error_ = ftxui::text(_("Invalid Address/OpenAlias"));
return;
}
const auto oa_lookup = [dest] (std::shared_ptr<Monero::WalletManager> wm, const std::string& uri)
-> std::optional<std::pair<std::string, std::shared_ptr<dest_pair>>>
{
bool dnssec = false;
std::string result = wm->resolveOpenAlias(uri, dnssec);
if (!dnssec)
return std::nullopt;
return {{std::move(result), dest}};
};
if (!wm_)
throw std::runtime_error{"WalletManager is nullptr"};
oa_ = std::async(std::launch::async, oa_lookup, wm_, dest->second);
return;
}
dests.first.push_back(dest->second);
}
const auto tx_construct = [] (std::shared_ptr<Monero::Wallet> wal, dest_group dests, const std::uint32_t account)
-> std::tuple<std::shared_ptr<Monero::PendingTransaction>, dest_group, std::string>
{
const auto dispose = [wal] (Monero::PendingTransaction* ptr)
{
if (ptr)
wal->disposeTransaction(ptr);
};
std::shared_ptr<Monero::PendingTransaction> tx{
wal->createTransactionMultDest(dests.first, {}, dests.second, 0 /*mixin_count*/, Monero::PendingTransaction::Priority_Default, account),
dispose
};
if (!tx)
return {nullptr, {}, "Unexpected nullptr tx"};
if (tx->status() == Monero::PendingTransaction::Status_Ok)
return {std::move(tx), std::move(dests), {}};
return {nullptr, {}, tx->errorString()};
};
tx_ = std::async(std::launch::async, tx_construct, wal_, std::move(dests), account_);
}
bool OnEvent(ftxui::Event event) override final
{
const bool is_waiting = oa_.valid() || tx_.valid();
try
{
if (!event.is_mouse() && event != event::send_async)
error_.reset();
if (overlay_)
return overlay_->OnEvent(std::move(event));
else if (closing_ && event == event::send_async)
throw event::close{};
else if (event == ftxui::Event::CtrlQ)
throw event::close{};
else if (!is_waiting)
ui_->OnEvent(std::move(event));
}
catch (const confirmed&)
{ throw event::close{}; }
catch (const event::close&)
{
if (!overlay_ && !is_waiting)
throw;
if (overlay_)
overlay_->Detach();
else
closing_ = true;
overlay_.reset();
}
return true;
}
ftxui::Element OnRender() override final
{
bool animate = false;
if (oa_.valid() || tx_.valid())
{
animate = true;
if (oa_.valid() && oa_.wait_for(std::chrono::seconds{0}) == std::future_status::ready)
{
auto oa = oa_.get();
error_.reset();
if (!oa)
error_ = ftxui::text(_("dnssec failure for domain"));
else if (oa->first.empty())
error_ = ftxui::text(_("No XMR OpenAlias found"));
else if (!lwsf::addressValid(oa->first, wal_->nettype()))
error_ = ftxui::text(_("OpenAlias record is invalid"));
else if (!closing_)
{
oa->second->second = std::move(oa->first);
try_construct();
}
animate = oa_.valid();
}
else if (tx_.valid() && tx_.wait_for(std::chrono::seconds{0}) == std::future_status::ready)
{
animate = false;
auto tx = tx_.get();
if (std::get<0>(tx))
overlay_ = confirm(std::move(std::get<0>(tx)), std::move(std::get<1>(tx)));
else
error_ = ftxui::text(std::move(std::get<2>(tx)));
}
if (animate)
{
char const* const label = oa_.valid() ?
_(" OpenAlias Lookup ") : _(" Constructing Transaction ");
animation_ = (animation_ + 1) % spinner.size();
error_ = ftxui::text(std::string{spinner[animation_]} + label + spinner[animation_]);
ftxui::animation::RequestAnimationFrame();
}
else
{
ftxui::ScreenInteractive* const active = ftxui::ScreenInteractive::Active();
if (active)
active->PostEvent(event::send_async);
}
}
if (!overlay_)
{
ftxui::Elements rows;
rows.reserve(3); // dests_ui_.size() + 2);
if (!animate)
rows.push_back(buttons_->Render() | ftxui::hcenter);
if (error_)
rows.push_back(decorate::banner(error_) | ftxui::inverted);
else
rows.push_back(ftxui::separator());
if (!closing_)
{
std::vector<std::vector<ftxui::Element>> grid;
grid.reserve(dests_ui_.size());
for (const auto& e : dests_ui_)
{
ftxui::Elements row;
row.reserve(6);
row.push_back(std::get<0>(e)->Render());
row.push_back(ftxui::text(" XMR to "));
row.push_back(std::get<1>(e)->Render());
if (!animate)
{
row.push_back(ftxui::separator());
row.push_back(std::get<2>(e)->Render());
row.push_back(std::get<3>(e)->Render());
}
grid.push_back(std::move(row));
}
rows.push_back(ftxui::gridbox(std::move(grid)));
}
else
rows.push_back(ftxui::text(_("...Cleaning Up...")) | ftxui::hcenter);
cached_ = ftxui::window(title_, ftxui::vbox(std::move(rows)));
return cached_;
}
return ftxui::dbox(cached_, decorate::overlay(overlay_->Render()));
}
};
}
ftxui::Component send(std::shared_ptr<Monero::WalletManager> wm, std::shared_ptr<Monero::Wallet> wal, const std::uint32_t account)
{
if (!wal)
throw std::invalid_argument{"views::send cannot be given nullptr"};
return std::make_shared<send_>(std::move(wm), std::move(wal), account);
}
}} // lwcli // view
+44
View File
@@ -0,0 +1,44 @@
// Copyright (c) 2025, Cifro Codes LLC
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <cstdint>
#include <ftxui/component/component_base.hpp>
// 9sMNg6xAhC15Y2r51xthUaHCuKDiFaojSBaSPUrAeBosZBYahFEii7dDq6y3pgaNUzBhvKmxPpWdnPquzsVLkDWB9JM3tiS
namespace Monero
{
class Wallet;
class WalletManager;
}
namespace lwcli { namespace view
{
//! Shows Transaction History
ftxui::Component send(std::shared_ptr<Monero::WalletManager> wm, std::shared_ptr<Monero::Wallet> wallet, std::uint32_t account);
}} // lwscli // view
+2 -3
View File
@@ -47,13 +47,12 @@ namespace lwcli { namespace view
{
bool set_proxy(Monero::Wallet& wal, const std::string& proxy)
{
wal.setProxy(proxy);
return true;
return wal.setProxy(proxy);
}
bool set_url(Monero::Wallet& wal, const std::string& url)
{
const bool is_ssl = bool(from_string(wal.getCacheAttribute(std::string{config::server::ssl})));
const bool is_ssl = bool(from_string(wal.getCacheAttribute(std::string{config::server::ssl})).value_or(0));
wal.init(url, 0, "", "", is_ssl, true, wal.getCacheAttribute(std::string{config::server::proxy}));
return true;
}
+9 -10
View File
@@ -41,6 +41,7 @@
#include "translate.h"
#include "views/accounts.h"
#include "views/history.h"
#include "views/send.h"
#include "views/settings.h"
namespace lwcli { namespace view
@@ -51,6 +52,7 @@ namespace lwcli { namespace view
struct wallet_state
{
const std::shared_ptr<Monero::WalletManager> wm;
const std::shared_ptr<Monero::Wallet> wal;
ftxui::Component overlay;
std::uint32_t selected_account = 0;
@@ -60,8 +62,7 @@ namespace lwcli { namespace view
{
const std::shared_ptr<Monero::Wallet> wal = state->wal;
return ftxui::Container::Horizontal({
ftxui::Button("[s]end", [] () {}, ascii()),
ftxui::Button("[b]ook", [] () {}, ascii()),
ftxui::Button("[s]end", [state] () { state->overlay = send(state->wm, state->wal, state->selected_account); }, ascii()),
ftxui::Button("[a]ccounts", [state] () { state->overlay = accounts(state->wal, &state->selected_account); }, ascii()),
ftxui::Button("[r]efresh", [wal] () { wal->refreshAsync(); }, ascii()),
ftxui::Button("s[e]ttings", [state] () { state->overlay = settings(state->wal); }, ascii())
@@ -108,9 +109,9 @@ namespace lwcli { namespace view
}
public:
explicit wallet_(std::shared_ptr<Monero::Wallet>&& data)
explicit wallet_(std::shared_ptr<Monero::WalletManager>&& wm, std::shared_ptr<Monero::Wallet>&& data)
: ftxui::ComponentBase(),
state_{std::move(data)},
state_{std::move(wm), std::move(data)},
title_(nullptr),
active_account_(-1),
bar_(),
@@ -121,7 +122,7 @@ namespace lwcli { namespace view
throw std::runtime_error{"Unexpected nullptr Monero wallet"};
state_.wal->setListener(this);
bar_ = menu_bar(&state_);
title_ = ftxui::text(_("lwcli Wallet (Primary ") + state_.wal->mainAddress() + ")");
title_ = ftxui::text(_("lwcli Wallet (Primary ") + state_.wal->mainAddress().substr(0, 40) + "...)");
update_account();
}
@@ -158,9 +159,7 @@ namespace lwcli { namespace view
else if (!ui_->OnEvent(event))
{
if (event == ftxui::Event::s || event == ftxui::Event::S)
; // state_.overlay = send(state_.wal);
else if (event == ftxui::Event::b || event == ftxui::Event::B)
; // state_.overlay = book(state_.wal);
state_.overlay = send(state_.wm, state_.wal, state_.selected_account);
else if (event == ftxui::Event::a || event == ftxui::Event::a)
state_.overlay = accounts(state_.wal, &state_.selected_account);
else if (event == ftxui::Event::r || event == ftxui::Event::R)
@@ -214,8 +213,8 @@ namespace lwcli { namespace view
};
} // anonymous
ftxui::Component wallet(std::shared_ptr<Monero::Wallet> data)
ftxui::Component wallet(std::shared_ptr<Monero::WalletManager> wm, std::shared_ptr<Monero::Wallet> data)
{
return std::make_shared<wallet_>(std::move(data));
return std::make_shared<wallet_>(std::move(wm), std::move(data));
}
}} // lwcli // view
+7 -2
View File
@@ -30,9 +30,14 @@
#include <cstdint>
#include <ftxui/component/component_base.hpp>
namespace Monero { class Wallet; }
namespace Monero
{
class Wallet;
class WalletManager;
}
namespace lwcli { namespace view
{
ftxui::Component wallet(std::shared_ptr<Monero::Wallet> wallet);
ftxui::Component wallet(std::shared_ptr<Monero::WalletManager> wm, std::shared_ptr<Monero::Wallet> wallet);
}} // lwscli // view