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
+48 -17
View File
@@ -4410,21 +4410,10 @@ void testChooseDaemonStatus()
EXPECT_TRUE(chooseDaemonStatus(false, false, idle) == DaemonStatusKind::Launching); // not bound yet
}
// S10: every string added by the daemon-stop work exists in English and in all 8 languages, with the same
// printf arguments (a mismatch is rejected at runtime and would fall back to English, or worse).
void testDaemonStopStringsTranslated()
// Every key exists in English and in all 8 languages, with the same printf arguments (a mismatch is rejected
// at runtime and falls back to English, or worse).
static void expectKeysTranslated(const std::vector<const char*>& keys)
{
const char* keys[] = {
"shutdown_leave_running", "shutdown_leave_running_tip", "shutdown_kill_node", "shutdown_kill_node_tip",
"shutdown_kill_confirm_title", "shutdown_kill_confirm_msg", "shutdown_kill_confirm_yes",
"shutdown_stop_asking", "shutdown_stop_queued", "shutdown_stop_delivered", "shutdown_stop_cpu_suffix",
"shutdown_stop_idle", "node_busy_rebuild_title", "node_busy_rebuild_desc", "node_busy_rebuild_reading",
"node_busy_rebuild_extending", "node_busy_replay_title", "node_busy_replay_desc", "node_busy_rpc_title",
"node_busy_rpc_desc", "node_starting_title", "node_starting_busy_desc", "node_starting_launch_desc",
"sb_node_busy", "stopconfirm_rebuild_title", "stopconfirm_rebuild_body", "stopconfirm_busy_title",
"stopconfirm_busy_body", "stopconfirm_stop_wait", "appx_daemon_reinstall_node_busy", "node_busy_running",
"appx_node_busy_try_later", "appx_node_not_stopped",
};
// printf conversions in order, "%%" excluded: what must survive translation.
auto conversions = [](const std::string& s) {
std::string sig;
@@ -4451,6 +4440,34 @@ void testDaemonStopStringsTranslated()
}
}
// S10: every string added by the daemon-stop work.
void testDaemonStopStringsTranslated()
{
expectKeysTranslated({
"shutdown_leave_running", "shutdown_leave_running_tip", "shutdown_kill_node", "shutdown_kill_node_tip",
"shutdown_kill_confirm_title", "shutdown_kill_confirm_msg", "shutdown_kill_confirm_yes",
"shutdown_stop_asking", "shutdown_stop_queued", "shutdown_stop_delivered", "shutdown_stop_cpu_suffix",
"shutdown_stop_idle", "node_busy_rebuild_title", "node_busy_rebuild_desc", "node_busy_rebuild_reading",
"node_busy_rebuild_extending", "node_busy_replay_title", "node_busy_replay_desc", "node_busy_rpc_title",
"node_busy_rpc_desc", "node_starting_title", "node_starting_busy_desc", "node_starting_launch_desc",
"sb_node_busy", "stopconfirm_rebuild_title", "stopconfirm_rebuild_body", "stopconfirm_busy_title",
"stopconfirm_busy_body", "stopconfirm_stop_wait", "appx_daemon_reinstall_node_busy", "node_busy_running",
"appx_node_busy_try_later", "appx_node_not_stopped",
});
}
// Mining addresses: the strings that explain transparent addresses only receive mining rewards.
void testMiningAddressStringsTranslated()
{
expectKeysTranslated({
"send_mining_addr_short", "send_mining_addr_blocked", "mining_payout_needs_shielded", "new_mining_address",
"tt_new_mining_address", "mining_address", "addr_type_mining", "mining_rewards", "mining_rewards_unshielded",
"baltab_mining_addresses_count", "baltab_mining_amount", "balance_mining_fmt", "mining_card_tooltip",
"mining_card_click_to_shield", "contact_chip_mining", "failed_create_mining_address",
"new_mining_address_created",
});
}
// S3: on Linux a node in its final shutdown phase reads comm "hush-shutoff"; its executable still says
// dragonxd, so it must still count as running (it holds the datadir lock until it exits).
void testDaemonProcessIdentity()
@@ -7787,12 +7804,25 @@ void testAddressChecksumValidation()
EXPECT_FALSE(isShieldedAddress(p2pkh));
EXPECT_TRUE(isShieldedAddress(zaddr));
EXPECT_FALSE(isTransparentAddress(zaddr));
EXPECT_TRUE(isValidRecipientAddress(p2pkh));
EXPECT_TRUE(isValidRecipientAddress(p2sh));
// Both kinds are DragonX addresses, but only a shielded one can be paid: the chain is private, so
// consensus rejects a payment to a transparent address (it only ever receives mining rewards).
using dragonx::util::isDragonXAddress;
using dragonx::util::classifyRecipient;
using dragonx::util::RecipientKind;
EXPECT_TRUE(isDragonXAddress(p2pkh));
EXPECT_TRUE(isDragonXAddress(p2sh));
EXPECT_TRUE(isDragonXAddress(zaddr));
EXPECT_FALSE(isValidRecipientAddress(p2pkh));
EXPECT_FALSE(isValidRecipientAddress(p2sh));
EXPECT_TRUE(isValidRecipientAddress(zaddr));
EXPECT_TRUE(classifyRecipient(zaddr) == RecipientKind::Shielded);
EXPECT_TRUE(classifyRecipient(p2pkh) == RecipientKind::MiningOnly);
EXPECT_TRUE(classifyRecipient(p2sh) == RecipientKind::MiningOnly);
EXPECT_TRUE(classifyRecipient("x") == RecipientKind::Invalid);
EXPECT_TRUE(classifyRecipient("zivks1qqqsyqcyq5rqwzqfpg9scrgwpugpzysn") == RecipientKind::Invalid);
// A WIF spending key is NOT a recipient (33/34-byte payload, not 21); nor is a typo'd address.
EXPECT_FALSE(isTransparentAddress("Up3W7uVYkLxCfH91APxjSpkkGBWJyBrm3tt1bCz64V5fpZK9ef3C"));
EXPECT_FALSE(isValidRecipientAddress("R9NXAVJezHiBnT3ijTpg3JUZre7PxhJWtX")); // flipped checksum char
EXPECT_FALSE(isDragonXAddress("R9NXAVJezHiBnT3ijTpg3JUZre7PxhJWtX")); // flipped checksum char
EXPECT_FALSE(isValidRecipientAddress(""));
}
@@ -9213,6 +9243,7 @@ int main()
testOutputSinceAcrossTrim();
testChooseDaemonStatus();
testDaemonStopStringsTranslated();
testMiningAddressStringsTranslated();
#ifdef _WIN32
testWindowsStopNeverKillsForeignDaemon();
#endif