-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMainWindow.xaml.cs
More file actions
3673 lines (3302 loc) · 173 KB
/
Copy pathMainWindow.xaml.cs
File metadata and controls
3673 lines (3302 loc) · 173 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using WpfEllipse = System.Windows.Shapes.Ellipse;
using HandshakeDVPN.Services;
namespace HandshakeDVPN;
public partial class MainWindow : Window
{
// ─── State ───
private string? _userAddr;
private string? _selectedNode;
private string _connState = "off"; // off | ing | on
private string _activeFilter = "";
private List<HnsNodeInfo> _allNodes = new();
private string? _generatedMnemonic;
private int _balFailCount;
private string _payMode = "gb"; // "gb" or "hr"
private HnsNodeInfo? _selectedNodeInfo;
private bool _isClosing;
private DateTime _lastAllocCheck;
private string? _planSubId;
private int _planId;
private CancellationTokenSource? _connectCts;
private CancellationTokenSource? _refreshCts;
private string? _lastSessionId;
private List<ActiveSession> _activeSessions = new();
private readonly DispatcherTimer _statusPoll;
private readonly DispatcherTimer _ipPoll;
private readonly DispatcherTimer _balPoll;
// ─── User persistence ───
private static readonly string UserFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"HandshakeDVPN", "user.json");
public MainWindow()
{
InitializeComponent();
_statusPoll = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
_statusPoll.Tick += async (_, _) => await PollStatus();
_ipPoll = new DispatcherTimer { Interval = TimeSpan.FromSeconds(60) };
_ipPoll.Tick += async (_, _) => await PollIp();
_balPoll = new DispatcherTimer { Interval = TimeSpan.FromMinutes(5) };
_balPoll.Tick += async (_, _) => await LoadBalance();
Loaded += async (_, _) => await Init();
SetHnsIcon();
}
private void SetHnsIcon()
{
// Render HNS crystalline slash as window icon
var geo = Geometry.Parse("M38.848,15.331 L35.927,10.142 L41.586,10.144 C41.737,10.144 41.913,10.245 41.996,10.38 C42.086,10.529 42.545,11.286 43.097,12.196 C43.758,13.286 44.548,14.588 45,15.331 L38.848,15.331 Z M28.382,46.754 C28.245,47 28.067,47 28.01,47 L25.698,47 C24.474,46.999 23.063,46.998 22.223,46.998 L31.214,30.857 C31.363,30.589 31.36,30.262 31.204,29.997 C31.049,29.733 30.767,29.57 30.461,29.57 L15.137,29.59 L12.09,24.314 L34.409,24.314 C34.433,24.314 34.457,24.31 34.457,24.309 C34.508,24.306 34.558,24.301 34.605,24.289 C34.642,24.281 34.678,24.269 34.714,24.256 C34.733,24.248 34.751,24.241 34.77,24.232 C34.937,24.158 35.085,24.035 35.179,23.862 L38.858,17.06 L44.932,17.06 L28.382,46.754 Z M20.725,46.141 C20.53,45.819 20.276,45.401 19.999,44.946 C19.07,43.412 17.879,41.448 17.744,41.232 C17.699,41.16 17.678,40.982 17.773,40.813 C17.988,40.426 22.057,33.096 23.049,31.31 L28.99,31.302 L20.725,46.141 Z M10.593,36.04 L7.639,30.793 L10.616,25.213 L13.602,30.382 C12.674,32.129 11.226,34.854 10.593,36.04 Z M6.629,36.857 C5.126,36.857 3.631,36.856 3.411,36.856 C3.263,36.856 3.085,36.753 3.004,36.62 L2.043,35.036 C1.358,33.906 0.484,32.466 0,31.669 L6.152,31.669 L9.072,36.858 C8.435,36.858 7.533,36.857 6.629,36.857 Z M16.618,0.246 C16.755,0 16.932,0 16.989,0 L22.802,0.001 L13.786,16.142 C13.779,16.155 13.775,16.17 13.768,16.184 C13.755,16.211 13.743,16.238 13.732,16.267 C13.723,16.295 13.715,16.321 13.707,16.349 C13.7,16.375 13.695,16.401 13.69,16.427 C13.685,16.46 13.682,16.491 13.681,16.523 C13.68,16.538 13.677,16.551 13.677,16.566 C13.677,16.577 13.68,16.587 13.68,16.598 C13.681,16.63 13.685,16.661 13.69,16.692 C13.693,16.718 13.697,16.744 13.704,16.77 C13.71,16.797 13.72,16.823 13.729,16.85 C13.738,16.876 13.747,16.903 13.759,16.928 C13.771,16.953 13.784,16.977 13.798,17.001 C13.812,17.024 13.826,17.048 13.842,17.07 C13.858,17.093 13.877,17.114 13.896,17.136 C13.914,17.156 13.933,17.177 13.953,17.196 C13.973,17.215 13.994,17.231 14.015,17.247 C14.04,17.267 14.064,17.284 14.09,17.3 C14.1,17.306 14.107,17.313 14.117,17.319 C14.127,17.325 14.138,17.327 14.149,17.332 C14.193,17.355 14.24,17.374 14.289,17.389 C14.307,17.394 14.323,17.401 14.342,17.405 C14.405,17.42 14.469,17.429 14.536,17.429 L14.921,17.429 L29.866,17.41 C30.261,18.097 30.999,19.39 31.628,20.491 C32.111,21.337 32.51,22.035 32.824,22.584 L10.594,22.584 C10.584,22.584 10.575,22.587 10.566,22.588 C10.264,22.593 9.972,22.753 9.818,23.041 L6.139,29.939 L0.069,29.939 C2.719,25.184 16.441,0.563 16.618,0.246 Z M24.287,0.879 L24.827,1.767 C25.775,3.331 27.114,5.538 27.256,5.768 C27.301,5.84 27.322,6.017 27.226,6.187 L21.95,15.69 L16.011,15.698 L24.287,0.879 Z M34.405,10.954 L37.36,16.204 L34.347,21.772 C33.988,21.145 33.542,20.364 33.124,19.631 C32.297,18.182 31.694,17.127 31.354,16.538 C31.996,15.362 33.694,12.252 34.405,10.954 Z");
var drawing = new GeometryDrawing(Brushes.Black, null, geo);
var drawingImage = new DrawingImage(drawing);
drawingImage.Freeze();
Icon = drawingImage;
}
// ═══ INIT ═══
private bool _initDone;
private Task Init()
{
if (_initDone) return Task.CompletedTask;
_initDone = true;
App.Backend.OnProgress += (step, detail) =>
{
Dispatcher.Invoke(() =>
{
if (_connState == "ing") StatusSub.Text = detail ?? step;
AddLog(detail ?? step);
});
};
App.Backend.OnLog += (msg) =>
{
if (msg.StartsWith("WARN:")) return; // suppress SDK warnings (pagination, etc)
Dispatcher.Invoke(() => AddLog(msg));
};
return Task.CompletedTask;
}
// ═══ AUTH ═══
private void AuthTab_Click(object sender, RoutedEventArgs e)
{
var tag = (string)((Button)sender).Tag;
ImportPanel.Visibility = tag == "import" ? Visibility.Visible : Visibility.Collapsed;
GenPanel.Visibility = tag == "new" ? Visibility.Visible : Visibility.Collapsed;
AuthTabImport.Foreground = FindBrush(tag == "import" ? "T1" : "T3");
AuthTabImport.Background = tag == "import" ? FindBrush("Bg0") : Brushes.Transparent;
AuthTabNew.Foreground = FindBrush(tag == "new" ? "T1" : "T3");
AuthTabNew.Background = tag == "new" ? FindBrush("Bg0") : Brushes.Transparent;
}
private async void BtnImport_Click(object sender, RoutedEventArgs e)
{
var mn = TbMnemonic.Text.Trim();
var words = mn.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (words.Length != 12 && words.Length != 24)
{
AuthErr.Text = "Mnemonic must be 12 or 24 words";
return;
}
try
{
BtnImport.IsEnabled = false;
AuthErr.Text = "";
var r = await App.Backend.ImportWalletAsync(mn);
if (r is { Valid: true })
{
_userAddr = r.Address;
SaveUser(_userAddr, mn);
AuthOverlay.Visibility = Visibility.Collapsed;
await EnterApp();
}
}
catch (Exception ex) { AuthErr.Text = ex.Message; }
finally { BtnImport.IsEnabled = true; }
}
private async void BtnGen_Click(object sender, RoutedEventArgs e)
{
try
{
BtnGen.IsEnabled = false;
var w = await App.Backend.CreateWalletAsync();
if (w != null)
{
_generatedMnemonic = w.Mnemonic;
GenMnemonic.Text = w.Mnemonic;
GenAddr.Text = w.Address;
GenResult.Visibility = Visibility.Visible;
}
}
catch (Exception ex) { AuthErr.Text = ex.Message; }
finally { BtnGen.IsEnabled = true; }
}
private void BtnUseGen_Click(object sender, RoutedEventArgs e)
{
if (_generatedMnemonic != null)
{
TbMnemonic.Text = _generatedMnemonic;
AuthTab_Click(AuthTabImport, e);
}
}
private void BtnTestWallet_Click(object sender, RoutedEventArgs e)
{
// Generate a fresh test wallet — fund it with P2P tokens before connecting
BtnGen_Click(sender, e);
}
// ═══ WALLET ═══
private void BtnCloseWallet_Click(object sender, RoutedEventArgs e) => WalletOverlay.Visibility = Visibility.Collapsed;
private async void BtnWallet_Click(object sender, RoutedEventArgs e)
{
var panel = WalletPanel;
panel.Children.Clear();
var addr = _userAddr ?? "—";
// Balance (centered)
var balStack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 0, 0, 4) };
var balText = MakeText("Loading...", 32, "T1", HorizontalAlignment.Center, fontWeight: FontWeights.Bold);
balStack.Children.Add(balText);
// P2P label with Sentinel shield
var p2pRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 6, 0, 0) };
var sentLogo = new Viewbox { Width = 20, Height = 22, Margin = new Thickness(0, 0, 6, 0), VerticalAlignment = VerticalAlignment.Center };
var sentCanvas = new Canvas { Width = 241, Height = 241 };
sentCanvas.Children.Add(new System.Windows.Shapes.Path { Fill = new SolidColorBrush(Color.FromRgb(0x01, 0x56, 0xFC)), Data = Geometry.Parse("M178.171 60.8573C179.801 60.8573 181.123 62.1775 181.123 63.8045V110.662V113.572C181.161 119.355 181.207 126.551 180.202 134.203C179.028 143.119 176.7 150.371 173.115 156.367C161.437 175.91 140.465 190.218 120.788 192.134C120.695 192.143 120.593 192.152 120.5 192.152C120.407 192.152 120.304 192.152 120.211 192.134C100.506 190.209 79.5436 175.901 67.8844 156.367C64.2898 150.361 61.9804 143.11 60.7977 134.203C59.7919 126.561 59.8385 119.355 59.8851 113.572V110.662L59.8944 63.8045C59.8944 62.1775 61.2167 60.8573 62.8464 60.8573H178.171ZM178.171 55.0001H62.8464C57.9853 55.0001 54.0276 58.9514 54.0276 63.8045V113.554C53.9717 119.16 53.9065 126.867 54.9774 134.965C56.2625 144.69 58.8328 152.667 62.8371 159.361C68.9833 169.662 77.588 178.755 87.7106 185.672C97.917 192.645 108.934 196.894 119.568 197.944C119.876 197.981 120.183 198 120.49 198C120.798 198 121.105 197.981 121.412 197.944C132.038 196.903 143.045 192.664 153.261 185.681C163.383 178.764 171.988 169.662 178.144 159.361C182.157 152.648 184.727 144.671 186.003 134.965C187.065 126.932 187.018 119.513 186.981 113.563V110.644V63.8045C186.981 58.9514 183.023 55.0001 178.162 55.0001L178.171 55.0001Z") });
sentCanvas.Children.Add(new System.Windows.Shapes.Path { Fill = new SolidColorBrush(Color.FromRgb(0x01, 0x56, 0xFC)), Data = Geometry.Parse("M170.145 120.192C171.011 120.843 172.24 120.229 172.24 119.151L172.213 113.228V71.0006C172.222 70.2754 171.635 69.6804 170.909 69.6804H70.0832C69.3569 69.6804 68.7609 70.2661 68.7609 71.0006V96.9769C68.7609 98.0926 69.0216 99.1896 69.5152 100.184C70.018 101.179 70.7444 102.044 71.6291 102.713L147.479 159.333C148.14 159.826 148.186 160.783 147.591 161.341C146.184 162.662 144.713 163.907 143.176 165.079C142.711 165.432 142.059 165.432 141.593 165.088L70.8655 112.168C69.9994 111.518 68.7702 112.131 68.7702 113.219C68.7702 122.293 67.9693 139.223 75.6055 152.193C84.1357 166.687 98.7841 178.123 113.852 181.823C116.031 182.353 118.201 182.734 120.38 182.92C120.463 182.929 120.547 182.929 120.631 182.92C130.493 182.037 140.532 177.537 149.146 170.741C151.306 169.03 153.374 167.189 155.338 165.209C159.222 161.304 162.602 156.934 165.405 152.202C167.938 147.916 169.531 143.202 170.546 138.489C170.769 137.466 170.965 136.434 171.132 135.402C171.579 132.697 170.48 129.973 168.292 128.327L104.083 80.3814C103.077 79.6283 103.599 78.0199 104.874 78.0199H113.367C113.656 78.0199 113.926 78.1129 114.159 78.2803L170.145 120.183V120.192Z") });
sentLogo.Child = sentCanvas;
p2pRow.Children.Add(sentLogo);
p2pRow.Children.Add(MakeText("P2P", 16, "T2", fontWeight: FontWeights.SemiBold));
balStack.Children.Add(p2pRow);
panel.Children.Add(balStack);
// Deposit section
panel.Children.Add(SettingsDivider());
panel.Children.Add(SettingsSection("DEPOSIT"));
panel.Children.Add(MakeText("Send P2P tokens to this address:", 11, "T3", margin: new Thickness(0, 0, 0, 8)));
// Address with copy
var addrBorder = new Border
{
Background = FindBrush("Bg2"), CornerRadius = new CornerRadius(8),
Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(0, 0, 0, 8),
};
var addrGrid = new Grid();
addrGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
addrGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var addrTb = new TextBox
{
Text = addr, IsReadOnly = true, BorderThickness = new Thickness(0),
Background = Brushes.Transparent, FontSize = 11.5,
FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Foreground = FindBrush("T1"), TextWrapping = TextWrapping.Wrap,
};
Grid.SetColumn(addrTb, 0);
var copyBtn = new Button
{
Content = "Copy", FontSize = 10, Padding = new Thickness(10, 4, 10, 4),
Cursor = Cursors.Hand, Background = FindBrush("Acc"), Foreground = Brushes.White,
VerticalAlignment = VerticalAlignment.Center,
};
copyBtn.Template = CreateBtnTemplate();
copyBtn.Click += (_, _) => { Clipboard.SetText(addr); AddLog("Address copied"); copyBtn.Content = "Copied"; };
Grid.SetColumn(copyBtn, 1);
addrGrid.Children.Add(addrTb);
addrGrid.Children.Add(copyBtn);
addrBorder.Child = addrGrid;
panel.Children.Add(addrBorder);
// QR code
var qrImg = new Image { Width = 180, Height = 180, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 4, 0, 0) };
panel.Children.Add(qrImg);
_ = Task.Run(async () =>
{
try
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
var bytes = await http.GetByteArrayAsync($"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={Uri.EscapeDataString(addr)}");
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.StreamSource = new MemoryStream(bytes);
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit();
bmp.Freeze();
Dispatcher.Invoke(() => qrImg.Source = bmp);
}
catch { }
});
// Withdraw section
panel.Children.Add(SettingsDivider());
panel.Children.Add(SettingsSection("WITHDRAW"));
var tbToAddr = new TextBox
{
FontSize = 11.5, FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Background = FindBrush("Bg2"), Foreground = FindBrush("T1"),
BorderBrush = FindBrush("Bdr"), BorderThickness = new Thickness(1),
Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 0, 0, 6),
};
panel.Children.Add(MakeText("Recipient address:", 11, "T3", margin: new Thickness(0, 0, 0, 4)));
panel.Children.Add(tbToAddr);
var amountRow = new Grid { Margin = new Thickness(0, 0, 0, 8) };
amountRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
amountRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var tbSendAmt = new TextBox
{
FontSize = 12, FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Background = FindBrush("Bg2"), Foreground = FindBrush("T1"),
BorderBrush = FindBrush("Bdr"), BorderThickness = new Thickness(1),
Padding = new Thickness(10, 8, 10, 8),
};
Grid.SetColumn(tbSendAmt, 0);
var unitLabel = MakeText("P2P", 12, "T3");
unitLabel.VerticalAlignment = VerticalAlignment.Center;
unitLabel.Margin = new Thickness(8, 0, 0, 0);
Grid.SetColumn(unitLabel, 1);
amountRow.Children.Add(tbSendAmt);
amountRow.Children.Add(unitLabel);
panel.Children.Add(MakeText("Amount:", 11, "T3", margin: new Thickness(0, 0, 0, 4)));
panel.Children.Add(amountRow);
var sendStatus = MakeText("", 11, "T3", margin: new Thickness(0, 4, 0, 0));
var sendBtn = new Button
{
Content = "Send", FontSize = 13, FontWeight = FontWeights.SemiBold,
Padding = new Thickness(0, 10, 0, 10), Cursor = Cursors.Hand,
Background = FindBrush("Acc"), Foreground = Brushes.White,
HorizontalAlignment = HorizontalAlignment.Stretch,
};
sendBtn.Style = (Style)Application.Current.FindResource("BtnAcc");
sendBtn.Click += async (_, _) =>
{
var to = tbToAddr.Text.Trim();
var amtText = tbSendAmt.Text.Trim();
if (string.IsNullOrEmpty(to) || !to.StartsWith("sent1"))
{
sendStatus.Text = "Invalid address — must start with sent1";
sendStatus.Foreground = FindBrush("Red");
return;
}
if (!double.TryParse(amtText, out var amt) || amt <= 0)
{
sendStatus.Text = "Invalid amount";
sendStatus.Foreground = FindBrush("Red");
return;
}
var udvpn = (long)(amt * 1_000_000);
sendBtn.IsEnabled = false;
sendStatus.Text = "Broadcasting...";
sendStatus.Foreground = FindBrush("T3");
try
{
var backend = (NativeVpnClient)App.Backend;
await backend.EnsureChainPublicAsync();
var chain = backend.GetChain()!;
var wallet = backend.GetWallet()!;
var msg = Sentinel.SDK.Core.MessageBuilder.Send(wallet.Address, to, udvpn);
var txBuilder = new Sentinel.SDK.Core.TransactionBuilder(wallet, chain);
var tx = await txBuilder.BroadcastAsync(msg);
if (tx.Success)
{
sendStatus.Text = $"Sent! TX: {tx.TxHash?[..16]}...";
sendStatus.Foreground = FindBrush("Green");
AddLog($"Sent {amt} P2P to {Trunc(to, 12, 6)}");
await LoadBalance();
}
else
{
sendStatus.Text = $"Failed: {tx.RawLog}";
sendStatus.Foreground = FindBrush("Red");
}
}
catch (Exception ex)
{
sendStatus.Text = $"Error: {ex.Message}";
sendStatus.Foreground = FindBrush("Red");
}
finally { sendBtn.IsEnabled = true; }
};
panel.Children.Add(sendBtn);
panel.Children.Add(sendStatus);
// Export section
panel.Children.Add(SettingsDivider());
panel.Children.Add(SettingsSection("EXPORT"));
// Recovery Phrase
var mnResult = new Border
{
Visibility = Visibility.Collapsed, Background = FindBrush("RedDim"),
CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 10, 12, 10),
Margin = new Thickness(0, 6, 0, 0),
};
var exportMnBtn = MakeExportButton("Recovery Phrase", mnResult, () =>
{
var w = ((NativeVpnClient)App.Backend).GetWallet();
return w?.ExportMnemonicString() ?? "No mnemonic available";
});
panel.Children.Add(exportMnBtn);
panel.Children.Add(mnResult);
// Private Key
var pkResult = new Border
{
Visibility = Visibility.Collapsed, Background = FindBrush("RedDim"),
CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 10, 12, 10),
Margin = new Thickness(0, 6, 0, 0),
};
var exportPkBtn = MakeExportButton("Public Key", pkResult, () =>
{
var w = ((NativeVpnClient)App.Backend).GetWallet();
if (w == null) return "No wallet";
return Convert.ToHexString(w.GetPublicKeyCompressed()).ToLower();
});
panel.Children.Add(exportPkBtn);
panel.Children.Add(pkResult);
WalletOverlay.Visibility = Visibility.Visible;
// Load balance
try
{
var bal = await App.Backend.GetBalanceAsync();
if (bal != null) balText.Text = bal.P2P.ToString("F2");
else balText.Text = "—";
}
catch { balText.Text = "—"; }
}
private void BtnCloseSettings_Click(object sender, RoutedEventArgs e)
{
SettingsOverlay.Visibility = Visibility.Collapsed;
}
private void BtnSettings_Click(object sender, RoutedEventArgs e)
{
var settings = ((NativeVpnClient)App.Backend).Settings;
var stack = SettingsPanel;
stack.Children.Clear();
// ─── DNS ───
stack.Children.Add(SettingsSection("DNS RESOLUTION"));
var dnsRadios = new StackPanel { Margin = new Thickness(0, 0, 0, 8) };
var rbHns = new RadioButton { Content = " Handshake (103.196.38.38, 103.196.38.39)", IsChecked = settings.DnsPreset == "handshake", Tag = "handshake", Margin = new Thickness(0, 4, 0, 4), FontSize = 12, Foreground = FindBrush("T1") };
var rbGoogle = new RadioButton { Content = " Google (8.8.8.8, 8.8.4.4)", IsChecked = settings.DnsPreset == "google", Tag = "google", Margin = new Thickness(0, 4, 0, 4), FontSize = 12, Foreground = FindBrush("T1") };
var rbCf = new RadioButton { Content = " Cloudflare (1.1.1.1, 1.0.0.1)", IsChecked = settings.DnsPreset == "cloudflare", Tag = "cloudflare", Margin = new Thickness(0, 4, 0, 4), FontSize = 12, Foreground = FindBrush("T1") };
var rbCustomDns = new RadioButton { Content = " Custom DNS", IsChecked = settings.DnsPreset == "custom", Tag = "custom", Margin = new Thickness(0, 4, 0, 4), FontSize = 12, Foreground = FindBrush("T1") };
dnsRadios.Children.Add(rbHns);
dnsRadios.Children.Add(rbGoogle);
dnsRadios.Children.Add(rbCf);
dnsRadios.Children.Add(rbCustomDns);
stack.Children.Add(dnsRadios);
var tbCustomDns = new TextBox
{
Text = settings.CustomDns,
FontSize = 11.5,
FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Background = FindBrush("Bg2"),
Foreground = FindBrush("T1"),
BorderBrush = FindBrush("Bdr"),
BorderThickness = new Thickness(1),
Padding = new Thickness(10, 7, 10, 7),
IsEnabled = settings.DnsPreset == "custom",
};
tbCustomDns.SetValue(TagProperty, "customDns");
// placeholder via watermark
if (string.IsNullOrEmpty(tbCustomDns.Text)) tbCustomDns.Text = "9.9.9.9, 149.112.112.112";
rbCustomDns.Checked += (_, _) => tbCustomDns.IsEnabled = true;
rbHns.Checked += (_, _) => tbCustomDns.IsEnabled = false;
rbGoogle.Checked += (_, _) => tbCustomDns.IsEnabled = false;
rbCf.Checked += (_, _) => tbCustomDns.IsEnabled = false;
stack.Children.Add(tbCustomDns);
stack.Children.Add(SettingsNote("Handshake DNS resolves both ICANN domains and Handshake TLDs (.forever, .badass, etc)."));
stack.Children.Add(SettingsDivider());
// ─── LCD ───
stack.Children.Add(SettingsSection("LCD ENDPOINTS (REST API)"));
stack.Children.Add(SettingsEndpoint("https://lcd.sentinel.co", true));
stack.Children.Add(SettingsEndpoint("https://api.sentinel.quokkastake.io"));
stack.Children.Add(SettingsEndpoint("https://sentinel-api.polkachu.com"));
stack.Children.Add(SettingsEndpoint("https://sentinel.api.trivium.network:1317"));
stack.Children.Add(SettingsNote("Custom LCD (overrides defaults):"));
var tbCustomLcd = new TextBox
{
Text = settings.CustomLcd,
FontSize = 11,
FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Background = FindBrush("Bg2"),
Foreground = FindBrush("T1"),
BorderBrush = FindBrush("Bdr"),
BorderThickness = new Thickness(1),
Padding = new Thickness(10, 7, 10, 7),
Margin = new Thickness(0, 4, 0, 0),
};
if (string.IsNullOrEmpty(tbCustomLcd.Text)) tbCustomLcd.Foreground = FindBrush("T3");
stack.Children.Add(tbCustomLcd);
stack.Children.Add(SettingsDivider());
// ─── RPC ───
stack.Children.Add(SettingsSection("RPC ENDPOINTS"));
stack.Children.Add(SettingsEndpoint("https://rpc.sentinel.co:443", true));
stack.Children.Add(SettingsEndpoint("https://sentinel-rpc.polkachu.com"));
stack.Children.Add(SettingsEndpoint("https://rpc.mathnodes.com"));
stack.Children.Add(SettingsNote("Custom RPC (overrides defaults):"));
var tbCustomRpc = new TextBox
{
Text = settings.CustomRpc,
FontSize = 11,
FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Background = FindBrush("Bg2"),
Foreground = FindBrush("T1"),
BorderBrush = FindBrush("Bdr"),
BorderThickness = new Thickness(1),
Padding = new Thickness(10, 7, 10, 7),
Margin = new Thickness(0, 4, 0, 0),
};
stack.Children.Add(tbCustomRpc);
stack.Children.Add(SettingsDivider());
// ─── Tunnel ───
stack.Children.Add(SettingsSection("TUNNEL"));
var cbFullTunnel = new CheckBox { Content = " Full Tunnel — route all traffic through VPN", IsChecked = settings.FullTunnel, FontSize = 12, Foreground = FindBrush("T1"), Margin = new Thickness(0, 4, 0, 4) };
var cbSystemProxy = new CheckBox { Content = " System Proxy — OS SOCKS5 proxy for V2Ray", IsChecked = settings.SystemProxy, FontSize = 12, Foreground = FindBrush("T1"), Margin = new Thickness(0, 4, 0, 4) };
stack.Children.Add(cbFullTunnel);
stack.Children.Add(cbSystemProxy);
stack.Children.Add(SettingsDivider());
// ─── Protocols ───
stack.Children.Add(SettingsSection("PROTOCOLS"));
var tbMtu = SettingsInput("WireGuard MTU", settings.WgMtu.ToString(), "1280-1500");
var tbKeepalive = SettingsInput("Keepalive (sec)", settings.WgKeepalive.ToString(), "15-60");
var tbSocksPort = SettingsInput("V2Ray SOCKS Port", settings.V2RaySocksPort.ToString(), "1024-65535");
stack.Children.Add(tbMtu.row);
stack.Children.Add(tbKeepalive.row);
stack.Children.Add(tbSocksPort.row);
stack.Children.Add(SettingsDivider());
// ─── Session ───
stack.Children.Add(SettingsSection("SESSION DEFAULTS"));
var tbDefaultGb = SettingsInput("Default GB", settings.DefaultGb.ToString(), "1-100");
var cbPreferHourly = new CheckBox { Content = " Prefer hourly pricing when cheaper", IsChecked = settings.PreferHourly, FontSize = 12, Foreground = FindBrush("T1"), Margin = new Thickness(0, 4, 0, 4) };
stack.Children.Add(tbDefaultGb.row);
stack.Children.Add(cbPreferHourly);
stack.Children.Add(SettingsDivider());
// ─── Polling ───
stack.Children.Add(SettingsSection("POLLING INTERVALS"));
var tbStatusPoll = SettingsInput("Status check (sec)", settings.StatusPollSec.ToString(), "1-30");
var tbIpPoll = SettingsInput("IP check (sec)", settings.IpCheckSec.ToString(), "30-300");
var tbBalPoll = SettingsInput("Balance check (sec)", settings.BalanceCheckSec.ToString(), "60-600");
var tbAllocPoll = SettingsInput("Allocation check (sec)", settings.AllocationCheckSec.ToString(), "30-600");
stack.Children.Add(tbStatusPoll.row);
stack.Children.Add(tbIpPoll.row);
stack.Children.Add(tbBalPoll.row);
stack.Children.Add(tbAllocPoll.row);
stack.Children.Add(SettingsDivider());
// ─── Discovery ───
stack.Children.Add(SettingsSection("PLAN DISCOVERY"));
var tbPlanMax = SettingsInput("Max plan ID to probe", settings.PlanProbeMax.ToString(), "100-1000");
stack.Children.Add(tbPlanMax.row);
stack.Children.Add(SettingsDivider());
// ─── Chain (read-only) ───
stack.Children.Add(SettingsSection("CHAIN"));
stack.Children.Add(SettingsRow("Chain ID", "sentinelhub-2", "Sentinel mainnet"));
stack.Children.Add(SettingsRow("Denom", "udvpn", "1 P2P = 1,000,000 udvpn"));
stack.Children.Add(SettingsRow("Gas Price", "0.2 udvpn", "Per gas unit"));
// ─── Save Button ───
var btnSave = new Button
{
Content = "Save Settings",
Margin = new Thickness(0, 20, 0, 0),
Padding = new Thickness(20, 11, 20, 11),
FontSize = 13,
FontWeight = FontWeights.SemiBold,
Cursor = Cursors.Hand,
Background = FindBrush("Acc"),
Foreground = Brushes.White,
HorizontalAlignment = HorizontalAlignment.Stretch,
};
btnSave.Style = (Style)Application.Current.FindResource("BtnAcc");
btnSave.Click += (_, _) =>
{
// DNS
if (rbHns.IsChecked == true) settings.DnsPreset = "handshake";
else if (rbGoogle.IsChecked == true) settings.DnsPreset = "google";
else if (rbCf.IsChecked == true) settings.DnsPreset = "cloudflare";
else if (rbCustomDns.IsChecked == true) { settings.DnsPreset = "custom"; settings.CustomDns = tbCustomDns.Text.Trim(); }
settings.CustomLcd = tbCustomLcd.Text.Trim();
settings.CustomRpc = tbCustomRpc.Text.Trim();
// Tunnel
settings.FullTunnel = cbFullTunnel.IsChecked == true;
settings.SystemProxy = cbSystemProxy.IsChecked == true;
// Protocols
if (int.TryParse(tbMtu.input.Text, out var mtu) && mtu >= 1280 && mtu <= 1500) settings.WgMtu = mtu;
if (int.TryParse(tbKeepalive.input.Text, out var ka) && ka >= 15 && ka <= 60) settings.WgKeepalive = ka;
if (int.TryParse(tbSocksPort.input.Text, out var sp) && sp >= 1024 && sp <= 65535) settings.V2RaySocksPort = sp;
// Session
if (int.TryParse(tbDefaultGb.input.Text, out var gb) && gb >= 1 && gb <= 100) settings.DefaultGb = gb;
settings.PreferHourly = cbPreferHourly.IsChecked == true;
// Polling
if (int.TryParse(tbStatusPoll.input.Text, out var v1) && v1 >= 1) { settings.StatusPollSec = v1; _statusPoll.Interval = TimeSpan.FromSeconds(v1); }
if (int.TryParse(tbIpPoll.input.Text, out var v2) && v2 >= 30) { settings.IpCheckSec = v2; _ipPoll.Interval = TimeSpan.FromSeconds(v2); }
if (int.TryParse(tbBalPoll.input.Text, out var v3) && v3 >= 60) { settings.BalanceCheckSec = v3; _balPoll.Interval = TimeSpan.FromSeconds(v3); }
if (int.TryParse(tbAllocPoll.input.Text, out var v4) && v4 >= 30) settings.AllocationCheckSec = v4;
// Discovery
if (int.TryParse(tbPlanMax.input.Text, out var pm) && pm >= 100) settings.PlanProbeMax = pm;
settings.Save();
AddLog($"Settings saved — DNS: {settings.GetDnsDisplay()}");
SettingsOverlay.Visibility = Visibility.Collapsed;
};
stack.Children.Add(btnSave);
SettingsOverlay.Visibility = Visibility.Visible;
}
private static StackPanel SettingsSection(string title)
{
var sp = new StackPanel { Margin = new Thickness(0, 0, 0, 10) };
sp.Children.Add(new TextBlock
{
Text = title,
FontSize = 10,
FontWeight = FontWeights.Bold,
Foreground = (Brush)Application.Current.FindResource("T3"),
FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Margin = new Thickness(0, 0, 0, 8),
});
return sp;
}
private static Grid SettingsRow(string label, string value, string? hint = null)
{
var grid = new Grid { Margin = new Thickness(0, 0, 0, 6) };
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var left = new StackPanel();
left.Children.Add(new TextBlock
{
Text = label,
FontSize = 12,
Foreground = (Brush)Application.Current.FindResource("T1"),
});
if (hint != null)
left.Children.Add(new TextBlock
{
Text = hint,
FontSize = 10,
Foreground = (Brush)Application.Current.FindResource("T3"),
Margin = new Thickness(0, 1, 0, 0),
});
Grid.SetColumn(left, 0);
var right = new TextBlock
{
Text = value,
FontSize = 11.5,
FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Foreground = (Brush)Application.Current.FindResource("T2"),
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Right,
};
Grid.SetColumn(right, 1);
grid.Children.Add(left);
grid.Children.Add(right);
return grid;
}
private static Border SettingsEndpoint(string url, bool primary = false)
{
var border = new Border
{
Background = primary
? (Brush)Application.Current.FindResource("GreenDim")
: (Brush)Application.Current.FindResource("Bg2"),
CornerRadius = new CornerRadius(5),
Padding = new Thickness(10, 5, 10, 5),
Margin = new Thickness(0, 0, 0, 4),
};
var grid = new Grid();
grid.Children.Add(new TextBlock
{
Text = url,
FontSize = 10.5,
FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Foreground = (Brush)Application.Current.FindResource(primary ? "Green" : "T2"),
});
if (primary)
{
var badge = new TextBlock
{
Text = "PRIMARY",
FontSize = 8,
FontWeight = FontWeights.Bold,
FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Foreground = (Brush)Application.Current.FindResource("Green"),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
};
grid.Children.Add(badge);
}
border.Child = grid;
return border;
}
private static TextBlock SettingsNote(string text)
{
return new TextBlock
{
Text = text,
FontSize = 10,
Foreground = (Brush)Application.Current.FindResource("T3"),
TextWrapping = TextWrapping.Wrap,
FontStyle = FontStyles.Italic,
Margin = new Thickness(0, 4, 0, 0),
};
}
private Button MakeExportButton(string label, Border resultBorder, Func<string> getValue)
{
var btn = new Button
{
Content = $"Show {label}", FontSize = 11, Padding = new Thickness(0, 7, 0, 7),
Cursor = Cursors.Hand, Background = FindBrush("Bg2"), Foreground = FindBrush("T2"),
HorizontalAlignment = HorizontalAlignment.Stretch, Margin = new Thickness(0, 6, 0, 0),
};
btn.Template = CreateBtnTemplate();
btn.Click += (_, _) =>
{
if (resultBorder.Visibility == Visibility.Visible) { resultBorder.Visibility = Visibility.Collapsed; btn.Content = $"Show {label}"; return; }
var value = getValue();
var stack = new StackPanel();
stack.Children.Add(MakeText("KEEP THIS SECRET — DO NOT SHARE", 9, "Red", fontWeight: FontWeights.Bold));
var valueRow = new Grid { Margin = new Thickness(0, 6, 0, 0) };
valueRow.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
valueRow.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var valueTb = new TextBox
{
Text = value, IsReadOnly = true, TextWrapping = TextWrapping.Wrap,
BorderThickness = new Thickness(0), Background = Brushes.Transparent,
FontSize = 11, FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Foreground = FindBrush("T1"),
};
Grid.SetColumn(valueTb, 0);
var copyBtn = new Button
{
Content = "Copy", FontSize = 9, Padding = new Thickness(8, 3, 8, 3),
Cursor = Cursors.Hand, Background = FindBrush("Acc"), Foreground = Brushes.White,
VerticalAlignment = VerticalAlignment.Top,
};
copyBtn.Template = CreateBtnTemplate();
copyBtn.Click += (_, _) => { Clipboard.SetText(value); copyBtn.Content = "Copied"; };
Grid.SetColumn(copyBtn, 1);
valueRow.Children.Add(valueTb);
valueRow.Children.Add(copyBtn);
stack.Children.Add(valueRow);
resultBorder.Child = stack;
resultBorder.Visibility = Visibility.Visible;
btn.Content = $"Hide {label}";
};
return btn;
}
private static (Grid row, TextBox input) SettingsInput(string label, string value, string hint)
{
var grid = new Grid { Margin = new Thickness(0, 4, 0, 4) };
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(100) });
var left = new StackPanel();
left.Children.Add(new TextBlock { Text = label, FontSize = 12, Foreground = (Brush)Application.Current.FindResource("T1") });
left.Children.Add(new TextBlock { Text = hint, FontSize = 9, Foreground = (Brush)Application.Current.FindResource("T3") });
Grid.SetColumn(left, 0);
var input = new TextBox
{
Text = value,
FontSize = 12,
FontFamily = (FontFamily)Application.Current.FindResource("Mono"),
Background = (Brush)Application.Current.FindResource("Bg2"),
Foreground = (Brush)Application.Current.FindResource("T1"),
BorderBrush = (Brush)Application.Current.FindResource("Bdr"),
BorderThickness = new Thickness(1),
Padding = new Thickness(8, 4, 8, 4),
TextAlignment = TextAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetColumn(input, 1);
grid.Children.Add(left);
grid.Children.Add(input);
return (grid, input);
}
private static Border SettingsDivider()
{
return new Border
{
Height = 1,
Background = (Brush)Application.Current.FindResource("Bdr"),
Margin = new Thickness(0, 12, 0, 12),
};
}
// ═══ ENTER APP ═══
private static bool _nodesLoaded;
// ═══ ENTER APP ═══
private async Task EnterApp()
{
AddLog($"Wallet: {Trunc(_userAddr ?? "", 12, 6)}");
// Show cached data instantly
var cachedNodes = DiskCache.Load<List<HnsNodeInfo>>("nodes", TimeSpan.FromMinutes(30));
var cachedPlans = DiskCache.Load<List<PlanInfo>>("plans", TimeSpan.FromMinutes(30));
var cachedTests = DiskCache.Load<List<NodeTestResult>>("test-results", TimeSpan.FromDays(7));
if (cachedTests?.data != null) _testResults = cachedTests.Value.data;
var cachedSessions = DiskCache.Load<List<ActiveSession>>("sessions", TimeSpan.FromMinutes(10));
if (cachedNodes?.data != null && cachedNodes.Value.data.Count > 0)
{
_allNodes = cachedNodes.Value.data;
_nodesLoaded = true;
var online = _allNodes.Count(n => n.Moniker != null);
TbNodeCount.Text = $"{online} nodes";
TbOnlineCount.Text = cachedNodes.Value.isStale ? "updating..." : "online";
RenderNodes();
AddLog($"Cached: {online} nodes");
}
if (cachedPlans?.data != null) { _plans = cachedPlans.Value.data; AddLog($"Cached: {_plans.Count} plans"); }
if (cachedSessions?.data != null) _activeSessions = cachedSessions.Value.data;
// Refresh in background (single probe)
if (!_nodesLoaded)
{
// No cache — must load fresh
await LoadNodes();
_nodesLoaded = true;
}
else
{
// Cache shown — refresh silently in background
_refreshCts = new CancellationTokenSource();
_ = RefreshAllAsync();
}
// Balance loads inside RefreshAllAsync — don't call separately (avoids double LCD init)
await CheckExisting();
_statusPoll.Start();
_ipPoll.Start();
_balFailCount = 0;
_balPoll.Interval = TimeSpan.FromMinutes(5);
_balPoll.Start();
}
private async Task LoadBalance()
{
try
{
var b = await App.Backend.GetBalanceAsync();
if (b != null)
{
TbAddr.Text = Trunc(_userAddr ?? "", 10, 4);
TbBalVal.Text = b.P2P.ToString("F2");
_balFailCount = 0;
_balPoll.Interval = TimeSpan.FromMinutes(5);
}
else
{
_balFailCount++;
if (_balFailCount >= 10) _balPoll.Stop();
else if (_balFailCount >= 3) _balPoll.Interval = TimeSpan.FromMinutes(5);
}
}
catch { /* silent */ }
}
// ═══ NODES ═══
private static bool _refreshing;
private async Task RefreshAllAsync()
{
if (_refreshing) return;
_refreshing = true;
try
{
// Load balance first (uses same chain client, no extra LCD init)
await LoadBalance();
// Refresh nodes
var data = await App.Backend.GetAllNodesAsync();
if (data != null && data.Nodes.Count > 0)
{
DiskCache.Save("nodes", data.Nodes);
Dispatcher.Invoke(() =>
{
_allNodes = data.Nodes;
_nodesLoaded = true;
var online = _allNodes.Count(n => n.Moniker != null);
TbNodeCount.Text = $"{online} nodes";
AddLog($"Updated: {online} nodes");
});
}
// Refresh sessions (only if wallet loaded)
if (App.Backend.HasWallet)
{
var sessions = await App.Backend.GetActiveSessionsAsync();
if (sessions != null) { _activeSessions = sessions; DiskCache.Save("sessions", sessions); }
}
// Refresh plans (only once — guard against double discovery)
if (App.Backend.HasWallet)
{
var plans = await App.Backend.DiscoverPlansAsync();
if (plans != null) { _plans = plans; DiskCache.Save("plans", plans); }
}
}
catch { }
finally { _refreshing = false; }
}
private async Task LoadNodes()
{
// Show loading state immediately
NodeListPanel.Children.Clear();
NodeListPanel.Children.Add(MakeLoadingIndicator());
TbNodeCount.Text = "loading...";
TbOnlineCount.Text = "";
try
{
var data = await App.Backend.GetAllNodesAsync();
if (data == null) { AddLog("Failed to load nodes", true); ShowEmptyState("Failed to load nodes"); return; }
_allNodes = data.Nodes;
var online = _allNodes.Count(n => n.Moniker != null);
TbNodeCount.Text = $"{online} nodes";
TbOnlineCount.Text = "online";
RenderNodes();
DiskCache.Save("nodes", _allNodes);
if (_unknownCountries.Count > 0)
AddLog($"Unknown countries (no flag): {string.Join(", ", _unknownCountries)}");
_unknownCountries.Clear();
}
catch (Exception ex) { AddLog($"Node error: {ex.Message}", true); ShowEmptyState(ex.Message); }
}
private Border MakeLoadingIndicator()
{
var stack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 40, 0, 0) };
// Sentinel shield logo
var sentinelLogo = new Viewbox { Width = 40, Height = 44, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 0, 0, 14) };
var canvas = new Canvas { Width = 241, Height = 241 };
var path1 = new System.Windows.Shapes.Path { Fill = new SolidColorBrush(Color.FromRgb(0x01, 0x56, 0xFC)), Data = Geometry.Parse("M178.171 60.8573C179.801 60.8573 181.123 62.1775 181.123 63.8045V110.662V113.572C181.161 119.355 181.207 126.551 180.202 134.203C179.028 143.119 176.7 150.371 173.115 156.367C161.437 175.91 140.465 190.218 120.788 192.134C120.695 192.143 120.593 192.152 120.5 192.152C120.407 192.152 120.304 192.152 120.211 192.134C100.506 190.209 79.5436 175.901 67.8844 156.367C64.2898 150.361 61.9804 143.11 60.7977 134.203C59.7919 126.561 59.8385 119.355 59.8851 113.572V110.662L59.8944 63.8045C59.8944 62.1775 61.2167 60.8573 62.8464 60.8573H178.171ZM178.171 55.0001H62.8464C57.9853 55.0001 54.0276 58.9514 54.0276 63.8045V113.554C53.9717 119.16 53.9065 126.867 54.9774 134.965C56.2625 144.69 58.8328 152.667 62.8371 159.361C68.9833 169.662 77.588 178.755 87.7106 185.672C97.917 192.645 108.934 196.894 119.568 197.944C119.876 197.981 120.183 198 120.49 198C120.798 198 121.105 197.981 121.412 197.944C132.038 196.903 143.045 192.664 153.261 185.681C163.383 178.764 171.988 169.662 178.144 159.361C182.157 152.648 184.727 144.671 186.003 134.965C187.065 126.932 187.018 119.513 186.981 113.563V110.644V63.8045C186.981 58.9514 183.023 55.0001 178.162 55.0001L178.171 55.0001Z") };
var path2 = new System.Windows.Shapes.Path { Fill = new SolidColorBrush(Color.FromRgb(0x01, 0x56, 0xFC)), Data = Geometry.Parse("M170.145 120.192C171.011 120.843 172.24 120.229 172.24 119.151L172.213 113.228V71.0006C172.222 70.2754 171.635 69.6804 170.909 69.6804H70.0832C69.3569 69.6804 68.7609 70.2661 68.7609 71.0006V96.9769C68.7609 98.0926 69.0216 99.1896 69.5152 100.184C70.018 101.179 70.7444 102.044 71.6291 102.713L147.479 159.333C148.14 159.826 148.186 160.783 147.591 161.341C146.184 162.662 144.713 163.907 143.176 165.079C142.711 165.432 142.059 165.432 141.593 165.088L70.8655 112.168C69.9994 111.518 68.7702 112.131 68.7702 113.219C68.7702 122.293 67.9693 139.223 75.6055 152.193C84.1357 166.687 98.7841 178.123 113.852 181.823C116.031 182.353 118.201 182.734 120.38 182.92C120.463 182.929 120.547 182.929 120.631 182.92C130.493 182.037 140.532 177.537 149.146 170.741C151.306 169.03 153.374 167.189 155.338 165.209C159.222 161.304 162.602 156.934 165.405 152.202C167.938 147.916 169.531 143.202 170.546 138.489C170.769 137.466 170.965 136.434 171.132 135.402C171.579 132.697 170.48 129.973 168.292 128.327L104.083 80.3814C103.077 79.6283 103.599 78.0199 104.874 78.0199H113.367C113.656 78.0199 113.926 78.1129 114.159 78.2803L170.145 120.183V120.192Z") };
canvas.Children.Add(path1);
canvas.Children.Add(path2);
sentinelLogo.Child = canvas;
stack.Children.Add(sentinelLogo);
stack.Children.Add(MakeText("Querying Sentinel chain...", 13, "T2", HorizontalAlignment.Center));
stack.Children.Add(MakeText("Fetching active nodes", 11, "T3", HorizontalAlignment.Center, new Thickness(0, 4, 0, 0)));
return new Border { Child = stack };
}
private void ShowEmptyState(string message)
{
NodeListPanel.Children.Clear();
var stack = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 40, 0, 0) };
stack.Children.Add(MakeText("No nodes loaded", 13, "T3", HorizontalAlignment.Center));
stack.Children.Add(MakeText(message, 11, "Red", HorizontalAlignment.Center, new Thickness(0, 6, 0, 0)));
NodeListPanel.Children.Add(new Border { Child = stack });
}
private void RenderNodes()
{
NodeListPanel.Children.Clear();
var filtered = FilterNodes();
if (filtered.Count == 0)
{
NodeListPanel.Children.Add(MakeText("No nodes found", 13, "T3", HorizontalAlignment.Center, new Thickness(0, 30, 0, 0)));
return;
}
// Group by country
var groups = filtered
.GroupBy(n => string.IsNullOrWhiteSpace(n.Country) ? "Unknown" : n.Country)
.OrderBy(g => g.Key);
foreach (var g in groups)
{
var country = g.Key;
var nodes = g.ToList();
var code = CountryCode(country);
var onlineCount = nodes.Count(n => n.Moniker != null);
// Country header with proper vertical alignment
var headerGrid = new Grid();
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var leftPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
var chevron = new TextBlock { Text = "\u25B6", FontSize = 8, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0) };
try { chevron.Foreground = (Brush)Application.Current.FindResource("T3"); } catch { }
leftPanel.Children.Add(chevron);
var flagImg = MakeFlagImage(code);
if (flagImg is FrameworkElement fe) fe.VerticalAlignment = VerticalAlignment.Center;
leftPanel.Children.Add(flagImg);
var countryTb = MakeText(country, 11.5, "T1", fontWeight: FontWeights.Medium);
countryTb.VerticalAlignment = VerticalAlignment.Center;
countryTb.Margin = new Thickness(6, 0, 0, 0);
leftPanel.Children.Add(countryTb);
Grid.SetColumn(leftPanel, 0);
var countText = MakeText($"{onlineCount}/{nodes.Count}", 9.5, "T3", fontFamily: "Mono");
countText.VerticalAlignment = VerticalAlignment.Center;
Grid.SetColumn(countText, 1);
headerGrid.Children.Add(leftPanel);
headerGrid.Children.Add(countText);
var header = new Border
{