Node Auto-reconnect and connectivity enhancements (#1513)

* Add auto-reconnect
Enhance connectivity issues

* minor enhancement [skip ci]

* minor: remove core secrets since it's empty

* pending transactions fix

* temporary fix for RBF

* remove unused hashes from cache key

* fix minimum limits check

* Add authentication to services api

* update polyseed

* override hashlib package
This commit is contained in:
Omar Hatem 2024-07-06 17:42:17 +03:00 committed by GitHub
parent 0335702aa9
commit f902a644db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 131 additions and 222 deletions

View file

@ -80,7 +80,7 @@ jobs:
/opt/android/cake_wallet/cw_monero/ios/External /opt/android/cake_wallet/cw_monero/ios/External
/opt/android/cake_wallet/cw_shared_external/ios/External /opt/android/cake_wallet/cw_shared_external/ios/External
/opt/android/cake_wallet/scripts/monero_c/release /opt/android/cake_wallet/scripts/monero_c/release
key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh', '**/build_haven.sh', '**/monero_api.cpp') }} key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh') }}
- if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }}
name: Generate Externals name: Generate Externals

View file

@ -1,2 +1,4 @@
- -
uri: ltc-electrum.cakewallet.com:50002 uri: ltc-electrum.cakewallet.com:50002
useSSL: true
isDefault: true

View file

@ -97,26 +97,7 @@ abstract class ElectrumWalletBase
this.walletInfo = walletInfo; this.walletInfo = walletInfo;
transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password);
reaction((_) => syncStatus, (SyncStatus syncStatus) async { reaction((_) => syncStatus, _syncStatusReaction);
if (syncStatus is! AttemptingSyncStatus && syncStatus is! SyncedTipSyncStatus) {
silentPaymentsScanningActive = syncStatus is SyncingSyncStatus;
}
if (syncStatus is NotConnectedSyncStatus) {
// Needs to re-subscribe to all scripthashes when reconnected
_scripthashesUpdateSubject = {};
// TODO: double check this and make sure it doesn't cause any un-necessary calls
// await this.electrumClient.connectToUri(node!.uri, useSSL: node!.useSSL);
}
// Message is shown on the UI for 3 seconds, revert to synced
if (syncStatus is SyncedTipSyncStatus) {
Timer(Duration(seconds: 3), () {
if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus();
});
}
});
} }
static bitcoin.HDWallet getAccountHDWallet( static bitcoin.HDWallet getAccountHDWallet(
@ -201,6 +182,8 @@ abstract class ElectrumWalletBase
@observable @observable
bool silentPaymentsScanningActive = false; bool silentPaymentsScanningActive = false;
bool _isTryingToConnect = false;
@action @action
Future<void> setSilentPaymentsScanning(bool active, bool usingElectrs) async { Future<void> setSilentPaymentsScanning(bool active, bool usingElectrs) async {
silentPaymentsScanningActive = active; silentPaymentsScanningActive = active;
@ -1830,6 +1813,38 @@ abstract class ElectrumWalletBase
syncStatus = NotConnectedSyncStatus(); syncStatus = NotConnectedSyncStatus();
} }
} }
void _syncStatusReaction(SyncStatus syncStatus) async {
if (syncStatus is! AttemptingSyncStatus && syncStatus is! SyncedTipSyncStatus) {
silentPaymentsScanningActive = syncStatus is SyncingSyncStatus;
}
if (syncStatus is NotConnectedSyncStatus) {
// Needs to re-subscribe to all scripthashes when reconnected
_scripthashesUpdateSubject = {};
if (_isTryingToConnect) return;
_isTryingToConnect = true;
Future.delayed(Duration(seconds: 10), () {
if (this.syncStatus is! SyncedSyncStatus && this.syncStatus is! SyncedTipSyncStatus) {
this.electrumClient.connectToUri(
node!.uri,
useSSL: node!.useSSL ?? false,
);
}
_isTryingToConnect = false;
});
}
// Message is shown on the UI for 3 seconds, revert to synced
if (syncStatus is SyncedTipSyncStatus) {
Timer(Duration(seconds: 3), () {
if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus();
});
}
}
} }
class ScanNode { class ScanNode {

View file

@ -866,10 +866,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: unorm_dart name: unorm_dart
sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b" sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.3.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View file

@ -227,6 +227,9 @@ class Node extends HiveObject with Keyable {
} }
} }
// TODO: this will return true most of the time, even if the node has useSSL set to true while
// it doesn't support SSL or vice versa, because it will connect normally, but it will fail if
// you try to communicate with it
Future<bool> requestElectrumServer() async { Future<bool> requestElectrumServer() async {
try { try {
if (useSSL == true) { if (useSSL == true) {

View file

@ -655,6 +655,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
unorm_dart:
dependency: transitive
description:
name: unorm_dart
sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View file

@ -543,10 +543,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: polyseed name: polyseed
sha256: a340962242d7917b0f3e6bd02c4acc3f90eae8ff766f1244f793ae7a6414dd68 sha256: edf28042e7b0b28f97a0469aa98e6e4015937cef6b9340cd6ad2822139c95217
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.4" version: "0.0.5"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -696,6 +696,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
unorm_dart:
dependency: transitive
description:
name: unorm_dart
sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View file

@ -805,6 +805,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.2"
unorm_dart:
dependency: transitive
description:
name: unorm_dart
sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View file

@ -543,10 +543,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: polyseed name: polyseed
sha256: a340962242d7917b0f3e6bd02c4acc3f90eae8ff766f1244f793ae7a6414dd68 sha256: edf28042e7b0b28f97a0469aa98e6e4015937cef6b9340cd6ad2822139c95217
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.4" version: "0.0.5"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -696,6 +696,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
unorm_dart:
dependency: transitive
description:
name: unorm_dart
sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View file

@ -19,7 +19,7 @@ dependencies:
flutter_mobx: ^2.0.6+1 flutter_mobx: ^2.0.6+1
intl: ^0.18.0 intl: ^0.18.0
encrypt: ^5.0.1 encrypt: ^5.0.1
polyseed: ^0.0.4 polyseed: ^0.0.5
cw_core: cw_core:
path: ../cw_core path: ../cw_core
monero: monero:

View file

@ -438,7 +438,7 @@ class CWBitcoin extends Bitcoin {
@override @override
int getMaxCustomFeeRate(Object wallet) { int getMaxCustomFeeRate(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as ElectrumWallet;
return (bitcoinWallet.feeRate(BitcoinTransactionPriority.fast) * 1.1).round(); return (bitcoinWallet.feeRate(BitcoinTransactionPriority.fast) * 10).round();
} }
@override @override

View file

@ -242,6 +242,7 @@ Future<void> defaultSettingsMigration(
await fixBtcDerivationPaths(walletInfoSource); await fixBtcDerivationPaths(walletInfoSource);
break; break;
case 39: case 39:
_fixNodesUseSSLFlag(nodes);
await changeDefaultNanoNode(nodes, sharedPreferences); await changeDefaultNanoNode(nodes, sharedPreferences);
break; break;
default: default:
@ -258,6 +259,17 @@ Future<void> defaultSettingsMigration(
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version); await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
} }
void _fixNodesUseSSLFlag(Box<Node> nodes) {
for (Node node in nodes.values) {
switch (node.uriRaw) {
case cakeWalletLitecoinElectrumUri:
case cakeWalletBitcoinElectrumUri:
node.useSSL = true;
break;
}
}
}
Future<void> updateNanoNodeList({required Box<Node> nodes}) async { Future<void> updateNanoNodeList({required Box<Node> nodes}) async {
final nodeList = await loadDefaultNanoNodes(); final nodeList = await loadDefaultNanoNodes();
var listOfNewEndpoints = <String>[ var listOfNewEndpoints = <String>[

View file

@ -1,163 +0,0 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/core/email_validator.dart';
import 'package:cake_wallet/ionia/ionia_create_state.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher.dart';
class IoniaCreateAccountPage extends BasePage {
IoniaCreateAccountPage(this._authViewModel)
: _emailFocus = FocusNode(),
_emailController = TextEditingController(),
_formKey = GlobalKey<FormState>() {
_emailController.text = _authViewModel.email;
_emailController.addListener(() => _authViewModel.email = _emailController.text);
}
final IoniaAuthViewModel _authViewModel;
final GlobalKey<FormState> _formKey;
final FocusNode _emailFocus;
final TextEditingController _emailController;
static const privacyPolicyUrl = 'https://ionia.docsend.com/view/jhjvdn7qq7k3ukwt';
static const termsAndConditionsUrl = 'https://ionia.docsend.com/view/uceirymz2ijacq5g';
@override
Widget middle(BuildContext context) {
return Text(
S.current.sign_up,
style: textMediumSemiBold(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
);
}
@override
Widget body(BuildContext context) {
reaction((_) => _authViewModel.createUserState, (IoniaCreateAccountState state) {
if (state is IoniaCreateStateFailure) {
_onCreateUserFailure(context, state.error);
}
if (state is IoniaCreateStateSuccess) {
_onCreateSuccessful(context, _authViewModel);
}
});
return ScrollableWithBottomSection(
contentPadding: EdgeInsets.all(24),
content: Form(
key: _formKey,
child: BaseTextFormField(
hintText: S.of(context).email_address,
focusNode: _emailFocus,
validator: EmailValidator(),
keyboardType: TextInputType.emailAddress,
controller: _emailController,
onSubmit: (_) => _createAccount(),
),
),
bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24),
bottomSection: Column(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Observer(
builder: (_) => LoadingPrimaryButton(
text: S.of(context).create_account,
onPressed: _createAccount,
isLoading:
_authViewModel.createUserState is IoniaCreateStateLoading,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
),
SizedBox(
height: 20,
),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: S.of(context).agree_to,
style: TextStyle(
color: Color(0xff7A93BA),
fontSize: 12,
fontFamily: 'Lato',
),
children: [
TextSpan(
text: S.of(context).settings_terms_and_conditions,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w700,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
final uri = Uri.parse(termsAndConditionsUrl);
if (await canLaunchUrl(uri))
await launchUrl(uri, mode: LaunchMode.externalApplication);
},
),
TextSpan(text: ' ${S.of(context).and} '),
TextSpan(
text: S.of(context).privacy_policy,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w700,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
final uri = Uri.parse(privacyPolicyUrl);
if (await canLaunchUrl(uri))
await launchUrl(uri, mode: LaunchMode.externalApplication);
}),
TextSpan(text: ' ${S.of(context).by_cake_pay}'),
],
),
),
],
),
],
),
);
}
void _onCreateUserFailure(BuildContext context, String error) {
showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.create_account,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
void _onCreateSuccessful(BuildContext context, IoniaAuthViewModel authViewModel) => Navigator.pushNamed(
context,
Routes.ioniaVerifyIoniaOtpPage,
arguments: [authViewModel.email, false],
);
void _createAccount() async {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return;
}
await _authViewModel.createUser(_emailController.text);
}
}

View file

@ -48,6 +48,7 @@ import 'package:mobx/mobx.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
part 'dashboard_view_model.g.dart'; part 'dashboard_view_model.g.dart';
@ -375,7 +376,8 @@ abstract class DashboardViewModelBase with Store {
.toList(); .toList();
} }
bool get hasSellProviders => ProvidersHelper.getAvailableSellProviderTypes(wallet.type).isNotEmpty; bool get hasSellProviders =>
ProvidersHelper.getAvailableSellProviderTypes(wallet.type).isNotEmpty;
bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup; bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
@ -535,7 +537,10 @@ abstract class DashboardViewModelBase with Store {
Future<List<String>> checkForHavenWallets() async { Future<List<String>> checkForHavenWallets() async {
final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName); final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
return walletInfoSource.values.where((element) => element.type == WalletType.haven).map((e) => e.name).toList(); return walletInfoSource.values
.where((element) => element.type == WalletType.haven)
.map((e) => e.name)
.toList();
} }
Future<List<String>> checkAffectedWallets() async { Future<List<String>> checkAffectedWallets() async {
@ -576,7 +581,13 @@ abstract class DashboardViewModelBase with Store {
Future<ServicesResponse> getServicesStatus() async { Future<ServicesResponse> getServicesStatus() async {
try { try {
if (isEnabledBulletinAction) { if (isEnabledBulletinAction) {
final res = await http.get(Uri.parse("https://service-api.cakewallet.com/v1/active-notices")); final uri = Uri.https(
"service-api.cakewallet.com",
"/v1/active-notices",
{'key': secrets.fiatApiKey},
);
final res = await http.get(uri);
if (res.statusCode < 200 || res.statusCode >= 300) { if (res.statusCode < 200 || res.statusCode >= 300) {
throw res.body; throw res.body;
@ -594,11 +605,10 @@ abstract class DashboardViewModelBase with Store {
hasUpdates, hasUpdates,
currentSha, currentSha,
); );
} } else {
else {
return ServicesResponse([], false, ''); return ServicesResponse([], false, '');
} }
} catch (_) { } catch (e) {
return ServicesResponse([], false, ''); return ServicesResponse([], false, '');
} }
} }

View file

@ -508,7 +508,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
if (limitsState is LimitsLoadedSuccessfully) { if (limitsState is LimitsLoadedSuccessfully) {
if (double.tryParse(amount) == null) continue; if (double.tryParse(amount) == null) continue;
if (limits.max != null && double.parse(amount) < limits.min!) if (limits.min != null && double.parse(amount) < limits.min!)
continue; continue;
else if (limits.max != null && double.parse(amount) > limits.max!) else if (limits.max != null && double.parse(amount) > limits.max!)
continue; continue;

View file

@ -10,6 +10,7 @@ import device_info_plus
import devicelocale import devicelocale
import flutter_inappwebview_macos import flutter_inappwebview_macos
import flutter_local_authentication import flutter_local_authentication
import flutter_secure_storage_macos
import in_app_review import in_app_review
import package_info import package_info
import package_info_plus import package_info_plus
@ -25,6 +26,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterLocalAuthenticationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalAuthenticationPlugin")) FlutterLocalAuthenticationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalAuthenticationPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))

View file

@ -106,6 +106,7 @@ dependencies:
url: https://github.com/cake-tech/bitcoin_base url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v3 ref: cake-update-v3
ledger_flutter: ^1.0.1 ledger_flutter: ^1.0.1
hashlib: 1.12.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -4,7 +4,6 @@ import 'utils/secret_key.dart';
import 'utils/utils.dart'; import 'utils/utils.dart';
const baseConfigPath = 'tool/.secrets-config.json'; const baseConfigPath = 'tool/.secrets-config.json';
const coreConfigPath = 'tool/.core-secrets-config.json';
const evmChainsConfigPath = 'tool/.evm-secrets-config.json'; const evmChainsConfigPath = 'tool/.evm-secrets-config.json';
const solanaConfigPath = 'tool/.solana-secrets-config.json'; const solanaConfigPath = 'tool/.solana-secrets-config.json';
const nanoConfigPath = 'tool/.nano-secrets-config.json'; const nanoConfigPath = 'tool/.nano-secrets-config.json';
@ -38,7 +37,6 @@ Future<void> generateSecretsConfig(List<String> args) async {
}); });
final baseConfigFile = File(baseConfigPath); final baseConfigFile = File(baseConfigPath);
final coreConfigFile = File(coreConfigPath);
final evmChainsConfigFile = File(evmChainsConfigPath); final evmChainsConfigFile = File(evmChainsConfigPath);
final solanaConfigFile = File(solanaConfigPath); final solanaConfigFile = File(solanaConfigPath);
final nanoConfigFile = File(nanoConfigPath); final nanoConfigFile = File(nanoConfigPath);
@ -64,7 +62,6 @@ Future<void> generateSecretsConfig(List<String> args) async {
await writeConfig(baseConfigFile, SecretKey.base, existingSecrets: secrets); await writeConfig(baseConfigFile, SecretKey.base, existingSecrets: secrets);
await writeConfig(coreConfigFile, SecretKey.coreSecrets);
await writeConfig(evmChainsConfigFile, SecretKey.evmChainsSecrets); await writeConfig(evmChainsConfigFile, SecretKey.evmChainsSecrets);
await writeConfig(solanaConfigFile, SecretKey.solanaSecrets); await writeConfig(solanaConfigFile, SecretKey.solanaSecrets);
await writeConfig(nanoConfigFile, SecretKey.nanoSecrets); await writeConfig(nanoConfigFile, SecretKey.nanoSecrets);

View file

@ -45,8 +45,6 @@ class SecretKey {
SecretKey('authorization', () => ''), SecretKey('authorization', () => ''),
]; ];
static final coreSecrets = [];
static final evmChainsSecrets = [ static final evmChainsSecrets = [
SecretKey('etherScanApiKey', () => ''), SecretKey('etherScanApiKey', () => ''),
SecretKey('polygonScanApiKey', () => ''), SecretKey('polygonScanApiKey', () => ''),