Files
hash-wallet/lib/src/screens/nodes/widgets/node_form.dart
T
David AdegokeandGitHub 4b2af63fa5 CW-974: Add support for automatic node switching. (#2361)
* feat: Add support for automatic node switching.

This change:
- Adds isTrustedNodeForSwitching property to Node class.
- Adds UI Checkbox to mark a node as trusted for automatic switching in Node create or edit form.
- Implements checkNodeHealth method in wallet classes to verify node connectivity.
- Adds NodeSwitchingService for managing automatic node switching based on health checks.
- Adds setting option for user to enable/disable automated node switching

* fix(automatic-node-switch): Set node health check interval to 5 minutes in NodeSwitchingService

* refactor: Update handler method

* feat: Automatic Node Switching Improvements

This change:
- Moves enable automatic node switch to the Manage Nodes Page
- Adds check to verify node health before attempting to sync in EVM, Sol and Tron Wallets.
- Modifies trusted ndoes list for monero
- Adds trigger for Node Health Check when app resumes from background

* fix: Increase default request timeout for Tron and update health check interval for node switch

* feat: Enhance Node health check functionality.

This change adds additional checks for token balance in EVM, Solana and Tron Wallets in checkNodeHealth.

* fix: Update health check interval to 30 seconds in NodeSwitchingService

* Update bitcoin_electrum_server_list.yml

* fix: Electru wallet not connecting to new node after switching

* fix: Automated Node Switch not working on Decred Wallet

* fix: Add more nodes for AutoSwitching

This change:
- Enables more nodes across wallet types for auto switching
- Modifies CTA for checkbox
- Renames flags to fit

* fix: Add migration function for setting up default nodes for autoswitching

* fix: Review and testing fixes

* chore: Adjust balance and history fetch timer
2025-07-25 16:42:59 +03:00

280 lines
11 KiB
Dart

import 'package:cake_wallet/core/node_address_validator.dart';
import 'package:cake_wallet/core/node_port_validator.dart';
import 'package:cake_wallet/core/socks_proxy_node_address_validator.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:mobx/mobx.dart';
class NodeForm extends StatelessWidget {
NodeForm({
required this.nodeViewModel,
required this.formKey,
this.editingNode,
this.type,
}) : _addressController = TextEditingController(text: editingNode?.uri.host.toString()),
_pathController = TextEditingController(text: editingNode?.path.toString()),
_portController = TextEditingController(text: editingNode?.uri.port.toString()),
_loginController = TextEditingController(text: editingNode?.login),
_passwordController = TextEditingController(text: editingNode?.password),
_socksAddressController = TextEditingController(text: editingNode?.socksProxyAddress) {
if (editingNode != null) {
nodeViewModel
..setAddress((editingNode!.uri.host.toString()))
..setPath((editingNode!.path.toString()))
..setPort((editingNode!.uri.port.toString()))
..setPassword((editingNode!.password ?? ''))
..setLogin((editingNode!.login ?? ''))
..setSSL((editingNode!.isSSL))
..setTrusted((editingNode!.trusted))
..setIsEnabledForAutoSwitching((editingNode!.isEnabledForAutoSwitching))
..setSocksProxy((editingNode!.useSocksProxy))
..setSocksProxyAddress((editingNode!.socksProxyAddress ?? ''));
}
if (nodeViewModel.hasAuthCredentials) {
reaction((_) => nodeViewModel.login, (String login) {
if (login != _loginController.text) {
_loginController.text = login;
}
});
reaction((_) => nodeViewModel.password, (String password) {
if (password != _passwordController.text) {
_passwordController.text = password;
}
});
}
reaction((_) => nodeViewModel.address, (String address) {
if (address != _addressController.text) {
_addressController.text = address;
}
});
reaction((_) => nodeViewModel.port, (String port) {
if (port != _portController.text) {
_portController.text = port;
}
});
reaction((_) => nodeViewModel.path, (String path) {
if (path != _pathController.text) {
_pathController.text = path;
}
});
_addressController.addListener(() => nodeViewModel.address = _addressController.text);
_pathController.addListener(() => nodeViewModel.path = _pathController.text);
_portController.addListener(() => nodeViewModel.port = _portController.text);
_loginController.addListener(() => nodeViewModel.login = _loginController.text);
_passwordController.addListener(() => nodeViewModel.password = _passwordController.text);
_socksAddressController
.addListener(() => nodeViewModel.socksProxyAddress = _socksAddressController.text);
}
final NodeCreateOrEditViewModel nodeViewModel;
final GlobalKey<FormState> formKey;
final Node? editingNode;
final WalletType? type;
final TextEditingController _addressController;
final TextEditingController _pathController;
final TextEditingController _portController;
final TextEditingController _loginController;
final TextEditingController _passwordController;
final TextEditingController _socksAddressController;
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _addressController,
hintText: S.of(context).node_address,
validator: type == WalletType.decred
? NodeAddressValidatorDecredBlankException()
: NodeAddressValidator(),
),
)
],
),
SizedBox(height: 10.0),
if (nodeViewModel.hasPathSupport) ...[
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _pathController,
hintText: "/path",
validator: NodePathValidator(),
),
)
],
),
SizedBox(height: 10.0),
],
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _portController,
hintText: S.of(context).node_port,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false),
validator: NodePortValidator(),
),
)
],
),
SizedBox(height: 10.0),
Padding(
padding: EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Observer(
builder: (_) => StandardCheckbox(
value: nodeViewModel.useSSL,
borderColor: Theme.of(context).colorScheme.surfaceContainerHighest,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: (value) => nodeViewModel.useSSL = value,
caption: S.of(context).use_ssl,
),
)
],
),
),
SizedBox(height: 10.0),
Padding(
padding: EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Observer(
builder: (_) => StandardCheckbox(
value: nodeViewModel.isEnabledForAutoSwitching,
borderColor: Theme.of(context).colorScheme.surfaceContainerHighest,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: (value) => nodeViewModel.isEnabledForAutoSwitching = value,
caption: S.current.enable_for_auto_switching,
),
),
],
),
),
SizedBox(height: 10.0),
if (nodeViewModel.hasAuthCredentials) ...[
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _loginController,
hintText: S.of(context).login,
),
)
],
),
SizedBox(height: 10.0),
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _passwordController,
hintText: S.of(context).password,
),
)
],
),
Padding(
padding: EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Observer(
builder: (_) => StandardCheckbox(
value: nodeViewModel.trusted,
borderColor: Theme.of(context).colorScheme.surfaceContainerHighest,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: (value) => nodeViewModel.trusted = value,
caption: S.of(context).trusted,
),
),
],
),
),
Observer(
builder: (_) => Column(
children: [
if (nodeViewModel.usesEmbeddedProxy) ...[
Padding(
padding: EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
StandardCheckbox(
value: nodeViewModel.usesEmbeddedProxy,
gradientBackground: false,
borderColor: Theme.of(context).dividerColor,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: null,
caption: 'Embedded Tor SOCKS Proxy',
),
],
),
),
],
Padding(
padding: EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
StandardCheckbox(
value: nodeViewModel.useSocksProxy,
borderColor: Theme.of(context).colorScheme.surfaceContainerHighest,
iconColor: Theme.of(context).colorScheme.primary,
onChanged: (value) {
if (!value) {
_socksAddressController.text = '';
}
nodeViewModel.useSocksProxy = value;
},
caption: 'SOCKS Proxy',
),
],
),
),
if (nodeViewModel.useSocksProxy) ...[
SizedBox(height: 10.0),
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _socksAddressController,
hintText: '[<ip>:]<port>',
validator: SocksProxyNodeAddressValidator(),
))
],
),
]
],
)),
]
],
),
);
}
}