Skip to content

Commit ef88f12

Browse files
Ryanmello07claude
andcommitted
app: three defects the wallet surfaces only showed once they were driven
All three were found by running the app and clicking it, not by reading it. 1. EVERY UrCard IN THE APP HAD NO EDGE. NavigationView paints its content region with NavigationViewContentBackground, which in the dark theme is LayerFillColorDefaultBrush - white at 5.36%. Over the brand #101010 that composites to #1C1C1C, which is EXACTLY UrCardBrush. Screen-captured and sampled: page and card both read (28,28,28), on the wallet destination and on the connect drawer, so it is the shell and not one screen. Overriding the key to Transparent lets the root grid's brand #101010 through and every card gets its edge back. This was invisible until the mica removal (c82fe28) put a solid colour behind the content again. 2. A FAILED ACTION IN THE WALLET SHEET RENDERED BEHIND THE WALLET SHEET. The sheet is modal and covers the page, and it was raising the page's snackbar - so pressing Remove with an expired session left the dialog open, unchanged, with "401 Unauthorized" drawn underneath it. Screenshotted. Failures now render on the sheet's own line; the page snackbar is reserved for the success path, which also closes the sheet. 3. A DROPPED REQUEST LEFT THE SHEET PERMANENTLY DEAD. Api::setPayoutWallet does not invoke its callback at all when wallet_id is not a UUID - no result, no error, nothing in the log - so the sheet sat with both actions greyed out and no message, with no way out but Close. Every id the UI passes comes from getAccountWallets and is well-formed, so this is a guard rather than a routine path, but "the caller always passes a good id" is an invariant nothing enforces. A 20s watchdog now re-enables the actions and says so, verified by feeding a deliberately malformed id and watching it recover. Also: the points and reliability cards collapse when their panel is empty (an empty card is a bar of nothing sitting under its own status line), and --preview-ui grows a second gate. URNETWORK_PREVIEW_SAMPLE=1, on top of --preview-ui, pushes obviously synthetic rows through the same Apply* functions the API path uses. Reason: --preview-ui makes no request by design, so every panel here renders EMPTY - and a wallet card, a payouts table, a points breakdown, a reliability chart and a leaderboard row are exactly the things this project has repeatedly shipped without ever drawing. Two gates, no network, and a warning line in the log every time, since data on screen that did not come from the server is the one thing a screenshot cannot tell you. Defects 2 and 3 were both found through it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PT7KcWCPKfFwQUc7SM3oZY
1 parent f3783c0 commit ef88f12

5 files changed

Lines changed: 296 additions & 32 deletions

File tree

app/src/App/App.xaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,26 @@
8686
keeps 1dp and only changes its colour -->
8787
<Thickness x:Key="TextControlBorderThemeThickness">0,0,0,1</Thickness>
8888
<Thickness x:Key="TextControlBorderThemeThicknessFocused">0,0,0,1</Thickness>
89+
90+
<!-- NavigationView paints its own content region with
91+
NavigationViewContentBackground, which in the dark theme is
92+
LayerFillColorDefaultBrush: white at 5.36%. Over the brand
93+
#101010 that composites to #1C1C1C — EXACTLY UrCardBrush.
94+
The result was that every UrCard in the app (the connect
95+
drawer's controls card, the account panel's, all of the
96+
wallet destination's) drew its background in the same
97+
colour as the page behind it and had no edge at all;
98+
screen-captured and sampled, page and card both read
99+
(28,28,28). Verified across two destinations, so it is the
100+
shell, not one screen.
101+
102+
Transparent lets the root grid's brand #101010 through,
103+
which is the value every other client uses for the page and
104+
the one the card colour was chosen against. Nothing else
105+
depends on the layer tint: the app pins RequestedTheme=Dark
106+
and has no light variant to keep in step. -->
107+
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
108+
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
89109
</ResourceDictionary>
90110
</ResourceDictionary.ThemeDictionaries>
91111
</ResourceDictionary>

app/src/App/MainWindow.xaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,10 @@
800800
<TextBlock x:Name="AccountPointsHeading" FontFamily="{StaticResource UrHeadingFontFamily}" FontSize="16" FontWeight="SemiBold"
801801
Margin="0,12,0,0" />
802802
<TextBlock x:Name="AccountPointsStatusText" Style="{StaticResource UrSupportingTextStyle}" />
803-
<Border Style="{StaticResource UrCardStyle}">
803+
<!-- named so the page can collapse it: an empty card is a
804+
bar of nothing sitting under its own error message -->
805+
<Border x:Name="AccountPointsCard" Style="{StaticResource UrCardStyle}"
806+
Visibility="Collapsed">
804807
<StackPanel x:Name="AccountPointsPanel" Spacing="0" />
805808
</Border>
806809

@@ -820,7 +823,8 @@
820823
<TextBlock x:Name="NetworkReliabilityHeading" FontFamily="{StaticResource UrHeadingFontFamily}" FontSize="16" FontWeight="SemiBold"
821824
Margin="0,12,0,0" />
822825
<TextBlock x:Name="ReliabilityStatusText" Style="{StaticResource UrSupportingTextStyle}" />
823-
<Border Style="{StaticResource UrCardStyle}">
826+
<Border x:Name="ReliabilityCard" Style="{StaticResource UrCardStyle}"
827+
Visibility="Collapsed">
824828
<StackPanel x:Name="ReliabilityPanel" Spacing="8" />
825829
</Border>
826830

app/src/App/WalletPage.cpp

Lines changed: 195 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-License-Identifier: MPL-2.0
1+
// SPDX-License-Identifier: MPL-2.0
22
#include "pch.h"
33

44
#include "WalletPage.h"
@@ -11,8 +11,10 @@
1111
#include <chrono>
1212
#include <cmath>
1313
#include <cstdio>
14+
#include <cstdlib>
1415
#include <cwchar>
1516
#include <iterator>
17+
#include <string_view>
1618

1719
#include "Log.h"
1820
#include "MainWindow.xaml.h"
@@ -191,6 +193,167 @@ UIElement BuildReliabilityChart(std::vector<double> weights, std::vector<double>
191193
return host;
192194
}
193195

196+
// ---- --preview-ui sample data ----------------------------------------------
197+
//
198+
// WHY THIS EXISTS. --preview-ui deliberately makes no API call (Startup.h): with
199+
// no token every request would be an unauthenticated hit on the production API,
200+
// so every panel on this destination renders EMPTY. That is worth looking at and
201+
// it is not the populated one - and a wallet card, a payouts table, a points
202+
// breakdown, a reliability chart and a leaderboard row are exactly the things
203+
// where "reads correct" has never been evidence on this project. Without this
204+
// they could ship having never been drawn.
205+
//
206+
// So: with --preview-ui ALREADY on, URNETWORK_PREVIEW_SAMPLE=1 pushes obviously
207+
// synthetic rows through the SAME Apply* functions the API path uses. Two gates,
208+
// no network, and a warning in the log every time, because data on screen that
209+
// did not come from the server is the one thing a screenshot cannot show you.
210+
//
211+
// The values are deliberately not plausible as anybody's account: the wallet
212+
// addresses spell what they are.
213+
constexpr const char* kSampleOwnNetworkId = "sample-network-self";
214+
215+
bool PreviewSample() {
216+
static const bool on = [] {
217+
size_t len = 0;
218+
char value[16]{};
219+
if (getenv_s(&len, value, sizeof(value), "URNETWORK_PREVIEW_SAMPLE") != 0 || len == 0) {
220+
return false;
221+
}
222+
const bool enabled = std::string_view(value) == "1";
223+
if (enabled) {
224+
urnw::LogWarn(
225+
"preview-sample: rendering SYNTHETIC wallet/leaderboard rows - none of "
226+
"this came from the api");
227+
}
228+
return enabled;
229+
}();
230+
return on;
231+
}
232+
233+
std::vector<urnet::AccountWallet> SampleWallets() {
234+
urnet::AccountWallet sol;
235+
sol.wallet_id = "5a3e0000-0000-4000-8000-00000000501a";
236+
sol.blockchain = urnet::SOL;
237+
sol.wallet_address = "SAMPLEwa11etADDRESSnotREALsolana000000SAMPLE";
238+
sol.default_token_type = "USDC";
239+
sol.active = true;
240+
sol.has_seeker_token = true;
241+
242+
urnet::AccountWallet matic;
243+
matic.wallet_id = "5a3e0000-0000-4000-8000-0000000a71c0";
244+
matic.blockchain = urnet::MATIC;
245+
matic.wallet_address = "0xSAMPLEwa11etADDRESSnotREALpolygonSAMPLE";
246+
matic.default_token_type = "USDC";
247+
matic.active = true;
248+
249+
urnet::AccountWallet tao;
250+
tao.wallet_id = "5a3e0000-0000-4000-8000-00000000007a";
251+
tao.blockchain = urnet::TAO;
252+
tao.wallet_address = "5SAMPLEwa11etADDRESSnotREALbittensorSAMPLE";
253+
tao.default_token_type = "USDC";
254+
tao.active = true;
255+
return {sol, matic, tao};
256+
}
257+
258+
std::vector<urnet::AccountPayment> SamplePayments() {
259+
auto make = [](const char* id, const char* wallet, const char* chain, double amount,
260+
const char* when, const char* tx, bool completed) {
261+
urnet::AccountPayment p;
262+
p.payment_id = id;
263+
p.wallet_id = wallet;
264+
p.blockchain = chain;
265+
p.token_type = "USDC";
266+
p.token_amount = amount;
267+
p.complete_time = completed ? std::optional<std::string>(when) : std::nullopt;
268+
p.create_time = when;
269+
p.completed = completed;
270+
p.wallet_address = "SAMPLEwa11etADDRESSnotREALsolana000000SAMPLE";
271+
if (tx) p.tx_hash = tx;
272+
p.payout_byte_count = 8'123'456'789;
273+
return p;
274+
};
275+
return {
276+
make("5a3e0000-0000-4000-8000-0000000000a1", "5a3e0000-0000-4000-8000-00000000501a", urnet::SOL, 0, "2026-08-02T00:00:00Z", nullptr,
277+
false),
278+
make("5a3e0000-0000-4000-8000-0000000000a2", "5a3e0000-0000-4000-8000-00000000501a", urnet::SOL, 12.34, "2026-07-26T00:00:00Z",
279+
"SAMPLEtxHASHnotREAL2222222222222222", true),
280+
make("5a3e0000-0000-4000-8000-0000000000a3", "5a3e0000-0000-4000-8000-0000000a71c0", urnet::MATIC, 8.90, "2026-07-19T00:00:00Z",
281+
"0xSAMPLEtxHASHnotREAL33333333333333", true),
282+
};
283+
}
284+
285+
std::vector<urnet::AccountPoint> SamplePoints() {
286+
auto make = [](const char* event, int64_t nanoPoints, const char* paymentId) {
287+
urnet::AccountPoint p;
288+
p.event = event;
289+
p.point_value = nanoPoints;
290+
p.account_payment_id = paymentId;
291+
return p;
292+
};
293+
const int64_t nano = 1'000'000'000;
294+
return {
295+
make(kEventPayout, 12'340 * nano, "5a3e0000-0000-4000-8000-0000000000a2"),
296+
make(kEventReferral, 2'100 * nano, "5a3e0000-0000-4000-8000-0000000000a2"),
297+
make(kEventReliability, 860 * nano, "5a3e0000-0000-4000-8000-0000000000a2"),
298+
make(kEventMultiplier, 12'340 * nano, "5a3e0000-0000-4000-8000-0000000000a2"),
299+
make(kEventPayout, 8'900 * nano, "5a3e0000-0000-4000-8000-0000000000a3"),
300+
make(kEventReliability, 415 * nano, "5a3e0000-0000-4000-8000-0000000000a3"),
301+
};
302+
}
303+
304+
urnet::ReliabilityWindow SampleReliability() {
305+
urnet::ReliabilityWindow rw;
306+
rw.mean_reliability_weight = 0.62;
307+
rw.max_total_client_count = 18;
308+
rw.max_client_count = 11;
309+
rw.bucket_duration_seconds = 3600;
310+
std::vector<double> weights;
311+
std::vector<int64_t> totals;
312+
for (int i = 0; i < 24; ++i) {
313+
weights.push_back(0.35 + 0.45 * std::sin(i * 0.5) * std::sin(i * 0.5) + 0.05 * (i % 3));
314+
totals.push_back(4 + (i * 7) % 15);
315+
}
316+
rw.reliability_weights = weights;
317+
rw.total_client_counts = totals;
318+
rw.client_counts = totals;
319+
320+
urnet::CountryMultiplierList countries;
321+
auto country = [](const char* name, const char* code, double multiplier) {
322+
urnet::CountryMultiplier cm;
323+
cm.country_location_id = std::string("sample-") + code;
324+
cm.country = name;
325+
cm.country_code = code;
326+
cm.reliability_multiplier = multiplier;
327+
return cm;
328+
};
329+
countries.push_back(country("Sample Republic", "SR", 3.25));
330+
countries.push_back(country("Sampleland", "SL", 2.00));
331+
countries.push_back(country("Samplia", "SA", 1.40));
332+
countries.push_back(country("Not Multiplied", "NM", 1.00)); // filtered out
333+
rw.country_multipliers = countries;
334+
return rw;
335+
}
336+
337+
urnet::LeaderboardEarnersList SampleEarners() {
338+
auto make = [](const char* id, const char* name, float mib, bool isPublic,
339+
bool profanity = false) {
340+
urnet::LeaderboardEarner e;
341+
e.network_id = id;
342+
e.network_name = name;
343+
e.net_mib_count = mib;
344+
e.is_public = isPublic;
345+
e.contains_profanity = profanity;
346+
return e;
347+
};
348+
return {
349+
make("sample-net-1", "sample-alpha", 4'194'304.0f, true),
350+
make("sample-net-2", "hidden-should-not-render", 2'097'152.0f, /*isPublic=*/false),
351+
make(kSampleOwnNetworkId, "sample-my-network", 786'432.0f, true),
352+
make("sample-net-4", "profane-should-not-render", 524'288.0f, true, /*profanity=*/true),
353+
make("sample-net-5", "sample-epsilon", 131'072.0f, true),
354+
};
355+
}
356+
194357
} // namespace
195358

196359
WalletPage::WalletPage(winrt::URnetwork::implementation::MainWindow& window)
@@ -519,10 +682,10 @@ winrt::fire_and_forget WalletPage::ShowWalletDetail(urnet::AccountWallet wallet)
519682
[weak] {
520683
if (auto w = weak.get()) w->wallet().RefreshAfterWalletChange();
521684
},
522-
[weak](hstring message, bool ok) {
685+
[weak](hstring message) {
686+
// success only: the sheet is gone by the time this runs
523687
if (auto w = weak.get()) {
524-
w->wallet().snackbar_.Show(message, ok ? InfoBarSeverity::Success
525-
: InfoBarSeverity::Error);
688+
w->wallet().snackbar_.Show(message, InfoBarSeverity::Success);
526689
}
527690
});
528691
co_await self->wallet().walletSheet_->Dialog().ShowAsync();
@@ -571,9 +734,11 @@ void WalletPage::ApplyPoints(std::vector<urnet::AccountPoint> const& points, Fet
571734
w_.AccountPointsStatusText().Text(Loc("something_went_wrong"));
572735
w_.AccountPointsStatusText().Visibility(Visibility::Visible);
573736
w_.AccountPointsPanel().Children().Clear();
737+
w_.AccountPointsCard().Visibility(Visibility::Collapsed);
574738
return;
575739
}
576740
w_.AccountPointsStatusText().Visibility(Visibility::Collapsed);
741+
w_.AccountPointsCard().Visibility(Visibility::Visible);
577742
RebuildPointsCard();
578743
RebuildWalletCards(); // the per-wallet totals do not move, but the seeker row does
579744
}
@@ -736,6 +901,8 @@ void WalletPage::ApplyReliability(std::optional<urnet::ReliabilityWindow> window
736901
reliability_ = window;
737902
auto panel = w_.ReliabilityPanel();
738903
panel.Children().Clear();
904+
// an empty card is a bar of nothing sitting under its own status line
905+
w_.ReliabilityCard().Visibility(Visibility::Collapsed);
739906

740907
if (state == Fetch::Failed) {
741908
w_.ReliabilityStatusText().Text(Loc("something_went_wrong"));
@@ -748,6 +915,7 @@ void WalletPage::ApplyReliability(std::optional<urnet::ReliabilityWindow> window
748915
return;
749916
}
750917
w_.ReliabilityStatusText().Visibility(Visibility::Collapsed);
918+
w_.ReliabilityCard().Visibility(Visibility::Visible);
751919
auto const& rw = *reliability_;
752920

753921
// the two figures the window is summarised by
@@ -856,7 +1024,7 @@ void WalletPage::ApplySeekerState() {
8561024
// Claim the 2x multiplier by proving the wallet holds the Seeker / Saga token
8571025
// (android SettingsScreen.signAndVerifySeekerHolder). The wallet signs a
8581026
// timestamped challenge through the ur.io/wallet-connect browser bridge and the
859-
// signed triple goes to Api.verifySeekerHolder the address alone proves
1027+
// signed triple goes to Api.verifySeekerHolder — the address alone proves
8601028
// nothing, so there is no shortcut past the signature.
8611029
//
8621030
// android puts up its wallet picker through Mobile Wallet Adapter; the browser
@@ -949,7 +1117,7 @@ void WalletPage::ApplySeekerResult(bool ok, std::string const& serverError) {
9491117
// ---- connect wallet (external wallet, by address) -------------------------
9501118
// Paste an address; the server validates it per chain and the first chain that
9511119
// accepts it wins. Bittensor connects by address only (no signature), matching
952-
// apple/android the signed bridge flow is sign-in, not wallet connect.
1120+
// apple/android — the signed bridge flow is sign-in, not wallet connect.
9531121

9541122
void WalletPage::OnWalletAddressChanged(IInspectable const&, TextChangedEventArgs const&) {
9551123
walletValidation_ = {};
@@ -1063,6 +1231,17 @@ void WalletPage::ShowPreviewSnackbar() {
10631231
// otherwise sit on "Loading..." forever - which is exactly what a hang looks
10641232
// like. Settle them all on their empty state instead.
10651233
void WalletPage::ShowPreviewWalletState() {
1234+
if (PreviewSample()) {
1235+
ApplyWallets(SampleWallets(), Fetch::Ready);
1236+
ApplyPayoutWalletId("5a3e0000-0000-4000-8000-00000000501a");
1237+
ApplyTransferStats(41'237'481'984, /*ok=*/true);
1238+
ApplyWalletBalance(1'234'500'000'000, /*ok=*/true);
1239+
ApplyReferrals(7, /*ok=*/true);
1240+
ApplyPayments(SamplePayments(), Fetch::Ready);
1241+
ApplyPoints(SamplePoints(), Fetch::Ready);
1242+
ApplyReliability(SampleReliability(), Fetch::Ready);
1243+
return;
1244+
}
10661245
ApplyWallets({}, Fetch::Ready);
10671246
ApplyTransferStats(0, /*ok=*/false);
10681247
ApplyWalletBalance(0, /*ok=*/false);
@@ -1075,6 +1254,16 @@ void WalletPage::ShowPreviewWalletState() {
10751254
// ---- leaderboard ---------------------------------------------------------
10761255

10771256
void WalletPage::ShowPreviewLeaderboardState() {
1257+
if (PreviewSample()) {
1258+
ownNetworkId_ = kSampleOwnNetworkId;
1259+
urnet::NetworkRanking ranking;
1260+
ranking.leaderboard_rank = 42;
1261+
ranking.net_mib_count = 786432.0f; // 768 GiB
1262+
ranking.leaderboard_public = true;
1263+
ApplyRanking(ranking, /*ok=*/true);
1264+
ApplyLeaderboard(SampleEarners(), Fetch::Ready);
1265+
return;
1266+
}
10781267
ApplyRanking({}, /*ok=*/false);
10791268
ApplyLeaderboard({}, Fetch::Ready);
10801269
}

0 commit comments

Comments
 (0)