new-node-list-ui-on-dev (#3259)
What changed, and why it matters
This commit redesigns the node list user interface and adds a way to scan or share node connection details via QR code. The most notable security-relevant change is that node connectivity checks now route through a 'ProxyWrapper' helper with a flag that allows bypassing SSL certificate validation for Monero nodes. That bypass could make it easier for an attacker on the same network to impersonate a node and intercept traffic. The commit also introduces a new constructor that parses node URIs, including username and password, from scanned QR codes or links.
Review whether allowMitmMoneroBypassSSLCheck: true is intentional and necessary; if it is a debugging or Tor-compatibility flag, ensure it is gated by explicit user consent or Tor mode and not active for clearnet connections. Validate and sanitize Node.fromUri() inputs before persisting or connecting, especially for credentials and the 'trusted' query parameter. Confirm that parallel speed tests do not leak node lists or timing information unexpectedly, and that the new QR-code node import path cannot be triggered by malicious payment QR codes.
Security signals we found
SSL/TLS certificate validation bypass flag introduced for Monero node RPC checks (allowMitmMoneroBypassSSLCheck: true)
New Node.fromUri() parses credentials and trusted flag from arbitrary URIs and is reachable from QR/link scanning
Node speed tests now run in parallel against all configured nodes with stored latency results
Removal of separate PoW node list view model; consolidation into unified NodeListViewModel with isPow parameter
Evidence from the diff
The patch is primarily a UI/UX refactor of the node management screens. Security signals include: (1) in cw_core/lib/node.dart, the direct HTTP client call is replaced by ProxyWrapper().post() with allowMitmMoneroBypassSSLCheck: true, enabling certificate pinning bypass for Monero RPC checks; (2) a new Node.fromUri() constructor extracts host, port, path, credentials, SSL preference, and ‘trusted’ flag from arbitrary URIs and is invoked from the coin action row when a scanned string has http/https/tcp scheme; (3) node speed tests run in parallel with a 10-second timeout and store latencies in an observable map; (4) the separate PowNodeListViewModel is folded into NodeListViewModel via an isPow flag. No explicit security disclosure, CVE, or researcher attribution is present in the commit or supplied references.
Changed components
cw_core/lib/node.dartcw_core/lib/node_list.dartlib/src/screens/nodes/node_create_or_edit_page.dartlib/src/screens/nodes/node_share_page.dartlib/src/screens/nodes/widgets/node_list_row.dartlib/src/screens/settings/manage_nodes_page.dartlib/view_model/node_list/node_list_view_model.dartlib/view_model/node_list/node_create_or_edit_view_model.dartlib/new-ui/widgets/coins_page/action_row/coin_action_row.dartInspect captured patch +472 / −404
diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart
index 7550fc62..b7f2703b 100644
--- a/cw_core/lib/node.dart
+++ b/cw_core/lib/node.dart
@@ -132,6 +132,26 @@ class Node {
};
}
+ Node.fromUri(Uri uri, WalletType type)
+ : id = 0,
+ uriRaw =
+ "${uri.host}${uri.hasPort ? ':${uri.port}' : (uri.queryParameters['port']?.isNotEmpty == true ? ':${uri.queryParameters['port']}' : '')}",
+ path = uri.path,
+ login = uri.userInfo.contains(':')
+ ? uri.userInfo.split(':')[0]
+ : uri.queryParameters['username'],
+ password = uri.userInfo.contains(':')
+ ? uri.userInfo.split(':')[1]
+ : uri.queryParameters['password'],
+ useSSL = uri.queryParameters['protocol'] == 'https',
+ trusted = uri.queryParameters['trusted'] == 'true',
+ isPow = false,
+ isEnabledForAutoSwitching = false,
+ isOfficial = false,
+ isBuiltin = false,
+ isDefault = false,
+ typeRaw = serializeToInt(type);
+
Future<int> delete() async {
return await db!.delete(tableName, where: '${selfIdColumn} = ?', whereArgs: [id]);
}
@@ -378,14 +398,14 @@ class Node {
final body = {'jsonrpc': '2.0', 'id': '0', 'method': methodName};
try {
- final client = ProxyWrapper().getHttpIOClient();
final jsonBody = json.encode(body);
- final response = await client.post(
- rpcUri,
+ final response = await ProxyWrapper().post(
+ clearnetUri: rpcUri,
headers: {'Content-Type': 'application/json'},
body: jsonBody,
+ allowMitmMoneroBypassSSLCheck: true
);
// Check if we received a 401 Unauthorized response
if (response.statusCode == 401) {
diff --git a/cw_core/lib/node_list.dart b/cw_core/lib/node_list.dart
index e46f0eee..2c5007be 100644
--- a/cw_core/lib/node_list.dart
+++ b/cw_core/lib/node_list.dart
@@ -1,5 +1,4 @@
import 'package:flutter/services.dart';
-import 'package:hive/hive.dart';
import "package:yaml/yaml.dart";
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
@@ -78,6 +77,7 @@ Future<List<Node>> loadDefaultNodes(WalletType type) async {
map["isBuiltin"] = 1;
map["isOfficial"] = ((map["isOfficial"] as bool? ?? false) ? 1 : 0);
map["isDefault"] = ((map["isDefault"] as bool? ?? false) ? 1 : 0);
+
final node = Node.fromMap(Map<String, Object>.from(map));
node.type = type;
nodes.add(node);
@@ -94,7 +94,11 @@ Future<List<Node>> loadDefaultNanoPowNodes() async {
for (final raw in loadedPowNodes) {
if (raw is Map) {
- final node = Node.fromMap(Map<String, Object>.from(raw));
+ final map = Map.from(raw);
+ map["isBuiltin"] = 1;
+ map["isOfficial"] = ((map["isOfficial"] as bool? ?? false) ? 1 : 0);
+ map["isDefault"] = ((map["isDefault"] as bool? ?? false) ? 1 : 0);
+ final node = Node.fromMap(Map<String, Object>.from(map));
node.isPow = true;
node.type = WalletType.nano;
nodes.add(node);
diff --git a/lib/di.dart b/lib/di.dart
index aa42887d..991e2f7e 100644
--- a/lib/di.dart
+++ b/lib/di.dart
@@ -1118,9 +1118,9 @@ Future<void> setup({
getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));
- getIt.registerLazySingleton(() {
+ getIt.registerFactoryParam<NodeListViewModel, bool, void>((isPow, _) {
final appStore = getIt.get<AppStore>();
- return NodeListViewModel(appStore);
+ return NodeListViewModel(appStore, isPow);
});
getIt.registerFactoryParam<NewAddressesPage, bool, void>(
@@ -1131,11 +1131,6 @@ Future<void> setup({
),
);
- getIt.registerLazySingleton(() {
- final appStore = getIt.get<AppStore>();
- return PowNodeListViewModel(appStore);
- });
-
getIt.registerFactory(() => ConnectionSyncViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
getIt.registerFactory(() => ConnectionSyncPage(getIt.get<ConnectionSyncViewModel>()));
@@ -1178,8 +1173,6 @@ Future<void> setup({
final Node? editingNode = args['editingNode'] as Node?;
return NodeCreateOrEditViewModel(
isPow,
- getIt.get<NodeListViewModel>(),
- getIt.get<PowNodeListViewModel>(),
type,
getIt.get<SettingsStore>(),
editingNode: editingNode
@@ -1700,10 +1693,7 @@ Future<void> setup({
);
getIt.registerFactoryParam<ManageNodesPage, bool, void>((bool isPow, _) {
- if (isPow) {
- return ManageNodesPage(isPow, powNodeListViewModel: getIt.get<PowNodeListViewModel>(), dashboardViewModel: getIt.get<DashboardViewModel>());
- }
- return ManageNodesPage(isPow, nodeListViewModel: getIt.get<NodeListViewModel>(), dashboardViewModel: getIt.get<DashboardViewModel>());
+ return ManageNodesPage(isPow, nodeListViewModel: getIt.get<NodeListViewModel>(param1: isPow), dashboardViewModel: getIt.get<DashboardViewModel>());
});
getIt.registerFactory(
diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart
index 10a411c2..b700f4aa 100644
--- a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart
+++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart
@@ -17,10 +17,10 @@ import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/lnurl.dart';
+import 'package:cw_core/node.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_svg/svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import '../../../pages/receive_page.dart';
@@ -198,7 +198,12 @@ class CoinActionRow extends StatelessWidget {
Navigator.of(context)
.pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
return;
- } else {
+ } else if(["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
+ Navigator.of(context).pushNamed(Routes.newNode,
+ arguments: {"editingNode": Node.fromUri(Uri.parse(code), walletType)});
+ return;
+
+ }else {
final uri = Uri.tryParse(code);
if (uri == null) return;
req = PaymentRequest.fromUri(uri);
diff --git a/lib/new-ui/widgets/modal_header.dart b/lib/new-ui/widgets/modal_header.dart
index 46c292ce..6bef2bba 100644
--- a/lib/new-ui/widgets/modal_header.dart
+++ b/lib/new-ui/widgets/modal_header.dart
@@ -4,11 +4,12 @@ import 'package:flutter_svg/svg.dart';
class ModalHeader extends StatelessWidget {
const ModalHeader(
- {super.key, required this.iconPath, required this.message, required this.title});
+ {super.key, required this.iconPath, required this.message, required this.title, this.bottomWidget});
final String iconPath;
final String title;
final String message;
+ final Widget? bottomWidget;
@override
Widget build(BuildContext context) {
@@ -41,7 +42,11 @@ class ModalHeader extends StatelessWidget {
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
- ))
+ )),
+ if(bottomWidget != null) ...[
+ Padding(padding: EdgeInsets.symmetric(horizontal: 18), child: Container(height:1, color: Theme.of(context).colorScheme.surfaceContainerHigh),),
+ bottomWidget!
+ ]
],
),
),
diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart
index 92bccb27..0e45c51f 100644
--- a/lib/src/screens/nodes/node_create_or_edit_page.dart
+++ b/lib/src/screens/nodes/node_create_or_edit_page.dart
@@ -1,20 +1,20 @@
import 'package:cake_wallet/core/execution_state.dart';
+import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
+import 'package:cake_wallet/src/screens/nodes/node_share_page.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
-import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
-import 'package:cake_wallet/src/widgets/primary_button.dart';
-import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
+import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
+import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_mobx/flutter_mobx.dart';
-import 'package:flutter_svg/svg.dart';
import 'package:mobx/mobx.dart';
class NodeCreateOrEditPage extends StatefulWidget {
@@ -76,6 +76,14 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
},
);
+
+ reaction((_)=>widget.nodeCreateOrEditViewModel.state, (state) async {
+ if(state is ExecutedSuccessfullyState) {
+ await Future.delayed(Duration(milliseconds: 100));
+ if (mounted) Navigator.of(context).pop(widget.nodeCreateOrEditViewModel.editingNode);
+ }
+
+ });
}
@override
@@ -90,86 +98,62 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
title: widget.editingNode != null ? S.current.edit_node : S.current.node_new,
leadingIcon: Icon(Icons.arrow_back_ios_new),
onLeadingPressed: Navigator.of(context).pop,
- trailingIcon: CakeImageWidget(imageUrl:
- "assets/new-ui/scan.svg",width:24,height:24,
- colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
- ),
- onTrailingPressed: () => widget.nodeCreateOrEditViewModel.scanQRCodeForNewNode(context),
+ trailingIcon: Icon(Icons.check),
+ onTrailingPressed: () async {
+ if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
+ return;
+ }
+
+ await widget.nodeCreateOrEditViewModel
+ .save(saveAsCurrent: widget.isSelected ?? false);
+ },
),
Expanded(
child: KeyboardHideOverlay(
child: Container(
padding: const EdgeInsets.only(left: 18, right: 18),
- child: ScrollableWithBottomSection(
- contentPadding: const EdgeInsets.only(bottom: 24.0, top: 8),
- content: NodeForm(
+ child: Column(
+ spacing: 18,
+ children: [NodeForm(
key: _nodeFormKey,
nodeViewModel: widget.nodeCreateOrEditViewModel,
),
- bottomSectionPadding: const EdgeInsets.only(bottom: 24),
- bottomSection: Observer(
- builder: (_) => Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- Flexible(
- child: Container(
- padding: const EdgeInsets.only(right: 8.0),
- child: LoadingPrimaryButton(
- onPressed: () async {
- final confirmed = await showPopUp<bool>(
- context: context,
- builder: (context) => AlertWithTwoActions(
- alertTitle: S.of(context).remove_node,
- alertContent: S.of(context).remove_node_message,
- rightButtonText: S.of(context).remove,
- leftButtonText: S.of(context).cancel,
- actionRightButton: () => Navigator.pop(context, true),
- actionLeftButton: () => Navigator.pop(context, false),
- ),
- ) ??
- false;
-
- if (confirmed) {
- await widget.nodeCreateOrEditViewModel.delete(editingNode: widget.editingNode!);
- Navigator.of(context).pop();
- }
- },
- text: S.of(context).delete,
- isDisabled: widget.editingNode == null ||
- !widget.nodeCreateOrEditViewModel.isReady ||
- widget.editingNode!.isBuiltin ||
- (widget.isSelected ?? false),
- color: Theme.of(context).colorScheme.errorContainer,
- textColor: Theme.of(context).colorScheme.onErrorContainer,
- ),
- ),
- ),
- Flexible(
- child: Container(
- padding: const EdgeInsets.only(left: 8.0),
- child: PrimaryButton(
- onPressed: () async {
- if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
- return;
- }
-
- await widget.nodeCreateOrEditViewModel.save(
- saveAsCurrent: widget.isSelected ?? false);
- if (context.mounted) {
- Navigator.of(context).pop();
- }
- },
- text: S.of(context).save,
- color: Theme.of(context).colorScheme.primary,
- textColor: Theme.of(context).colorScheme.onPrimary,
- isDisabled: (!widget.nodeCreateOrEditViewModel.isReady) ||
- (widget.nodeCreateOrEditViewModel.connectionState is IsExecutingState),
- ),
- ),
- ),
- ],
- ),
- ),
+ NewListSections(sections: {"":[
+ if(widget.editingNode != null)
+
+ ListItemRegularRow(keyValue: "share", label: S.of(context).share_this_node, foregroundColor: Theme.of(context).colorScheme.primary, showArrow: false, onTap: (){
+
+ Navigator.of(context).push(CupertinoPageRoute(builder: (context)=>NodeSharePage(uri: widget.editingNode!.uri, currency: walletTypeToCryptoCurrency(widget.type!))));
+
+
+ }),
+ if(!(widget.editingNode == null ||
+ !widget.nodeCreateOrEditViewModel.isReady ||
+ widget.editingNode!.isBuiltin ||
+ (widget.isSelected ?? false)))
+ ListItemRegularRow(keyValue: "delete", label: S.of(context).delete_this_node, foregroundColor: Theme.of(context).colorScheme.errorContainer, showArrow: false, onTap: ()async{
+ final confirmed = await showPopUp<bool>(
+ context: context,
+ builder: (context) => AlertWithTwoActions(
+ alertTitle: S.of(context).remove_node,
+ alertContent: S.of(context).remove_node_message,
+ rightButtonText: S.of(context).remove,
+ leftButtonText: S.of(context).cancel,
+ actionRightButton: () => Navigator.pop(context, true),
+ actionLeftButton: () => Navigator.pop(context, false),
+ ),
+ ) ??
+ false;
+
+ if (confirmed) {
+ await widget.nodeCreateOrEditViewModel.delete(editingNode: widget.editingNode!);
+ Navigator.of(context).pop();
+ }
+
+ })
+ ]})
+
+ ]
),
),
),
diff --git a/lib/src/screens/nodes/node_share_page.dart b/lib/src/screens/nodes/node_share_page.dart
new file mode 100644
index 00000000..1319d842
--- /dev/null
+++ b/lib/src/screens/nodes/node_share_page.dart
@@ -0,0 +1,82 @@
+import 'package:cake_wallet/generated/i18n.dart';
+import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
+import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
+import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:flutter/material.dart';
+
+class NodeSharePage extends StatelessWidget {
+ const NodeSharePage({super.key, required this.uri, required this.currency});
+
+ final Uri uri;
+ final CryptoCurrency currency;
+
+ @override
+ Widget build(BuildContext context) {
+ final qrSize = MediaQuery.of(context).size.width * 0.786;
+ final padding = 16.0;
+ return Container(
+ color: Theme.of(context).colorScheme.surface,
+ child: SafeArea(
+ child: Column(
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ ModalTopBar(
+ title: S.of(context).share,
+ leadingIcon: Icon(Icons.arrow_back_ios_new),
+ onLeadingPressed: Navigator.of(context).pop,
+ ),
+ Expanded(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 40,
+ children: [
+ Stack(alignment: Alignment.center, children: [
+ SizedBox(
+ width: qrSize,
+ height: qrSize,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(18),
+ child: QrImage(
+ size: qrSize - padding * 2,
+ embeddedImagePath: "",
+ data: uri.toString())),
+ ),
+ SizedBox(
+ width: 60,
+ height: 60,
+ child: Stack(
+ children: [
+ CakeImageWidget(
+ imageUrl: "assets/new-ui/settings_row_icons/nodes.svg",
+ width: 54,
+ height: 54,
+ ),
+ Align(
+ alignment: Alignment.bottomRight,
+ child: CakeImageWidget(
+ imageUrl: currency.iconPath,
+ width: 24,
+ height: 24,
+ ))
+ ],
+ ),
+ ),
+ ]),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 36.0),
+ child: Text(S.of(context).node_share_desc,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontWeight: FontWeight.w500,
+ color: Theme.of(context).colorScheme.onSurfaceVariant)),
+ )
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/src/screens/nodes/widgets/node_list_row.dart b/lib/src/screens/nodes/widgets/node_list_row.dart
index 2e43ad3f..5be02748 100644
--- a/lib/src/screens/nodes/widgets/node_list_row.dart
+++ b/lib/src/screens/nodes/widgets/node_list_row.dart
@@ -1,115 +1,55 @@
import 'package:cake_wallet/routes.dart';
-import 'package:cake_wallet/src/screens/nodes/widgets/node_indicator.dart';
-import 'package:cake_wallet/src/widgets/standard_list.dart';
+import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
+import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:cw_core/node.dart';
import 'package:flutter/material.dart';
-class NodeListRow extends StandardListRow {
+class NodeListRow extends StatelessWidget {
NodeListRow(
- {required String title,
- String? subtitle,
- required this.node,
- required void Function(BuildContext context) onTap,
- required bool isSelected,
- required this.isPow})
- : super(title: title, onTap: onTap, isSelected: isSelected, subtitle: subtitle);
+ {required this.node,
+ required this.onTap,
+ required this.onEditComplete,
+ required this.isSelected,
+ required this.isPow, this.speed});
final Node node;
final bool isPow;
+ final VoidCallback onTap;
+ final Future<void> Function(Object?) onEditComplete;
+ final bool isSelected;
+ final NodeSpeed? speed;
@override
Widget build(BuildContext context) {
final leading = buildLeading(context);
final trailing = buildTrailing(context);
- return Container(
- padding: EdgeInsets.only(left: 18, right: 18, top: 2, bottom: 2),
- margin: EdgeInsets.only(top: 2, bottom: 2),
- child: FilledButton(
- onPressed: () => onTap?.call(context),
- style: FilledButton.styleFrom(
- backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.all(Radius.circular(20)),
- ),
- ),
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: <Widget>[
- leading,
- buildCenter(context, hasLeftOffset: true),
- trailing,
- ],
- ),
- ),
- ),
- );
- }
-
- @override
- Widget buildLeading(BuildContext context) {
- return FutureBuilder(
- future: node.requestNode(),
- builder: (context, snapshot) {
- switch (snapshot.connectionState) {
- case ConnectionState.done:
- return NodeIndicator(isLive: snapshot.data ?? false);
- default:
- return NodeIndicator(isLive: false);
- }
- });
- }
- @override
- Widget buildTrailing(BuildContext context) {
+ final hasLabel = node.label != null && node.label!.isNotEmpty;
+ final uriText = "${node.uri.host}:${node.uri.port}";
return GestureDetector(
- onTap: () => Navigator.of(context).pushNamed(
- isPow ? Routes.newPowNode : Routes.newNode,
- arguments: {'editingNode': node, 'isSelected': isSelected},
- ),
- child: Container(
- padding: EdgeInsets.all(10),
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- color: Theme.of(context).colorScheme.surface,
- ),
- child: Icon(
- Icons.edit,
- size: 14,
- color: Theme.of(context).colorScheme.onSurfaceVariant,
- ),
- ),
- );
- }
-}
-
-class NodeHeaderListRow extends StandardListRow {
- NodeHeaderListRow({required String title, required void Function(BuildContext context) onTap})
- : super(title: title, onTap: onTap, isSelected: false);
-
- @override
- Widget build(BuildContext context) {
- final leading = buildLeading(context);
- final trailing = buildTrailing(context);
- return Container(
- height: 56,
- padding: EdgeInsets.only(left: 12, right: 12, top: 2, bottom: 2),
- child: TextButton(
- onPressed: () => onTap?.call(context),
- style: ButtonStyle(
- backgroundColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
- shape: WidgetStateProperty.all(
- RoundedRectangleBorder(
- borderRadius: BorderRadius.all(Radius.circular(10)),
- ),
- ),
- ),
+ behavior: HitTestBehavior.opaque,
+ onTap: onTap,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 12),
child: Row(
+ spacing: 12,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
- if (leading != null) leading,
- buildCenter(context, hasLeftOffset: leading != null),
+ leading,
+ Expanded(
+ child: Column(crossAxisAlignment:CrossAxisAlignment.start,spacing:4,children: [
+ Row(
+ spacing: 4,
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ Text(hasLabel ? node.label! : uriText, style: TextStyle(fontSize: 12),),
+ if(badgePath != null)
+ CakeImageWidget(imageUrl: badgePath, colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary,BlendMode.srcIn),)
+ ],
+ ),
+ if(hasLabel) Text(uriText, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),),
+ ],),
+ ),
trailing,
],
),
@@ -117,12 +57,44 @@ class NodeHeaderListRow extends StandardListRow {
);
}
- @override
+ Widget buildLeading(BuildContext context) => isSelected
+ ? Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: Icon(
+ Icons.check,
+ size: 16,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ )
+ : SizedBox(width: 24, height: 24);
+
Widget buildTrailing(BuildContext context) {
- return SizedBox(
- width: 20,
- child: Icon(Icons.add,
- color: Theme.of(context).colorScheme.onSurfaceVariant, size: 24.0),
+ return Row(
+ children: [
+ if (speed != null) CakeImageWidget(imageUrl: speed!.iconPath, width: 24, height: 24),
+ GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: () async {
+ final res = await Navigator.of(context).pushNamed(isPow ? Routes.newPowNode : Routes.newNode,
+ arguments: {'editingNode': node, 'isSelected': isSelected});
+ await onEditComplete(res);
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: CakeImageWidget(
+ imageUrl: "assets/new-ui/3dots_vertical.svg",
+ colorFilter:
+ ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
+ ),
+ ),
+ )
+ ],
);
}
+
+ String? get badgePath {
+ if(node.isOfficial) return "assets/new-ui/official_node.svg";
+ if(node.isBuiltin) return "assets/new-ui/default_node.svg";
+ return null;
+ }
}
diff --git a/lib/src/screens/settings/manage_nodes_page.dart b/lib/src/screens/settings/manage_nodes_page.dart
index 02c2e4e6..9f69c658 100644
--- a/lib/src/screens/settings/manage_nodes_page.dart
+++ b/lib/src/screens/settings/manage_nodes_page.dart
@@ -1,42 +1,42 @@
import 'package:cake_wallet/generated/i18n.dart';
+import 'package:cake_wallet/new-ui/widgets/modal_header.dart';
import 'package:cake_wallet/new-ui/widgets/modal_page_wrapper.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
-import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart';
-import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
-import 'package:cake_wallet/src/widgets/standard_list.dart';
-import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
-import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
+import 'package:cw_core/node.dart';
+import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
-class ManageNodesPage extends BasePage {
- ManageNodesPage(this.isPow, {this.nodeListViewModel, this.powNodeListViewModel, this.dashboardViewModel})
- : assert((isPow && powNodeListViewModel != null) || (!isPow && nodeListViewModel != null)),
- assert(powNodeListViewModel == null || nodeListViewModel == null);
+class ManageNodesPage extends StatefulWidget {
+ ManageNodesPage(this.isPow, {required this.nodeListViewModel, this.dashboardViewModel});
final DashboardViewModel? dashboardViewModel;
- final NodeListViewModel? nodeListViewModel;
- final PowNodeListViewModel? powNodeListViewModel;
+ final NodeListViewModel nodeListViewModel;
final bool isPow;
@override
- bool get hideAppBar => true;
+ State<ManageNodesPage> createState() => _ManageNodesPageState();
+}
+class _ManageNodesPageState extends State<ManageNodesPage> {
@override
- String get title => S.current.manage_nodes;
+ void initState() {
+ super.initState();
+ Future.delayed(Duration(milliseconds: 300))
+ .then((_) => widget.nodeListViewModel.speedTestNodes());
+ }
@override
- Widget body(BuildContext context) {
+ Widget build(BuildContext context) {
return ModalPageWrapper(
- horizontalPadding: 0,
topBar: ModalTopBar(
title: S.of(context).manage_nodes,
leadingIcon: Icon(Icons.arrow_back_ios_new),
@@ -44,87 +44,96 @@ class ManageNodesPage extends BasePage {
trailingWidget: Row(
spacing: 8,
children: [
- if (dashboardViewModel?.hasRescan ?? true)
- ModernButton(
+ Observer(
+ builder: (_) => ModernButton(
size: 36,
- icon: Icon(Icons.history),
- onPressed: () => Navigator.of(context).pushNamed(Routes.rescan)),
+ icon: widget.nodeListViewModel.isTestingNodeSpeed
+ ? CupertinoActivityIndicator()
+ : Icon(Icons.refresh),
+ onPressed: () => widget.nodeListViewModel.speedTestNodes()),
+ ),
ModernButton(
size: 36,
icon: Icon(Icons.add),
- onPressed: () => Navigator.of(context).pushNamed(isPow ? Routes.newPowNode : Routes.newNode))
+ onPressed: ()async {
+ final res = await Navigator.of(context)
+ .pushNamed(widget.isPow ? Routes.newPowNode : Routes.newNode);
+ if(res != null && res is Node) {
+ widget.nodeListViewModel.nodes.add(res);
+ }
+ })
],
),
),
- content: Column(
- children: [
- if (FeatureFlag.isAutomaticNodeSwitchingEnabled)
- Observer(
- builder: (_) => SettingsSwitcherCell(
- key: ValueKey('manage_nodes_page_enable_auto_node_switching_button_key'),
- title: S.current.enable_auto_node_switching,
- value: isPow
- ? powNodeListViewModel!.enableAutomaticNodeSwitching
- : nodeListViewModel!.enableAutomaticNodeSwitching,
- onValueChange: (BuildContext context, bool value) {
- if (isPow) {
- powNodeListViewModel!.setEnableAutomaticNodeSwitching(value);
- } else {
- nodeListViewModel!.setEnableAutomaticNodeSwitching(value);
+ header: ModalHeader(
+ iconPath: "assets/new-ui/settings_row_icons/nodes.svg",
+ message: S.of(context).nodes_desc,
+ title: S.of(context).nodes,
+ bottomWidget: Observer(
+ builder: (_) => NodeListRow(
+ node: widget.nodeListViewModel.currentNode,
+ speed: widget.nodeListViewModel.nodeSpeedFor(widget.nodeListViewModel.currentNode),
+ onEditComplete: (res)async{
+ if(res != null && res is Node) {
+ widget.nodeListViewModel.nodes.removeWhere((item)=>item.id == res.id);
+ widget.nodeListViewModel.nodes.add(res);
}
+
},
- ),
- ),
- if (FeatureFlag.isAutomaticNodeSwitchingEnabled) SizedBox(height: 8),
- Observer(
- builder: (BuildContext context) {
- int itemsCount =
- nodeListViewModel?.nodes.length ?? powNodeListViewModel!.nodes.length;
- return SectionStandardList(
- scrollController: ModalScrollController.of(context),
- sectionCount: 1,
- shrinkWrap: true,
- physics: NeverScrollableScrollPhysics(),
- dividerPadding: EdgeInsets.symmetric(horizontal: 24),
- itemCounter: (int sectionIndex) => itemsCount,
- itemBuilder: (_, index) {
- return Observer(
- builder: (context) {
- final node =
- nodeListViewModel?.nodes[index] ?? powNodeListViewModel!.nodes[index];
- late bool isSelected;
- if (isPow) {
- isSelected = node.id == powNodeListViewModel!.currentNode.id;
- } else {
- isSelected = node.id == nodeListViewModel!.currentNode.id;
- }
- final nodeListRow = NodeListRow(
- title: node.uriRaw,
- subtitle: node.label ?? '',
- node: node,
- isSelected: isSelected,
- isPow: isPow,
- onTap: (_) async {
- if (isSelected) {
- return;
- }
+ onTap: () {},
+ isSelected: true,
+ isPow: widget.isPow)),
+ ),
+ content: Column(
+ children: [
+ Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainer,
+ borderRadius: BorderRadius.circular(18)),
+ child: Observer(
+ builder: (BuildContext context) {
+ int itemsCount = widget.nodeListViewModel.nonCurrentNodes.length;
+ return ListView.separated(
+ controller: ModalScrollController.of(context),
+ shrinkWrap: true,
+ physics: BouncingScrollPhysics(),
+ itemCount: itemsCount,
+ separatorBuilder: (context, index) => Padding(
+ padding: EdgeInsets.symmetric(horizontal: 12),
+ child: Container(
+ height: 1,
+ color: Theme.of(context).colorScheme.surfaceContainerHigh,
+ ),
+ ),
+ itemBuilder: (_, index) {
+ return Observer(
+ builder: (context) {
+ final node = widget.nodeListViewModel.nonCurrentNodes[index];
+ final nodeListRow = NodeListRow(
+ node: node,
+ isSelected: false,
+ isPow: widget.isPow,
+ speed: widget.nodeListViewModel.nodeSpeedFor(node),
+ onEditComplete: (res) async {
+ if(res != null && res is Node) {
+ widget.nodeListViewModel.nodes.removeWhere((item)=>item.id == res.id);
+ widget.nodeListViewModel.nodes.add(res);
+ }
+ },
+ onTap: () async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).change_current_node_title,
- alertContent: nodeListViewModel?.getAlertContent(node.uriRaw) ??
- powNodeListViewModel!.getAlertContent(node.uriRaw),
+ alertContent:
+ widget.nodeListViewModel.getAlertContent(node.uriRaw),
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).change,
actionLeftButton: () => Navigator.of(context).pop(),
actionRightButton: () async {
- if (isPow) {
- await powNodeListViewModel!.setAsCurrent(node);
- } else {
- await nodeListViewModel!.setAsCurrent(node);
- }
+ await widget.nodeListViewModel.setAsCurrent(node);
Navigator.of(context).pop();
},
);
@@ -132,32 +141,16 @@ class ManageNodesPage extends BasePage {
);
},
);
- return nodeListRow;
- },
- );
- },
- );
- },
+ return nodeListRow;
+ },
+ );
+ },
+ );
+ },
+ ),
),
],
),
);
}
- Future<void> _presentReconnectAlert(BuildContext context) async {
- await showPopUp<void>(
- context: context,
- builder: (BuildContext context) {
- return AlertWithTwoActions(
- alertTitle: S.of(context).reconnection,
- alertContent: S.of(context).reconnect_alert_text,
- rightButtonText: S.of(context).ok,
- leftButtonText: S.of(context).cancel,
- actionRightButton: () async {
- Navigator.of(context).pop();
- await dashboardViewModel!.reconnect();
- },
- actionLeftButton: () => Navigator.of(context).pop());
- },
- );
- }
}
diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart
index 774136ac..bef64ef5 100644
--- a/lib/view_model/node_list/node_create_or_edit_view_model.dart
+++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart
@@ -3,10 +3,7 @@ import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
-import 'package:collection/collection.dart';
import 'package:cw_core/node.dart';
-import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
-import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
@@ -20,7 +17,7 @@ class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase
abstract class NodeCreateOrEditViewModelBase with Store {
NodeCreateOrEditViewModelBase(
- this.isPow, this.nodeListViewModel,this.powNodeListViewModel, this.walletType, this._settingsStore, {this.editingNode})
+ this.isPow, this.walletType, this._settingsStore, {this.editingNode})
: state = InitialExecutionState(),
connectionState = InitialExecutionState(),
label = editingNode?.label ?? '',
@@ -116,8 +113,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
@observable
bool isPow;
- final NodeListViewModel? nodeListViewModel;
- final PowNodeListViewModel? powNodeListViewModel;
@computed
bool get isReady =>
@@ -257,12 +252,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
@action
Future<void> delete({required Node editingNode}) async {
await editingNode.delete();
- if(nodeListViewModel != null) {
- nodeListViewModel!.bindNodes();
- }
- if(powNodeListViewModel != null) {
- powNodeListViewModel!.bindNodes();
- }
}
@action
@@ -287,12 +276,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
}
state = ExecutedSuccessfullyState();
- if(nodeListViewModel != null) {
- nodeListViewModel!.bindNodes();
- }
- if(powNodeListViewModel != null) {
- powNodeListViewModel!.bindNodes();
- }
} catch (e) {
state = FailureState(e.toString());
diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart
index 55207f0d..cc91577a 100644
--- a/lib/view_model/node_list/node_list_view_model.dart
+++ b/lib/view_model/node_list/node_list_view_model.dart
@@ -1,12 +1,14 @@
+import 'dart:math';
+
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/node_list.dart';
+import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/node.dart';
-import 'package:cake_wallet/entities/node_list.dart';
import 'package:cake_wallet/entities/default_settings_migration.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/evm/evm.dart';
@@ -14,11 +16,34 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
part 'node_list_view_model.g.dart';
+
+class NodeSpeed {
+ final String iconPath;
+
+ // fraction of best node ping needed to be classified as this speed.
+ final double treshold;
+
+ const NodeSpeed._(this.iconPath, this.treshold);
+
+ static const disconnected = NodeSpeed._(
+ "assets/new-ui/node_speed_badges/disconnected.svg", 0);
+ static const slow = NodeSpeed._(
+ "assets/new-ui/node_speed_badges/slow.svg", 0);
+ static const medium = NodeSpeed._(
+ "assets/new-ui/node_speed_badges/medium.svg", 0.5);
+ static const fast = NodeSpeed._(
+ "assets/new-ui/node_speed_badges/fast.svg", 0.8);
+
+ static const all = [fast, medium, slow, disconnected];
+}
+
+
class NodeListViewModel = NodeListViewModelBase with _$NodeListViewModel;
abstract class NodeListViewModelBase with Store {
- NodeListViewModelBase(this._appStore)
+ NodeListViewModelBase(this._appStore, this.isPow)
: nodes = ObservableList<Node>(),
+ _nodeSpeeds = ObservableMap<String, int>(),
settingsStore = _appStore.settingsStore {
bindNodes();
@@ -37,6 +62,7 @@ abstract class NodeListViewModelBase with Store {
bindNodes();
});
}
+
@computed
Node get currentNode {
@@ -52,7 +78,7 @@ abstract class NodeListViewModelBase with Store {
return settingsStore.getCurrentNode(walletType, chainId: chainId);
}
- final node = settingsStore.nodes[walletType];
+ final node = isPow ? settingsStore.powNodes[walletType] : settingsStore.nodes[walletType];
if (node == null) {
throw Exception('No node for wallet type: $walletType');
}
@@ -73,6 +99,59 @@ abstract class NodeListViewModelBase with Store {
final ObservableList<Node> nodes;
final SettingsStore settingsStore;
final AppStore _appStore;
+ final bool isPow;
+ final ObservableMap<String, int> _nodeSpeeds;
+
+
+ @computed
+ List<Node> get nonCurrentNodes => nodes.where((item)=>item != currentNode).toList();
+
+ @observable
+ bool isTestingNodeSpeed = false;
+
+ Future<void> speedTestNodes() async {
+ try {
+ isTestingNodeSpeed = true;
+ _nodeSpeeds.clear();
+ final nodes = this.nodes.toList();
+ await Future.wait(nodes.map((node) async {
+ final sw = Stopwatch()..start();
+ final res = await node
+ .requestNode()
+ .timeout(const Duration(seconds: 10), onTimeout: () => false);
+ sw.stop();
+ if (res) {
+ _nodeSpeeds[node.uriRaw] = sw.elapsedMilliseconds;
+ }
+ }));
+ } finally {
+ isTestingNodeSpeed = false;
+ }
+ }
+
+ NodeSpeed? nodeSpeedFor(Node node) {
+ final bestSpeed = _bestNodeSpeed;
+ final currentSpeed = _nodeSpeeds[node.uriRaw];
+ if (bestSpeed == null) {
+ return null;
+ }
+ if (currentSpeed == null) {
+ return NodeSpeed.disconnected;
+ }
+ for (final speed in NodeSpeed.all) {
+ if (currentSpeed < (bestSpeed / speed.treshold)) return speed;
+ }
+
+ return null;
+ }
+
+ int? get _bestNodeSpeed {
+ try {
+ printV(_nodeSpeeds);
+ return _nodeSpeeds.values.reduce(min);
+ } catch (e) {}
+ return null;
+ }
Future<void> reset() async {
await resetToDefault();
@@ -125,7 +204,12 @@ abstract class NodeListViewModelBase with Store {
}
// For non-EVM wallets, use the wallet type directly
- settingsStore.nodes[walletType] = node;
+ if(isPow) {
+ settingsStore.powNodes[walletType] = node;
+ } else {
+ settingsStore.nodes[walletType] = node;
+ }
+
}
@action
@@ -150,7 +234,7 @@ abstract class NodeListViewModelBase with Store {
}
// For non-EVM wallets, use the wallet type directly
- nodes.addAll(await Node.getAllForWalletType(walletType));
+ nodes.addAll(isPow ? await Node.getAllForWalletTypePow(walletType) : await Node.getAllForWalletType(walletType));
}
}
diff --git a/lib/view_model/node_list/pow_node_list_view_model.dart b/lib/view_model/node_list/pow_node_list_view_model.dart
index b7e185e2..e69de29b 100644
--- a/lib/view_model/node_list/pow_node_list_view_model.dart
+++ b/lib/view_model/node_list/pow_node_list_view_model.dart
@@ -1,83 +0,0 @@
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/store/app_store.dart';
-import 'package:cake_wallet/utils/mobx.dart';
-import 'package:cw_core/node_list.dart';
-import 'package:cw_core/utils/proxy_wrapper.dart';
-import 'package:hive/hive.dart';
-import 'package:mobx/mobx.dart';
-import 'package:cw_core/wallet_base.dart';
-import 'package:cake_wallet/store/settings_store.dart';
-import 'package:cw_core/node.dart';
-import 'package:cake_wallet/entities/node_list.dart';
-import 'package:cake_wallet/entities/default_settings_migration.dart';
-import 'package:cw_core/wallet_type.dart';
-
-part 'pow_node_list_view_model.g.dart';
-
-class PowNodeListViewModel = PowNodeListViewModelBase with _$PowNodeListViewModel;
-
-abstract class PowNodeListViewModelBase with Store {
- PowNodeListViewModelBase(this._appStore)
- : nodes = ObservableList<Node>(),
- settingsStore = _appStore.settingsStore {
- bindNodes();
-
- reaction((_) => _appStore.wallet, (WalletBase? _wallet) {
- bindNodes();
- });
- }
-
- @computed
- Node get currentNode {
- final node = settingsStore.powNodes[_appStore.wallet!.type];
-
- if (node == null) {
- throw Exception('No node for wallet type: ${_appStore.wallet!.type}');
- }
-
- return node;
- }
-
- String getAlertContent(String uri) =>
- S.current.change_current_node(uri) +
- '${uri.endsWith('.onion') || uri.contains('.onion:') ? '\n' + (CakeTor.instance!.enabled ? '' : S.current.orbot_running_alert) : ''}';
-
- @computed
- bool get enableAutomaticNodeSwitching => settingsStore.enableAutomaticNodeSwitching;
-
- @action
- void setEnableAutomaticNodeSwitching(bool value) =>
- settingsStore.enableAutomaticNodeSwitching = value;
-
- final ObservableList<Node> nodes;
- final SettingsStore settingsStore;
- final AppStore _appStore;
-
- Future<void> reset() async {
- await resetPowToDefault();
-
- Node node;
-
- switch (_appStore.wallet!.type) {
- case WalletType.nano:
- node = (await getNanoDefaultPowNode())!;
- break;
- default:
- throw Exception('Unexpected wallet type: ${_appStore.wallet!.type} for pow node list');
- }
-
- await setAsCurrent(node);
- }
-
- @action
- Future<void> delete(Node node) async => node.delete();
-
- Future<void> setAsCurrent(Node node) async =>
- settingsStore.powNodes[_appStore.wallet!.type] = node;
-
- @action
- Future<void> bindNodes() async {
- nodes.clear();
- nodes.addAll(await Node.getAllPow());
- }
-}
diff --git a/pubspec_base.yaml b/pubspec_base.yaml
index 416c6dea..a04b093d 100644
--- a/pubspec_base.yaml
+++ b/pubspec_base.yaml
@@ -290,6 +290,7 @@ flutter:
- assets/new-ui/card_icons/outline_icons/
- assets/new-ui/card_icons/symbol_icons/
- assets/new-ui/crypto_full_icons/
+ - assets/new-ui/node_speed_badges/
fonts:
- family: Lato
diff --git a/res/pictures/default_node.svg b/res/pictures/default_node.svg
new file mode 100644
index 00000000..43d730b9
--- /dev/null
+++ b/res/pictures/default_node.svg
@@ -0,0 +1,3 @@
+<svg width="8" height="10" viewBox="0 0 8 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M3.475 5.275L2.775 4.575C2.675 4.475 2.55833 4.425 2.425 4.425C2.29167 4.425 2.175 4.475 2.075 4.575C1.975 4.675 1.925 4.79375 1.925 4.93125C1.925 5.06875 1.975 5.1875 2.075 5.2875L3.125 6.35C3.225 6.45 3.34167 6.5 3.475 6.5C3.60833 6.5 3.725 6.45 3.825 6.35L5.95 4.225C6.05 4.125 6.1 4.00625 6.1 3.86875C6.1 3.73125 6.05 3.6125 5.95 3.5125C5.85 3.4125 5.73125 3.3625 5.59375 3.3625C5.45625 3.3625 5.3375 3.4125 5.2375 3.5125L3.475 5.275ZM3.8375 9.8625C3.7875 9.85417 3.7375 9.84167 3.6875 9.825C2.5625 9.45 1.66667 8.75625 1 7.74375C0.333333 6.73125 0 5.64167 0 4.475V2.1125C0 1.90417 0.0604167 1.71667 0.18125 1.55C0.302083 1.38333 0.458333 1.2625 0.65 1.1875L3.65 0.0625C3.76667 0.0208333 3.88333 0 4 0C4.11667 0 4.23333 0.0208333 4.35 0.0625L7.35 1.1875C7.54167 1.2625 7.69792 1.38333 7.81875 1.55C7.93958 1.71667 8 1.90417 8 2.1125V4.475C8 5.64167 7.66667 6.73125 7 7.74375C6.33333 8.75625 5.4375 9.45 4.3125 9.825C4.2625 9.84167 4.2125 9.85417 4.1625 9.8625C4.1125 9.87083 4.05833 9.875 4 9.875C3.94167 9.875 3.8875 9.87083 3.8375 9.8625Z" fill="white"/>
+</svg>
diff --git a/res/pictures/node_speed_badges/disconnected.svg b/res/pictures/node_speed_badges/disconnected.svg
new file mode 100644
index 00000000..33b7d28b
--- /dev/null
+++ b/res/pictures/node_speed_badges/disconnected.svg
@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M5.4375 19.5625C5.14583 19.2708 5 18.9167 5 18.5V15.5C5 15.0833 5.14583 14.7292 5.4375 14.4375C5.72917 14.1458 6.08333 14 6.5 14C6.91667 14 7.27083 14.1458 7.5625 14.4375C7.85417 14.7292 8 15.0833 8 15.5V18.5C8 18.9167 7.85417 19.2708 7.5625 19.5625C7.27083 19.8542 6.91667 20 6.5 20C6.08333 20 5.72917 19.8542 5.4375 19.5625ZM11.4375 19.5625C11.1458 19.2708 11 18.9167 11 18.5V10.5C11 10.0833 11.1458 9.72917 11.4375 9.4375C11.7292 9.14583 12.0833 9 12.5 9C12.9167 9 13.2708 9.14583 13.5625 9.4375C13.8542 9.72917 14 10.0833 14 10.5V18.5C14 18.9167 13.8542 19.2708 13.5625 19.5625C13.2708 19.8542 12.9167 20 12.5 20C12.0833 20 11.7292 19.8542 11.4375 19.5625ZM17.4375 19.5625C17.1458 19.2708 17 18.9167 17 18.5V5.5C17 5.08333 17.1458 4.72917 17.4375 4.4375C17.7292 4.14583 18.0833 4 18.5 4C18.9167 4 19.2708 4.14583 19.5625 4.4375C19.8542 4.72917 20 5.08333 20 5.5V18.5C20 18.9167 19.8542 19.2708 19.5625 19.5625C19.2708 19.8542 18.9167 20 18.5 20C18.0833 20 17.7292 19.8542 17.4375 19.5625Z" fill="#FFB4AB" fill-opacity="0.5"/>
+ <path d="M16.4916 18.4367L14.6743 20.2178C14.4363 20.4552 14.1388 20.5794 13.7818 20.5902C13.4249 20.601 13.1166 20.4768 12.857 20.2178C12.619 19.9803 12.5 19.678 12.5 19.311C12.5 18.944 12.619 18.6418 12.857 18.4043L14.6743 16.5908L12.857 14.7773C12.619 14.5399 12.5 14.2376 12.5 13.8706C12.5 13.5036 12.619 13.2014 12.857 12.9639C13.0949 12.7264 13.3978 12.6077 13.7656 12.6077C14.1334 12.6077 14.4363 12.7264 14.6743 12.9639L16.4916 14.7773L18.3089 12.9639C18.5468 12.7264 18.8443 12.6023 19.2013 12.5915C19.5583 12.5807 19.8665 12.7048 20.1262 12.9639C20.3641 13.2014 20.4831 13.5036 20.4831 13.8706C20.4831 14.2376 20.3641 14.5399 20.1262 14.7773L18.3413 16.5908L20.1262 18.4043C20.3641 18.6418 20.4885 18.9386 20.4994 19.2948C20.5102 19.6511 20.3858 19.9587 20.1262 20.2178C19.8882 20.4552 19.5853 20.574 19.2175 20.574C18.8497 20.574 18.5468 20.4552 18.3089 20.2178L16.4916 18.4367Z" fill="white"/>
+</svg>
diff --git a/res/pictures/node_speed_badges/fast.svg b/res/pictures/node_speed_badges/fast.svg
new file mode 100644
index 00000000..4205def6
--- /dev/null
+++ b/res/pictures/node_speed_badges/fast.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M5.4375 19.5625C5.14583 19.2708 5 18.9167 5 18.5V15.5C5 15.0833 5.14583 14.7292 5.4375 14.4375C5.72917 14.1458 6.08333 14 6.5 14C6.91667 14 7.27083 14.1458 7.5625 14.4375C7.85417 14.7292 8 15.0833 8 15.5V18.5C8 18.9167 7.85417 19.2708 7.5625 19.5625C7.27083 19.8542 6.91667 20 6.5 20C6.08333 20 5.72917 19.8542 5.4375 19.5625ZM11.4375 19.5625C11.1458 19.2708 11 18.9167 11 18.5V10.5C11 10.0833 11.1458 9.72917 11.4375 9.4375C11.7292 9.14583 12.0833 9 12.5 9C12.9167 9 13.2708 9.14583 13.5625 9.4375C13.8542 9.72917 14 10.0833 14 10.5V18.5C14 18.9167 13.8542 19.2708 13.5625 19.5625C13.2708 19.8542 12.9167 20 12.5 20C12.0833 20 11.7292 19.8542 11.4375 19.5625ZM17.4375 19.5625C17.1458 19.2708 17 18.9167 17 18.5V5.5C17 5.08333 17.1458 4.72917 17.4375 4.4375C17.7292 4.14583 18.0833 4 18.5 4C18.9167 4 19.2708 4.14583 19.5625 4.4375C19.8542 4.72917 20 5.08333 20 5.5V18.5C20 18.9167 19.8542 19.2708 19.5625 19.5625C19.2708 19.8542 18.9167 20 18.5 20C18.0833 20 17.7292 19.8542 17.4375 19.5625Z" fill="#4DA865"/>
+</svg>
diff --git a/res/pictures/node_speed_badges/medium.svg b/res/pictures/node_speed_badges/medium.svg
new file mode 100644
index 00000000..39828025
--- /dev/null
+++ b/res/pictures/node_speed_badges/medium.svg
@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M5.4375 19.5625C5.14583 19.2708 5 18.9167 5 18.5V15.5C5 15.0833 5.14583 14.7292 5.4375 14.4375C5.72917 14.1458 6.08333 14 6.5 14C6.91667 14 7.27083 14.1458 7.5625 14.4375C7.85417 14.7292 8 15.0833 8 15.5V18.5C8 18.9167 7.85417 19.2708 7.5625 19.5625C7.27083 19.8542 6.91667 20 6.5 20C6.08333 20 5.72917 19.8542 5.4375 19.5625ZM11.4375 19.5625C11.1458 19.2708 11 18.9167 11 18.5V10.5C11 10.0833 11.1458 9.72917 11.4375 9.4375C11.7292 9.14583 12.0833 9 12.5 9C12.9167 9 13.2708 9.14583 13.5625 9.4375C13.8542 9.72917 14 10.0833 14 10.5V18.5C14 18.9167 13.8542 19.2708 13.5625 19.5625C13.2708 19.8542 12.9167 20 12.5 20C12.0833 20 11.7292 19.8542 11.4375 19.5625Z" fill="#FAC06A"/>
+ <path d="M17 18.5C17 18.9167 17.1458 19.2708 17.4375 19.5625C17.7292 19.8542 18.0833 20 18.5 20C18.9167 20 19.2708 19.8542 19.5625 19.5625C19.8542 19.2708 20 18.9167 20 18.5V5.5C20 5.08333 19.8542 4.72917 19.5625 4.4375C19.2708 4.14583 18.9167 4 18.5 4C18.0833 4 17.7292 4.14583 17.4375 4.4375C17.1458 4.72917 17 5.08333 17 5.5V18.5Z" fill="#FAC06A" fill-opacity="0.5"/>
+</svg>
diff --git a/res/pictures/node_speed_badges/slow.svg b/res/pictures/node_speed_badges/slow.svg
new file mode 100644
index 00000000..fc4d0b20
--- /dev/null
+++ b/res/pictures/node_speed_badges/slow.svg
@@ -0,0 +1,7 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M5.4375 19.5625C5.14583 19.2708 5 18.9167 5 18.5V15.5C5 15.0833 5.14583 14.7292 5.4375 14.4375C5.72917 14.1458 6.08333 14 6.5 14C6.91667 14 7.27083 14.1458 7.5625 14.4375C7.85417 14.7292 8 15.0833 8 15.5V18.5C8 18.9167 7.85417 19.2708 7.5625 19.5625C7.27083 19.8542 6.91667 20 6.5 20C6.08333 20 5.72917 19.8542 5.4375 19.5625Z" fill="#FFB4AB"/>
+ <g opacity="0.5">
+ <path d="M11 18.5C11 18.9167 11.1458 19.2708 11.4375 19.5625C11.7292 19.8542 12.0833 20 12.5 20C12.9167 20 13.2708 19.8542 13.5625 19.5625C13.8542 19.2708 14 18.9167 14 18.5V10.5C14 10.0833 13.8542 9.72917 13.5625 9.4375C13.2708 9.14583 12.9167 9 12.5 9C12.0833 9 11.7292 9.14583 11.4375 9.4375C11.1458 9.72917 11 10.0833 11 10.5V18.5Z" fill="#FFB4AB"/>
+ <path d="M17 18.5C17 18.9167 17.1458 19.2708 17.4375 19.5625C17.7292 19.8542 18.0833 20 18.5 20C18.9167 20 19.2708 19.8542 19.5625 19.5625C19.8542 19.2708 20 18.9167 20 18.5V5.5C20 5.08333 19.8542 4.72917 19.5625 4.4375C19.2708 4.14583 18.9167 4 18.5 4C18.0833 4 17.7292 4.14583 17.4375 4.4375C17.1458 4.72917 17 5.08333 17 5.5V18.5Z" fill="#FFB4AB"/>
+ </g>
+</svg>
diff --git a/res/pictures/official_node.svg b/res/pictures/official_node.svg
new file mode 100644
index 00000000..75a09a0b
--- /dev/null
+++ b/res/pictures/official_node.svg
@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M5.21354 5.92351L4.42812 5.15164C4.32882 5.05233 4.20469 5.00268 4.05573 5.00268C3.90677 5.00268 3.77813 5.05685 3.66979 5.16518C3.57049 5.26448 3.52083 5.39087 3.52083 5.54434C3.52083 5.69782 3.57049 5.82421 3.66979 5.92351L4.83437 7.0881C4.94271 7.19643 5.0691 7.2506 5.21354 7.2506C5.35799 7.2506 5.48438 7.19643 5.59271 7.0881L7.89479 4.78601C8.00313 4.67768 8.05503 4.55129 8.05052 4.40684C8.04601 4.2624 7.9941 4.13601 7.89479 4.02768C7.78646 3.91934 7.65781 3.86292 7.50885 3.85841C7.3599 3.85389 7.23125 3.9058 7.12292 4.01414L5.21354 5.92351ZM3.69688 10.8256L2.91146 9.49851L1.42188 9.17351C1.28646 9.14643 1.17812 9.07646 1.09687 8.96362C1.01562 8.85077 0.984028 8.72664 1.00208 8.59122L1.15104 7.06101L0.135417 5.89643C0.0451389 5.79712 0 5.67976 0 5.54434C0 5.40893 0.0451389 5.29157 0.135417 5.19226L1.15104 4.02768L1.00208 2.49747C0.984028 2.36205 1.01562 2.23792 1.09687 2.12507C1.17812 2.01223 1.28646 1.94226 1.42188 1.91518L2.91146 1.59018L3.69688 0.263095C3.7691 0.145734 3.8684 0.0667411 3.99479 0.0261161C4.12118 -0.0145089 4.24757 -0.00773809 4.37396 0.0464286L5.78229 0.642262L7.19063 0.0464286C7.31701 -0.00773809 7.4434 -0.0145089 7.56979 0.0261161C7.69618 0.0667411 7.79549 0.145734 7.86771 0.263095L8.65312 1.59018L10.1427 1.91518C10.2781 1.94226 10.3865 2.01223 10.4677 2.12507C10.549 2.23792 10.5806 2.36205 10.5625 2.49747L10.4135 4.02768L11.4292 5.19226C11.5194 5.29157 11.5646 5.40893 11.5646 5.54434C11.5646 5.67976 11.5194 5.79712 11.4292 5.89643L10.4135 7.06101L10.5625 8.59122C10.5806 8.72664 10.549 8.85077 10.4677 8.96362C10.3865 9.07646 10.2781 9.14643 10.1427 9.17351L8.65312 9.49851L7.86771 10.8256C7.79549 10.943 7.69618 11.0219 7.56979 11.0626C7.4434 11.1032 7.31701 11.0964 7.19063 11.0423L5.78229 10.4464L4.37396 11.0423C4.24757 11.0964 4.12118 11.1032 3.99479 11.0626C3.8684 11.0219 3.7691 10.943 3.69688 10.8256Z" fill="white"/>
+</svg>
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
index 70468bc3..f8828872 100644
--- a/res/values/strings_en.arb
+++ b/res/values/strings_en.arb
@@ -300,6 +300,7 @@
"default_sell_provider": "Default Sell Provider",
"delete": "Delete",
"delete_account": "Delete Account",
+ "delete_this_node": "Delete this node",
"delete_wallet": "Delete wallet",
"delete_wallet_confirm_message": "Are you sure that you want to delete ${wallet_name} wallet?",
"deleteConnectionConfirmationPrompt": "Are you sure that you want to delete the connection to",
@@ -681,8 +682,10 @@
"node_new": "New Node",
"node_port": "Node port",
"node_reset_settings_title": "Reset settings",
+ "node_share_desc": "Scan this QR code from the Scan screen to import this node.",
"node_test": "Test",
"nodes": "Nodes",
+ "nodes_desc": "Manage existing nodes or add a custom one.",
"nodes_list_reset_to_default_message": "Are you sure that you want to reset settings to default?",
"none_of_selected_providers_can_exchange": "None of the selected providers can make this swap",
"noNFTYet": "No NFTs yet",
@@ -1022,6 +1025,7 @@
"share_address": "Share address",
"shared_seed_wallet_groups": "Shared Seed Wallet Groups",
"shared_seed_wallets": "Shared Seed Wallets",
+ "share_this_node": "Share this node",
"show": "Show",
"show_address_book_popup": "Show Address Book popup",
"show_balance": "Long Press to Show Balance",
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.