feat(ui): treat transparent addresses as mining-only addresses

DragonX is a private chain (-ac_private=1): consensus rejects any
non-coinbase output to a transparent address, so a t-address can only
ever receive mining rewards, and its coins can only move to a shielded
address. The wallet treated them like ordinary recipients.

Block paying one:
- Send: a transparent recipient shows "Mining address - can't be paid"
  and disables Review Send (tooltip explains; ask for a zs... address).
  Typing, paste, contacts and payment URIs all go through this field.
- Overview drag-to-transfer: a mining-address row is never a transfer
  target (it only reorders).
- Pool payout must be shielded (pools pay with ordinary transactions):
  field tooltip, disabled Start button and the start check say why.
- isValidRecipientAddress() is now shielded-only; new isDragonXAddress()
  and classifyRecipient() (Shielded / MiningOnly / Invalid).

Call them mining addresses:
- Overview: "Mining rewards" card (amber "Not yet shielded" when there
  is a balance; hover explains; click opens the Shield dialog), "New
  mining address" button with a tooltip, rows typed "Mining".
- QR / label dialogs, portfolio filter, contact badge ("Mining only"),
  Receive notifications.

i18n: 17 new keys in all 8 languages, 12 dead keys removed, CJK subset
rebuilt. Tests: recipient classification, translation completeness.
UI sweep: new "send-mining-recipient" target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 20:08:14 -05:00
co-authored by Claude Opus 5.5
parent a3361020d1
commit 5246898968
26 changed files with 468 additions and 162 deletions
+20 -5
View File
@@ -693,6 +693,14 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
// ============================================================================
// Confirmation dialog
// ============================================================================
void SweepSetSendRecipient(App* app, const char* to) {
(void)app;
std::snprintf(s_from_address, sizeof(s_from_address), "%s",
to ? "zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000" : "");
std::snprintf(s_to_address, sizeof(s_to_address), "%s", to ? to : "");
s_amount = to ? 1.0 : 0.0;
}
void SweepShowSendConfirm(App* app, bool show) {
(void)app;
if (show) {
@@ -975,7 +983,7 @@ static bool RenderZeroBalanceCTA(App* app, ImDrawList* dl, float width) {
// Action buttons (Send + Clear + Undo)
// ============================================================================
static void RenderActionButtons(App* app, float width, float vScale,
bool is_valid_address, double available,
bool is_valid_address, bool is_mining_addr, double available,
const char* suffix = "") {
auto& S = schema::UI();
const float dp = Layout::dpiScale();
@@ -1027,6 +1035,8 @@ static void RenderActionButtons(App* app, float width, float vScale,
material::Tooltip("%s", TR("send_tooltip_syncing"));
else if (s_from_address[0] == '\0')
material::Tooltip("%s", TR("send_tooltip_select_source"));
else if (is_mining_addr)
material::Tooltip("%s", TR("send_mining_addr_blocked"));
else if (!is_valid_address)
material::Tooltip("%s", TR("send_tooltip_invalid_address"));
else if (s_amount <= 0)
@@ -1255,8 +1265,10 @@ void RenderSendTab(App* app)
double available = GetAvailableBalance(app);
bool is_valid_z = IsValidShieldedAddr(s_to_address);
// A transparent address is a mining address: it only ever receives mining rewards, and the network
// rejects a payment to it (DragonX is a private chain). Recognise it, but never send to it.
bool is_valid_t = IsValidTransparentAddr(s_to_address);
bool is_valid_address = is_valid_z || is_valid_t;
bool is_valid_address = is_valid_z;
float sectionGap = Layout::spacingXl() * vScale;
char buf[128];
@@ -1365,7 +1377,9 @@ void RenderSendTab(App* app)
if (vz)
Type().textColored(TypeStyle::Caption, Success(), TR("send_valid_shielded"));
else
Type().textColored(TypeStyle::Caption, Warning(), TR("send_valid_transparent"));
Type().textColored(TypeStyle::Caption, Error(), TR("send_mining_addr_short"));
if (vt && ImGui::IsItemHovered())
material::Tooltip("%s", TR("send_mining_addr_blocked"));
} else if (!checkPreview) {
ImGui::SameLine();
Type().textColored(TypeStyle::Caption, Error(), TR("invalid_address"));
@@ -1405,7 +1419,8 @@ void RenderSendTab(App* app)
trimmed.erase(trimmed.begin());
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\n' || trimmed.back() == '\r' || trimmed.back() == '\t'))
trimmed.pop_back();
bool looksValid = dragonx::util::isValidRecipientAddress(trimmed);
// Preview any address, a mining one too: once pasted, the field explains why it can't be paid.
bool looksValid = dragonx::util::isDragonXAddress(trimmed);
if (looksValid && s_to_address[0] == '\0') {
s_preview_text = trimmed;
s_paste_previewing = true;
@@ -1626,7 +1641,7 @@ void RenderSendTab(App* app)
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
// ---- ACTION BUTTONS (full width) ----
RenderActionButtons(app, innerW, vScale, is_valid_address, available, "##main");
RenderActionButtons(app, innerW, vScale, is_valid_address, is_valid_t, available, "##main");
// Add bottom padding
ImGui::Dummy(ImVec2(0, pad * vScale));