// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #include #include #include #include "../../app.h" #include "../../util/i18n.h" #include "../material/draw_helpers.h" #include "../theme.h" #include "imgui.h" namespace dragonx { namespace ui { /** * @brief Modal dialog for transferring funds between two addresses. * * Shown when a user drags one address row onto another in the address list. * Displays from/to addresses, balances, amount input, fee, and a preview of * the resulting balances after transfer. */ struct AddressTransferInfo { std::string fromAddr; std::string toAddr; double fromBalance = 0.0; double toBalance = 0.0; bool fromIsZ = false; bool toIsZ = false; }; class AddressTransferDialog { public: using TransferInfo = AddressTransferInfo; static void show(App* app, const TransferInfo& info) { s_open = true; s_app = app; s_info = info; s_sending = false; s_resultMsg.clear(); s_success = false; // Pre-fill amount with full source balance snprintf(s_amount, sizeof(s_amount), "%.8f", info.fromBalance); // Default fee s_fee = 0.0001; } static void render() { if (!s_open || !s_app) return; using namespace material; if (BeginOverlayDialog(TR("transfer_funds"), &s_open, 480.0f, 0.94f)) { float dp = Layout::dpiScale(); ImDrawList* dl = ImGui::GetWindowDrawList(); // If we got a result, show it if (!s_resultMsg.empty()) { renderResult(); EndOverlayDialog(); return; } // Shielding / deshielding warning if (s_info.fromIsZ && !s_info.toIsZ) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); ImGui::TextWrapped("%s", TR("deshielding_warning")); ImGui::PopStyleColor(); ImGui::Spacing(); } else if (!s_info.fromIsZ && s_info.toIsZ) { ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Success())); ImGui::TextWrapped("%s", TR("shielding_notice")); ImGui::PopStyleColor(); ImGui::Spacing(); } // From address Type().text(TypeStyle::Overline, TR("from")); renderAddressRow(s_info.fromAddr, s_info.fromBalance, s_info.fromIsZ, dp); ImGui::Spacing(); // Arrow { float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f; ImGui::SetCursorPosX(arrowCX - 8.0f); ImFont* iconFont = Type().iconMed(); float fsz = ScaledFontSize(iconFont); ImVec2 pos = ImGui::GetCursorScreenPos(); dl->AddText(iconFont, fsz, pos, OnSurfaceMedium(), ICON_MD_ARROW_DOWNWARD); ImGui::Dummy(ImVec2(fsz, fsz)); } ImGui::Spacing(); // To address Type().text(TypeStyle::Overline, TR("to")); renderAddressRow(s_info.toAddr, s_info.toBalance, s_info.toIsZ, dp); ImGui::Spacing(); ImGui::Spacing(); // Amount input Type().text(TypeStyle::Subtitle2, TR("amount")); ImGui::SetNextItemWidth(-1); ImGui::InputText("##TransferAmt", s_amount, sizeof(s_amount), ImGuiInputTextFlags_CharsDecimal); // Max button ImGui::SameLine(); if (ImGui::SmallButton(TR("max"))) { double maxAmt = s_info.fromBalance - s_fee; if (maxAmt < 0) maxAmt = 0; snprintf(s_amount, sizeof(s_amount), "%.8f", maxAmt); } ImGui::Spacing(); // Fee display { char feeBuf[64]; snprintf(feeBuf, sizeof(feeBuf), "%s: %.4f DRGX", TR("fee"), s_fee); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), feeBuf); } ImGui::Spacing(); // Preview after transfer double amount = atof(s_amount); double totalDeduct = amount + s_fee; double newFromBal = s_info.fromBalance - totalDeduct; double newToBal = s_info.toBalance + amount; bool amountValid = amount > 0 && totalDeduct <= s_info.fromBalance; ImGui::Separator(); ImGui::Spacing(); Type().text(TypeStyle::Subtitle2, TR("result_preview")); ImGui::Spacing(); { char buf[128]; snprintf(buf, sizeof(buf), "%s: %.8f DRGX → %.8f DRGX", TR("sender_balance"), s_info.fromBalance, amountValid ? newFromBal : s_info.fromBalance); Type().textColored(TypeStyle::Caption, (amountValid && newFromBal < 1e-9) ? Warning() : OnSurfaceMedium(), buf); } { char buf[128]; snprintf(buf, sizeof(buf), "%s: %.8f DRGX → %.8f DRGX", TR("recipient_balance"), s_info.toBalance, amountValid ? newToBal : s_info.toBalance); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf); } // Warnings if (!amountValid && amount > 0) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Error())); ImGui::TextWrapped("%s", TR("insufficient_funds")); ImGui::PopStyleColor(); } if (amountValid && newFromBal < 1e-9) { ImGui::Spacing(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); ImGui::TextWrapped("%s", TR("sends_full_balance_warning")); ImGui::PopStyleColor(); } ImGui::Spacing(); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Buttons float btnW = 140.0f; float totalW = btnW * 2 + Layout::spacingMd(); ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f); if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) { s_open = false; } ImGui::SameLine(0, Layout::spacingMd()); ImGui::BeginDisabled(!amountValid || s_sending); if (TactileButton(s_sending ? TR("sending") : TR("confirm_transfer"), ImVec2(btnW, 0))) { s_sending = true; s_app->sendTransaction(s_info.fromAddr, s_info.toAddr, amount, s_fee, "", [](bool ok, const std::string& result) { s_sending = false; s_success = ok; if (ok) s_resultMsg = result; // opid else s_resultMsg = result; // error message }); } ImGui::EndDisabled(); EndOverlayDialog(); } } static bool isOpen() { return s_open; } static void close() { s_open = false; } private: static void renderAddressRow(const std::string& addr, double balance, bool isZ, float dp) { using namespace material; ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 pos = ImGui::GetCursorScreenPos(); float w = ImGui::GetContentRegionAvail().x; float h = 40.0f * dp; ImVec2 mn = pos, mx(pos.x + w, pos.y + h); GlassPanelSpec g; g.rounding = 8.0f * dp; g.fillAlpha = 18; DrawGlassPanel(dl, mn, mx, g); float pad = 12.0f * dp; // Type icon ImFont* iconFont = Type().iconSmall(); const char* icon = isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE; ImU32 col = isZ ? Success() : Warning(); float iconFsz = ScaledFontSize(iconFont); ImVec2 iSz = iconFont->CalcTextSizeA(iconFsz, 1000.0f, 0.0f, icon); dl->AddText(iconFont, iconFsz, ImVec2(mn.x + pad, mn.y + (h - iSz.y) * 0.5f), col, icon); // Address (truncated) float textX = mn.x + pad + iSz.x + 8.0f * dp; std::string display = addr; if (display.size() > 42) display = display.substr(0, 18) + "..." + display.substr(display.size() - 14); ImFont* capFont = Type().caption(); float capFsz = ScaledFontSize(capFont); dl->AddText(capFont, capFsz, ImVec2(textX, mn.y + (h * 0.5f - capFsz)), OnSurfaceMedium(), display.c_str()); // Balance (right-aligned) char balBuf[32]; snprintf(balBuf, sizeof(balBuf), "%.8f DRGX", balance); ImFont* body = Type().body2(); float bodyFsz = ScaledFontSize(body); ImVec2 balSz = body->CalcTextSizeA(bodyFsz, 1000.0f, 0.0f, balBuf); dl->AddText(body, bodyFsz, ImVec2(mx.x - pad - balSz.x, mn.y + (h - balSz.y) * 0.5f), balance > 0 ? OnSurface() : OnSurfaceDisabled(), balBuf); ImGui::Dummy(ImVec2(w, h)); } static void renderResult() { using namespace material; if (s_success) { ImGui::Spacing(); Type().text(TypeStyle::H6, TR("transfer_sent")); ImGui::Spacing(); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("transfer_sent_desc")); ImGui::Spacing(); { char buf[128]; snprintf(buf, sizeof(buf), "Operation ID: %s", s_resultMsg.size() > 40 ? (s_resultMsg.substr(0, 20) + "..." + s_resultMsg.substr(s_resultMsg.size() - 10)).c_str() : s_resultMsg.c_str()); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf); } } else { ImGui::Spacing(); Type().textColored(TypeStyle::H6, Error(), TR("transfer_failed")); ImGui::Spacing(); ImGui::TextWrapped("%s", s_resultMsg.c_str()); } ImGui::Spacing(); ImGui::Spacing(); float btnW = 120.0f; ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f); if (TactileButton(TR("close"), ImVec2(btnW, 0))) { s_open = false; } } static inline bool s_open = false; static inline App* s_app = nullptr; static inline TransferInfo s_info; static inline char s_amount[64] = {}; static inline double s_fee = 0.0001; static inline bool s_sending = false; static inline bool s_success = false; static inline std::string s_resultMsg; }; } // namespace ui } // namespace dragonx