fix(ui): keep a chosen send-from address from auto-select overwrite

The Send tab auto-selected the highest-balance address on first load.
When another tab had already pre-filled the source with "Send from this
address", that first-load pick overwrote the user's choice, and the
dropdown's selected index could drift out of step with the address that
would actually be debited when the address list changed.

- The send-from string is now the single source of truth. The one-shot
  auto-pick only fills an empty source and never replaces one the user
  or another tab set, and the list index is re-derived from the string
  each frame so it cannot desync.
- An explicit "Send from this address" choice latches the auto-pick off,
  so the first visit cannot undo it.
- A chosen source the dropdown does not list (zero balance, view-only,
  or the list still loading) now previews the address that will be
  debited instead of the "Select a source" placeholder.

User-visible: a send-from address chosen from another tab survives the
first load and later refresh ticks.

Tests: the source resolver keeps a pre-fill on first visit, fills an
empty source with the best spendable address, keeps a source missing
from a short list with index -1, latches nothing while disconnected or
with an empty list, and re-derives a stale index from the string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 21:42:23 -05:00
co-authored by Claude Opus 4.8
parent 579accd0c6
commit d4339be94b
4 changed files with 89 additions and 9 deletions
+33
View File
@@ -1131,6 +1131,39 @@ void testSpendableFiltering()
auto includeZero = dragonx::sortedSpendableAddressIndices(addresses, false);
EXPECT_EQ(includeZero.size(), static_cast<size_t>(3));
using dragonx::resolveSourceSelection;
EXPECT_EQ(dragonx::addressIndexByString(addresses, "R-high"), 3);
EXPECT_EQ(dragonx::addressIndexByString(addresses, "nope"), -1);
EXPECT_EQ(dragonx::addressIndexByString(addresses, ""), -1);
{ // a "Send from this address" pre-fill on the first visit must survive the auto-pick
auto r = resolveSourceSelection(addresses, "zs-low", /*latched=*/false, /*connected=*/true);
EXPECT_EQ(r.address, std::string("zs-low"));
EXPECT_EQ(r.index, 1);
EXPECT_TRUE(r.autoLatched);
}
{ // empty source -> best spendable (R-high), latched
auto r = resolveSourceSelection(addresses, "", false, true);
EXPECT_EQ(r.address, std::string("R-high"));
EXPECT_EQ(r.index, 3);
EXPECT_TRUE(r.autoLatched);
}
{ // a pre-fill that is not in the (short) list is kept, index -1, no re-pick
auto r = resolveSourceSelection(addresses, "zs-gone", false, true);
EXPECT_EQ(r.address, std::string("zs-gone"));
EXPECT_EQ(r.index, -1);
EXPECT_TRUE(r.autoLatched);
}
{ // not connected / empty list: nothing latches, pre-fill kept
EXPECT_FALSE(resolveSourceSelection(addresses, "zs-low", false, false).autoLatched);
EXPECT_FALSE(resolveSourceSelection({}, "zs-low", false, true).autoLatched);
EXPECT_EQ(resolveSourceSelection({}, "zs-low", false, true).address, std::string("zs-low"));
}
{ // already latched + stale index: index is re-derived from the string
auto r = resolveSourceSelection(addresses, "R-high", true, true);
EXPECT_EQ(r.index, 3);
EXPECT_TRUE(r.autoLatched);
}
}
// S8: deferFor() is an absolute hold, independent of the page interval (the retry-storm fix).