feat(ui): explain mining addresses everywhere and stop offering them for payment

Follow-up to 5246898. A transparent address only ever receives mining
rewards on DragonX (private chain), so:

- One explainer ("Receives solo-mining rewards only - the network rejects
  payments to it. Rewards are shielded automatically once they mature.")
  on the Overview row hover, the Receive picker hover, the Receive card
  and the Validate Address result (now typed "Mining address
  (transparent)").
- Receive: a selected mining address shows the explainer and a "Use a
  shielded address" button (creates one if none exists) instead of the
  payment request, and a "Not for payments" panel instead of a QR code.
  The default selection prefers a shielded address; the "All" filter
  hides empty mining addresses unless solo mining or marked as a mining
  address; the filter reads All / Shielded / Mining.
- Request Payment: no transparent section, never prefilled with one.
- Overview: no "Mining - Mining" tag on a marked transparent row.
- FAQ: address answers rewritten; the send and memo answers no longer
  imply you can pay a transparent address. All 8 languages.

i18n: 4 new keys, 6 FAQ answers updated in place, 2 dead keys removed,
CJK subset rebuilt. UI sweep: receive-mining-address and
modal-validate-mining targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 20:32:34 -05:00
co-authored by Claude Opus 5.5
parent 5246898968
commit 31d8742ce7
19 changed files with 478 additions and 304 deletions
+4 -2
View File
@@ -546,7 +546,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
{
const char* typeLabel = row.isZ ? TR("shielded") : TR("addr_type_mining");
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
const char* miningTag = row.mining ? TR("mining_tag") : "";
// A transparent row is already typed "Mining"; the tag is for shielded payout addresses.
const char* miningTag = (row.mining && row.isZ) ? TR("mining_tag") : "";
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
@@ -655,7 +656,8 @@ void RenderSharedAddressList(App* app, float listH, float availW,
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
if (ImGui::IsItemHovered() && !s_dragActive) {
material::Tooltip("%s", addr.address.c_str());
if (row.isZ) material::Tooltip("%s", addr.address.c_str());
else material::Tooltip("%s\n\n%s", addr.address.c_str(), TR("mining_addr_explainer"));
}
// Context menu
+299 -207
View File
@@ -130,13 +130,17 @@ static void RenderAddressDropdown(App* app, float width) {
// Header row: label + address type toggle buttons
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("address_upper"));
// All / Shielded / Mining: each segment as wide as the widest label (they are words, not Z/T).
const char* filterLabels[] = { TR("all_filter"), TR("shielded"), TR("addr_type_mining") };
float toggleBtnW = std::max(schema::UI().drawElement("tabs.receive", "toggle-btn-min-width").size, schema::UI().drawElement("tabs.receive", "toggle-btn-width").size * Layout::hScale(width));
for (const char* lbl : filterLabels)
toggleBtnW = std::max(toggleBtnW, Type().caption()->CalcTextSizeA(Type().caption()->LegacySize, FLT_MAX, 0, lbl).x +
2.0f * Layout::spacingMd());
float toggleGap = schema::UI().drawElement("tabs.receive", "toggle-gap").size;
float toggleTotalW = toggleBtnW * 3 + toggleGap * 2;
float segH = ImGui::GetFrameHeight();
ImGui::SameLine(width - toggleTotalW);
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
const char* filterLabels[] = { TR("all_filter"), "Z", "T" };
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, toggleTotalW, segH,
filterLabels, 3, s_addr_type_filter, Type().caption(),
"##addrFilter", Layout::dpiScale());
@@ -147,14 +151,18 @@ static void RenderAddressDropdown(App* app, float width) {
TrackNewAddresses(state);
// Auto-select address with the largest balance on first load
// Auto-select the shielded address with the largest balance on first load. A mining (transparent)
// address can't be paid, so it is picked only when the wallet has no shielded address yet.
if (!s_auto_selected && app->isConnected() && !state.addresses.empty()) {
int bestIdx = -1;
double bestBal = -1.0;
bool bestIsZ = false;
for (size_t i = 0; i < state.addresses.size(); i++) {
if (state.addresses[i].balance > bestBal) {
const bool z = state.addresses[i].type == "shielded";
if ((z && !bestIsZ) || (z == bestIsZ && state.addresses[i].balance > bestBal)) {
bestBal = state.addresses[i].balance;
bestIdx = static_cast<int>(i);
bestIsZ = z;
}
}
if (bestIdx >= 0) {
@@ -222,6 +230,12 @@ static void RenderAddressDropdown(App* app, float width) {
bool isZ = state.addresses[i].type == "shielded";
if (s_addr_type_filter == 1 && !isZ) continue;
if (s_addr_type_filter == 2 && isZ) continue;
// "All" lists a mining address only when it matters: it holds rewards, it is marked as a
// mining address, we are solo mining, or it is the one selected. The Mining filter lists all.
if (s_addr_type_filter == 0 && !isZ && state.addresses[i].balance < 1e-9 &&
!app->isMiningAddress(state.addresses[i].address) && !state.mining.generate &&
s_selected_address_idx != static_cast<int>(i))
continue;
sortedIdx.push_back(i);
}
std::sort(sortedIdx.begin(), sortedIdx.end(),
@@ -272,9 +286,12 @@ static void RenderAddressDropdown(App* app, float width) {
s_cached_qr_data.clear(); // Force QR regeneration
}
if (ImGui::IsItemHovered()) {
material::Tooltip(TR("grpb_tooltip_address_balance"),
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? TR("grpb_selected_suffix") : "");
char tip[512];
snprintf(tip, sizeof(tip), TR("grpb_tooltip_address_balance"),
addr.address.c_str(), addr.balance, DRAGONX_TICKER,
isCurrent ? TR("grpb_selected_suffix") : "");
if (isZ) material::Tooltip("%s", tip);
else material::Tooltip("%s\n\n%s", tip, TR("mining_addr_explainer"));
}
ImGui::PopID();
}
@@ -337,6 +354,60 @@ static void RenderAddressDropdown(App* app, float width) {
ImGui::EndDisabled();
}
void SweepSelectReceiveAddress(App* app, const char* address) {
(void)app;
if (address) {
s_pending_select_address = address;
s_addr_type_filter = 0;
} else {
s_auto_selected = false; // re-run the default (shielded-first) choice
}
s_cached_qr_data.clear();
}
// ============================================================================
// Mining address notice (instead of a payment request)
// ============================================================================
// A mining (transparent) address only ever receives mining rewards: the network rejects payments to it.
// So it gets no payment request and no QR code — just what it is for, and a way to a shielded address.
static void RenderMiningAddressNotice(App* app, const WalletState& state, float width) {
Type().textColored(TypeStyle::Overline, Warning(), TR("mining_address"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + width);
Type().textWrapped(TypeStyle::Body2, TR("mining_addr_explainer"));
ImGui::PopTextWrapPos();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
int bestZ = -1;
double bestBal = -1.0;
for (size_t i = 0; i < state.addresses.size(); i++) {
if (state.addresses[i].type == "shielded" && state.addresses[i].balance > bestBal) {
bestBal = state.addresses[i].balance;
bestZ = static_cast<int>(i);
}
}
ImGui::BeginDisabled(bestZ < 0 && (!app->isConnected() || s_generating_address));
if (TactileButton(TrId("receive_use_shielded", "mining").c_str(), ImVec2(0, 0), schema::UI().resolveFont("button"))) {
if (bestZ >= 0) {
s_selected_address_idx = bestZ;
s_cached_qr_data.clear();
} else {
s_generating_address = true; // no shielded address yet: make one, as "New" does
app->createNewZAddress([](const std::string& addr) {
s_generating_address = false;
if (addr.empty())
Notifications::instance().error(TR("failed_create_shielded"));
else {
s_pending_select_address = addr;
Notifications::instance().success(TR("new_shielded_created"));
}
});
}
}
ImGui::EndDisabled();
ImGui::Dummy(ImVec2(width, Layout::spacingSm()));
}
// ============================================================================
// Helpers: DrawRecvIcon (local copy — original is static in send_tab).
// Relative time uses the shared util::formatTimeAgoShort ("14d ago"), matching Overview/Send.
@@ -580,9 +651,9 @@ void RenderReceiveTab(App* app)
const AddressInfo& selected = state.addresses[s_selected_address_idx];
bool isZ = selected.type == "shielded";
// Generate QR data
std::string qr_data = selected.address;
if (s_request_amount > 0) {
// Generate QR data — shielded only: a mining (transparent) address can't be paid, so it gets no QR.
std::string qr_data = isZ ? selected.address : std::string();
if (isZ && s_request_amount > 0) {
// Canonical, URL-encoded "drgx:" URI via the shared builder. The old inline code emitted an
// unparseable "dragonx:" scheme and appended the memo raw (spaces/&/= corrupted the QR).
qr_data = util::buildPaymentUri(selected.address, s_request_amount, "",
@@ -593,8 +664,10 @@ void RenderReceiveTab(App* app)
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
int w, h;
s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h);
if (!qr_data.empty()) {
int w, h;
s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h);
}
s_cached_qr_data = qr_data;
}
@@ -644,205 +717,210 @@ void RenderReceiveTab(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// ---- PAYMENT REQUEST ----
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("payment_request"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Amount input with currency toggle
float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size * Layout::dpiScale();
float amtInputW = addrColW - toggleW - Layout::spacingMd();
if (amtInputW < S.drawElement("tabs.receive", "amount-input-min-width").size) amtInputW = S.drawElement("tabs.receive", "amount-input-min-width").size;
double usd_price = state.market.price_usd;
ImGui::PushFont(body2);
if (s_request_usd_mode && usd_price > 0) {
ImGui::PushItemWidth(amtInputW);
if (ImGui::InputDouble("##RequestAmountUSD", &s_request_usd_amount, 0, 0, "$%.2f")) {
if (s_request_usd_amount < 0) s_request_usd_amount = 0;
s_request_amount = s_request_usd_amount / usd_price;
}
{
ImVec2 iMin = ImGui::GetItemRectMin();
ImVec2 iMax = ImGui::GetItemRectMax();
snprintf(buf, sizeof(buf), "\xe2\x89\x88 %.4f %s", s_request_amount, DRAGONX_TICKER);
ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x;
float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f;
dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty),
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf);
}
ImGui::PopItemWidth();
} else {
ImGui::PushItemWidth(amtInputW);
if (ImGui::InputDouble("##RequestAmount", &s_request_amount, 0, 0, "%.8f")) {
if (s_request_amount < 0) s_request_amount = 0;
if (usd_price > 0)
s_request_usd_amount = s_request_amount * usd_price;
}
if (usd_price > 0 && s_request_amount > 0) {
ImVec2 iMin = ImGui::GetItemRectMin();
ImVec2 iMax = ImGui::GetItemRectMax();
double usd_value = s_request_amount * usd_price;
if (usd_value >= 0.01)
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd_value);
else
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.6f", usd_value);
ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x;
float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f;
dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty),
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf);
}
ImGui::PopItemWidth();
}
// Toggle button
ImGui::SameLine(0, Layout::spacingMd());
{
const char* currLabel = s_request_usd_mode ? "DRGX" : "USD";
bool canToggle = (usd_price > 0);
ImGui::BeginDisabled(!canToggle);
if (TactileButton("##ToggleCurrencyRecv", ImVec2(toggleW, 0), S.resolveFont("button"))) {
s_request_usd_mode = !s_request_usd_mode;
if (s_request_usd_mode && usd_price > 0)
s_request_usd_amount = s_request_amount * usd_price;
}
{
ImVec2 bMin = ImGui::GetItemRectMin();
ImVec2 bMax = ImGui::GetItemRectMax();
float bH = bMax.y - bMin.y;
ImFont* font = ImGui::GetFont();
ImVec2 textSz = font->CalcTextSizeA(font->LegacySize, 10000, 0, currLabel);
float iconW = schema::UI().drawElement("tabs.receive", "currency-icon-width").size * Layout::dpiScale();
float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size * Layout::dpiScale();
float totalW2 = iconW + iconGap2 + textSz.x;
float startX = bMin.x + ((bMax.x - bMin.x) - totalW2) * 0.5f;
float cy = bMin.y + bH * 0.5f;
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
float ss = iconW * 0.5f;
float cx = startX + ss;
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
float ay1 = cy - ag;
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled(
ImVec2(cx + al, ay1),
ImVec2(cx + al - hss, ay1 - hss),
ImVec2(cx + al - hss, ay1 + hss), iconCol);
float ay2 = cy + ag;
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled(
ImVec2(cx - al, ay2),
ImVec2(cx - al + hss, ay2 - hss),
ImVec2(cx - al + hss, ay2 + hss), iconCol);
float tx = startX + iconW + iconGap2;
float ty = cy - textSz.y * 0.5f;
dl->AddText(font, font->LegacySize, ImVec2(tx, ty), iconCol, currLabel);
}
ImGui::EndDisabled();
}
ImGui::PopFont();
// Preset amount chips
{
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
struct Preset { const char* label; double amount; };
Preset presets[] = {
{ "0.1", 0.1 },
{ "1", 1.0 },
{ "10", 10.0 },
{ "100", 100.0 },
{ "1K", 1000.0 },
{ "10K", 10000.0 },
{ "50K", 50000.0 },
{ "100K", 100000.0 },
};
constexpr int presetCount = 8;
// Calculate equal chip widths to span full addrColW
float totalGap = chipGap * (presetCount - 1);
float chipW = (addrColW - totalGap) / presetCount;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, chipRound);
ImFont* chipFont = S.resolveFont("button");
for (int ci = 0; ci < presetCount; ci++) {
if (ci > 0) ImGui::SameLine(0, chipGap);
bool active = false;
if (s_request_usd_mode) {
active = (usd_price > 0 && std::abs(s_request_usd_amount - presets[ci].amount) < 0.001);
} else {
active = (std::abs(s_request_amount - presets[ci].amount) < 1e-8);
}
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "chip-active-bg-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
}
char chipId[32];
snprintf(chipId, sizeof(chipId), "%s##presetAmt%d", presets[ci].label, ci);
if (TactileButton(chipId, ImVec2(chipW, chipH), chipFont)) {
if (s_request_usd_mode) {
s_request_usd_amount = presets[ci].amount;
if (usd_price > 0)
s_request_amount = s_request_usd_amount / usd_price;
} else {
s_request_amount = presets[ci].amount;
if (usd_price > 0)
s_request_usd_amount = s_request_amount * usd_price;
}
}
if (active) ImGui::PopStyleColor(2);
}
ImGui::PopStyleVar();
ImGui::PopStyleColor();
}
// Memo (z-addresses only)
if (isZ) {
// ---- PAYMENT REQUEST ----
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("payment_request"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.receive", "memo-label-gap").size));
float memoInputH = std::max(schema::UI().drawElement("tabs.receive", "memo-input-min-height").size, schema::UI().drawElement("tabs.receive", "memo-input-height").size * vScale);
ImGui::PushItemWidth(addrColW);
ImGui::InputTextMultiline("##RequestMemo", s_request_memo, sizeof(s_request_memo),
ImVec2(addrColW, memoInputH));
ImGui::PopItemWidth();
// Amount input with currency toggle
float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size * Layout::dpiScale();
float amtInputW = addrColW - toggleW - Layout::spacingMd();
if (amtInputW < S.drawElement("tabs.receive", "amount-input-min-width").size) amtInputW = S.drawElement("tabs.receive", "amount-input-min-width").size;
double usd_price = state.market.price_usd;
// The Sapling memo limit is in BYTES and matches the enforced buffer size
// (InputTextMultiline caps at sizeof(s_request_memo)); count against that, not the
// old display-only 256 value, and warn in colour once the field is at its cap.
size_t memo_len = strlen(s_request_memo);
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
bool memoAtCap = memo_len + 1 >= memoMax;
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
}
ImGui::PushFont(body2);
if (s_request_usd_mode && usd_price > 0) {
ImGui::PushItemWidth(amtInputW);
if (ImGui::InputDouble("##RequestAmountUSD", &s_request_usd_amount, 0, 0, "$%.2f")) {
if (s_request_usd_amount < 0) s_request_usd_amount = 0;
s_request_amount = s_request_usd_amount / usd_price;
}
{
ImVec2 iMin = ImGui::GetItemRectMin();
ImVec2 iMax = ImGui::GetItemRectMax();
snprintf(buf, sizeof(buf), "\xe2\x89\x88 %.4f %s", s_request_amount, DRAGONX_TICKER);
ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x;
float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f;
dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty),
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf);
}
ImGui::PopItemWidth();
} else {
ImGui::PushItemWidth(amtInputW);
if (ImGui::InputDouble("##RequestAmount", &s_request_amount, 0, 0, "%.8f")) {
if (s_request_amount < 0) s_request_amount = 0;
if (usd_price > 0)
s_request_usd_amount = s_request_amount * usd_price;
}
if (usd_price > 0 && s_request_amount > 0) {
ImVec2 iMin = ImGui::GetItemRectMin();
ImVec2 iMax = ImGui::GetItemRectMax();
double usd_value = s_request_amount * usd_price;
if (usd_value >= 0.01)
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd_value);
else
snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.6f", usd_value);
ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x;
float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f;
dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty),
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf);
}
ImGui::PopItemWidth();
}
// URI preview
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
float uriWrapW = addrColW;
dl->AddText(capFont, capFont->LegacySize, ImGui::GetCursorScreenPos(),
OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW);
ImVec2 uriSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX,
uriWrapW, s_cached_qr_data.c_str());
ImGui::Dummy(ImVec2(addrColW, uriSz.y + Layout::spacingSm()));
// Toggle button
ImGui::SameLine(0, Layout::spacingMd());
{
const char* currLabel = s_request_usd_mode ? "DRGX" : "USD";
bool canToggle = (usd_price > 0);
ImGui::BeginDisabled(!canToggle);
if (TactileButton("##ToggleCurrencyRecv", ImVec2(toggleW, 0), S.resolveFont("button"))) {
s_request_usd_mode = !s_request_usd_mode;
if (s_request_usd_mode && usd_price > 0)
s_request_usd_amount = s_request_amount * usd_price;
}
{
ImVec2 bMin = ImGui::GetItemRectMin();
ImVec2 bMax = ImGui::GetItemRectMax();
float bH = bMax.y - bMin.y;
ImFont* font = ImGui::GetFont();
ImVec2 textSz = font->CalcTextSizeA(font->LegacySize, 10000, 0, currLabel);
float iconW = schema::UI().drawElement("tabs.receive", "currency-icon-width").size * Layout::dpiScale();
float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size * Layout::dpiScale();
float totalW2 = iconW + iconGap2 + textSz.x;
float startX = bMin.x + ((bMax.x - bMin.x) - totalW2) * 0.5f;
float cy = bMin.y + bH * 0.5f;
ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text);
float ss = iconW * 0.5f;
float cx = startX + ss;
float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size * cardDp;
float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size;
float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size * cardDp;
float ay1 = cy - ag;
dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled(
ImVec2(cx + al, ay1),
ImVec2(cx + al - hss, ay1 - hss),
ImVec2(cx + al - hss, ay1 + hss), iconCol);
float ay2 = cy + ag;
dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size * cardDp);
dl->AddTriangleFilled(
ImVec2(cx - al, ay2),
ImVec2(cx - al + hss, ay2 - hss),
ImVec2(cx - al + hss, ay2 + hss), iconCol);
float tx = startX + iconW + iconGap2;
float ty = cy - textSz.y * 0.5f;
dl->AddText(font, font->LegacySize, ImVec2(tx, ty), iconCol, currLabel);
}
ImGui::EndDisabled();
}
ImGui::PopFont();
// Preset amount chips
{
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size;
float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size * cardDp;
float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size * cardDp;
struct Preset { const char* label; double amount; };
Preset presets[] = {
{ "0.1", 0.1 },
{ "1", 1.0 },
{ "10", 10.0 },
{ "100", 100.0 },
{ "1K", 1000.0 },
{ "10K", 10000.0 },
{ "50K", 50000.0 },
{ "100K", 100000.0 },
};
constexpr int presetCount = 8;
// Calculate equal chip widths to span full addrColW
float totalGap = chipGap * (presetCount - 1);
float chipW = (addrColW - totalGap) / presetCount;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, chipRound);
ImFont* chipFont = S.resolveFont("button");
for (int ci = 0; ci < presetCount; ci++) {
if (ci > 0) ImGui::SameLine(0, chipGap);
bool active = false;
if (s_request_usd_mode) {
active = (usd_price > 0 && std::abs(s_request_usd_amount - presets[ci].amount) < 0.001);
} else {
active = (std::abs(s_request_amount - presets[ci].amount) < 1e-8);
}
if (active) {
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "chip-active-bg-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary()));
}
char chipId[32];
snprintf(chipId, sizeof(chipId), "%s##presetAmt%d", presets[ci].label, ci);
if (TactileButton(chipId, ImVec2(chipW, chipH), chipFont)) {
if (s_request_usd_mode) {
s_request_usd_amount = presets[ci].amount;
if (usd_price > 0)
s_request_amount = s_request_usd_amount / usd_price;
} else {
s_request_amount = presets[ci].amount;
if (usd_price > 0)
s_request_usd_amount = s_request_amount * usd_price;
}
}
if (active) ImGui::PopStyleColor(2);
}
ImGui::PopStyleVar();
ImGui::PopStyleColor();
}
// Memo (z-addresses only)
if (isZ) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.receive", "memo-label-gap").size));
float memoInputH = std::max(schema::UI().drawElement("tabs.receive", "memo-input-min-height").size, schema::UI().drawElement("tabs.receive", "memo-input-height").size * vScale);
ImGui::PushItemWidth(addrColW);
ImGui::InputTextMultiline("##RequestMemo", s_request_memo, sizeof(s_request_memo),
ImVec2(addrColW, memoInputH));
ImGui::PopItemWidth();
// The Sapling memo limit is in BYTES and matches the enforced buffer size
// (InputTextMultiline caps at sizeof(s_request_memo)); count against that, not the
// old display-only 256 value, and warn in colour once the field is at its cap.
size_t memo_len = strlen(s_request_memo);
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
bool memoAtCap = memo_len + 1 >= memoMax;
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : OnSurfaceDisabled(), buf);
}
// URI preview
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (s_request_amount > 0 && !s_cached_qr_data.empty()) {
float uriWrapW = addrColW;
dl->AddText(capFont, capFont->LegacySize, ImGui::GetCursorScreenPos(),
OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW);
ImVec2 uriSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX,
uriWrapW, s_cached_qr_data.c_str());
ImGui::Dummy(ImVec2(addrColW, uriSz.y + Layout::spacingSm()));
} else {
ImGui::Dummy(ImVec2(addrColW, capFont->LegacySize + Layout::spacingSm()));
}
} else {
ImGui::Dummy(ImVec2(addrColW, capFont->LegacySize + Layout::spacingSm()));
// A mining (transparent) address can't be paid: no payment request and no QR code.
RenderMiningAddressNotice(app, state, addrColW);
}
}
@@ -874,7 +952,21 @@ void RenderReceiveTab(App* app)
DrawGlassPanel(dl, qrPanelMin, qrPanelMax, qrGlass);
ImGui::SetCursorScreenPos(ImVec2(qrPanelMin.x + qrPadding, qrPanelMin.y + qrPadding));
if (s_qr_texture) {
if (!isZ) {
// A mining address: nothing to scan. Mark the panel so the empty square isn't a puzzle.
ImGui::Dummy(ImVec2(qrSize, qrSize));
ImFont* iconF = Type().iconXL();
const char* noPay = TR("receive_not_for_payments");
ImVec2 iSz = iconF->CalcTextSizeA(iconF->LegacySize, FLT_MAX, 0, ICON_MD_CONSTRUCTION);
ImVec2 tSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, qrSize, noPay);
const float blockH = iSz.y + Layout::spacingSm() + tSz.y;
const float cx = qrPanelMin.x + totalQrSize * 0.5f;
const float top = qrPanelMin.y + (totalQrSize - blockH) * 0.5f;
dl->AddText(iconF, iconF->LegacySize, ImVec2(cx - iSz.x * 0.5f, top), WithAlpha(Warning(), 200),
ICON_MD_CONSTRUCTION);
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx - tSz.x * 0.5f, top + iSz.y + Layout::spacingSm()),
OnSurfaceDisabled(), noPay, nullptr, qrSize);
} else if (s_qr_texture) {
RenderQRCode(s_qr_texture, qrSize);
} else {
ImGui::Dummy(ImVec2(qrSize, qrSize));
@@ -886,12 +978,12 @@ void RenderReceiveTab(App* app)
ImGui::SetCursorScreenPos(qrPanelMin);
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
if (ImGui::IsItemHovered()) {
if (isZ && ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s",
s_request_amount > 0 ? TR("click_copy_uri") : TR("click_copy_address"));
}
if (ImGui::IsItemClicked()) {
if (isZ && ImGui::IsItemClicked()) {
ImGui::SetClipboardText(qr_data.c_str());
Notifications::instance().info(s_request_amount > 0
? TR("payment_uri_copied") : TR("address_copied"));
+2
View File
@@ -14,6 +14,8 @@ namespace ui {
* Shows addresses for receiving funds with QR codes
*/
void RenderReceiveTab(App* app);
// UI sweep: select `address` on the Receive tab (nullptr: back to the default choice).
void SweepSelectReceiveAddress(App* app, const char* address);
} // namespace ui
} // namespace dragonx
+6 -20
View File
@@ -6,6 +6,7 @@
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/payment_uri.h"
#include "../../util/address_validation.h"
#include "../notifications.h"
#include "../schema/ui_schema.h"
#include "../widgets/qr_code.h"
@@ -47,8 +48,10 @@ void RequestPaymentDialog::show(const std::string& address)
s_label[0] = '\0';
s_uri_dirty = true;
if (!address.empty()) {
// Only a shielded address can be paid (DragonX is a private chain): never prefill a mining address.
if (!address.empty() && !util::isTransparentAddress(address)) {
strncpy(s_address, address.c_str(), sizeof(s_address) - 1);
s_address[sizeof(s_address) - 1] = '\0';
} else {
s_address[0] = '\0';
}
@@ -78,9 +81,6 @@ void RequestPaymentDialog::render(App* app)
auto zAddrLbl = S.label("dialogs.request-payment", "z-addr-label");
auto zAddrFrontLbl = S.label("dialogs.request-payment", "z-addr-front-label");
auto zAddrBackLbl = S.label("dialogs.request-payment", "z-addr-back-label");
auto tAddrLbl = S.label("dialogs.request-payment", "t-addr-label");
auto tAddrFrontLbl = S.label("dialogs.request-payment", "t-addr-front-label");
auto tAddrBackLbl = S.label("dialogs.request-payment", "t-addr-back-label");
auto amountInput = S.input("dialogs.request-payment", "amount-input");
auto memoInput = S.input("dialogs.request-payment", "memo-input");
auto qr = S.drawElement("dialogs.request-payment", "qr-code");
@@ -124,22 +124,8 @@ void RequestPaymentDialog::render(App* app)
}
}
// T-addresses
if (!state.t_addresses.empty()) {
ImGui::TextDisabled("%s", TR("request_transparent_addrs"));
for (size_t i = 0; i < state.t_addresses.size(); i++) {
const auto& addr = state.t_addresses[i];
std::string label = addr.address;
if (label.length() > static_cast<size_t>(tAddrLbl.truncate)) {
label = label.substr(0, tAddrFrontLbl.truncate) + "..." + label.substr(label.length() - tAddrBackLbl.truncate);
}
if (ImGui::Selectable(label.c_str(), addr.address == s_address)) {
strncpy(s_address, addr.address.c_str(), sizeof(s_address) - 1);
s_uri_dirty = true;
}
}
}
// No mining (transparent) addresses: they only receive mining rewards, so a request to one
// could never be paid.
ImGui::EndCombo();
}
+22 -1
View File
@@ -22,6 +22,7 @@ bool ValidateAddressDialog::s_validating = false;
char ValidateAddressDialog::s_address_input[512] = "";
bool ValidateAddressDialog::s_is_valid = false;
bool ValidateAddressDialog::s_is_mine = false;
bool ValidateAddressDialog::s_is_mining = false;
std::string ValidateAddressDialog::s_address_type;
std::string ValidateAddressDialog::s_error_message;
@@ -33,10 +34,22 @@ void ValidateAddressDialog::show()
s_address_input[0] = '\0';
s_is_valid = false;
s_is_mine = false;
s_is_mining = false;
s_address_type.clear();
s_error_message.clear();
}
void ValidateAddressDialog::sweepShowMiningResult(const char* address)
{
show();
snprintf(s_address_input, sizeof(s_address_input), "%s", address);
s_validated = true;
s_is_valid = true;
s_is_mine = true;
s_is_mining = true;
s_address_type = TR("validate_mining_type");
}
bool ValidateAddressDialog::isOpen()
{
return s_open;
@@ -112,6 +125,7 @@ void ValidateAddressDialog::render(App* app)
s_is_valid = valid;
s_is_mine = mine;
s_address_type = TR("validate_shielded_type");
s_is_mining = false;
} else {
s_error_message = error;
s_is_valid = false;
@@ -138,7 +152,8 @@ void ValidateAddressDialog::render(App* app)
if (error.empty()) {
s_is_valid = valid;
s_is_mine = mine;
s_address_type = TR("validate_transparent_type");
s_address_type = TR("validate_mining_type");
s_is_mining = true;
} else {
s_error_message = error;
s_is_valid = false;
@@ -204,6 +219,12 @@ void ValidateAddressDialog::render(App* app)
} else {
ImGui::TextDisabled("%s", TR("validate_not_mine"));
}
if (s_is_mining) { // valid, but not for payments
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning()));
ImGui::TextWrapped("%s", TR("mining_addr_explainer"));
ImGui::PopStyleColor();
}
}
}
} else if (!app->isConnected()) {
+4
View File
@@ -38,6 +38,9 @@ public:
*/
static void hide();
/** UI sweep: show the result for a valid mining (transparent) address of ours. */
static void sweepShowMiningResult(const char* address);
private:
static bool s_open;
static bool s_validated;
@@ -47,6 +50,7 @@ private:
// Validation results
static bool s_is_valid;
static bool s_is_mine;
static bool s_is_mining; // a valid transparent (mining) address: explain it can't be paid
static std::string s_address_type;
static std::string s_error_message;
};