Merge upstream Cake Wallet v6.5.0

Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
@@ -3,12 +3,10 @@ import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:hash_wallet/entities/qr_scanner.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/utils/permission_handler.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:permission_handler/permission_handler.dart';
@@ -17,8 +15,8 @@ part 'node_create_or_edit_view_model.g.dart';
class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase with _$NodeCreateOrEditViewModel;
abstract class NodeCreateOrEditViewModelBase with Store {
NodeCreateOrEditViewModelBase(
this._nodeSource, this.walletType, this.editingNode, this._settingsStore)
NodeCreateOrEditViewModelBase(this.isPow, this.walletType, this._settingsStore,
{this.editingNode})
: state = InitialExecutionState(),
connectionState = InitialExecutionState(),
label = editingNode?.label ?? '',
@@ -106,6 +104,12 @@ abstract class NodeCreateOrEditViewModelBase with Store {
@observable
String socksProxyAddress;
@observable
Node? editingNode;
@observable
bool isPow;
@computed
bool get isReady => address.isNotEmpty;
@@ -144,8 +148,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
}
final WalletType walletType;
final Node? editingNode;
final Box<Node> _nodeSource;
final SettingsStore _settingsStore;
void updateViewModelFromText(String key, String value) {
@@ -233,30 +235,29 @@ abstract class NodeCreateOrEditViewModelBase with Store {
void setSocksProxyAddress(String val) => socksProxyAddress = val;
@action
Future<void> save({Node? editingNode, bool saveAsCurrent = false}) async {
final node = Node(
label: label,
uri: uri,
path: path,
type: walletType,
login: login,
password: password,
useSSL: useSSL,
trusted: trusted,
isEnabledForAutoSwitching: isEnabledForAutoSwitching,
socksProxyAddress: socksProxyAddress);
Future<void> delete({required Node editingNode}) async {
await editingNode.delete();
}
@action
Future<void> save({bool saveAsCurrent = false}) async {
editingNode ??= Node();
editingNode!.type = walletType;
editingNode!.label = label;
editingNode!.uriRaw = uri;
editingNode!.path = path;
editingNode!.login = login;
editingNode!.password = password;
editingNode!.isPow = isPow;
editingNode!.useSSL = useSSL;
editingNode!.trusted = trusted;
editingNode!.socksProxyAddress = socksProxyAddress;
try {
state = IsExecutingState();
if (editingNode != null) {
await _nodeSource.put(editingNode.key, node);
} else if (_existingNode(node) != null) {
setAsCurrent(_existingNode(node)!);
} else {
await _nodeSource.add(node);
setAsCurrent(_nodeSource.values.last);
}
await editingNode!.save();
if (saveAsCurrent) {
setAsCurrent(node);
setAsCurrent(editingNode!);
}
state = ExecutedSuccessfullyState();
@@ -286,16 +287,6 @@ abstract class NodeCreateOrEditViewModelBase with Store {
}
}
Node? _existingNode(Node node) {
final nodes = _nodeSource.values.toList();
nodes.forEach((item) {
item.login ??= '';
item.password ??= '';
item.useSSL ??= false;
});
return nodes.firstWhereOrNull((item) => item == node);
}
@action
void setAsCurrent(Node node) => _settingsStore.nodes[walletType] = node;
@@ -1,13 +1,14 @@
import 'dart:math';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/utils/mobx.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:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:cw_core/node.dart';
import 'package:hash_wallet/entities/node_list.dart';
import 'package:hash_wallet/entities/default_settings_migration.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hash_wallet/evm/evm.dart';
@@ -15,16 +16,38 @@ import 'package:hash_wallet/reactions/wallet_connect.dart';
part 'node_list_view_model.g.dart';
class NodeSpeed {
final String iconPath;
final String darkIconPath;
// fraction of best node ping needed to be classified as this speed.
final double treshold;
const NodeSpeed._(this.iconPath, this.darkIconPath, this.treshold);
static const disconnected = NodeSpeed._("assets/new-ui/node_speed_badges/disconnected.svg",
"assets/new-ui/node_speed_badges/disconnected-dark.svg", 0);
static const slow = NodeSpeed._("assets/new-ui/node_speed_badges/slow.svg",
"assets/new-ui/node_speed_badges/slow-dark.svg", 0);
static const medium = NodeSpeed._("assets/new-ui/node_speed_badges/medium.svg",
"assets/new-ui/node_speed_badges/medium.svg", 0.5);
static const fast = NodeSpeed._(
"assets/new-ui/node_speed_badges/fast.svg", "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._nodeSource, this._appStore)
NodeListViewModelBase(this._appStore, this.isPow)
: nodes = ObservableList<Node>(),
_nodeSpeeds = ObservableMap<String, int>(),
settingsStore = _appStore.settingsStore {
_bindNodes();
bindNodes();
reaction((_) => _appStore.wallet, (WalletBase? _wallet) {
_bindNodes();
bindNodes();
});
reaction((_) {
@@ -35,7 +58,7 @@ abstract class NodeListViewModelBase with Store {
}
return null;
}, (_) {
_bindNodes();
bindNodes();
});
}
@@ -53,7 +76,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,24 +96,74 @@ abstract class NodeListViewModelBase with Store {
final ObservableList<Node> nodes;
final SettingsStore settingsStore;
final Box<Node> _nodeSource;
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(_nodeSource);
await resetToDefault();
final wallet = _appStore.wallet!;
final walletType = wallet.type;
Node node;
if (walletType == WalletType.bitcoin && wallet.isTestnet) {
node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!;
node = (await getBitcoinTestnetDefaultElectrumServer())!;
} else if (isEVMCompatibleChain(walletType)) {
final chainId = evm!.getSelectedChainId(wallet);
if (chainId != null) {
final nodeWalletType = evm!.getWalletTypeByChainId(chainId);
if (nodeWalletType != null) {
node = getDefaultNode(nodes: _nodeSource, type: nodeWalletType)!;
node = (await Node.getDefaultForWalletType(nodeWalletType))!;
} else {
throw Exception(
'Cannot reset node for EVM wallet: wallet type not found for chainId: $chainId');
@@ -99,7 +172,7 @@ abstract class NodeListViewModelBase with Store {
throw Exception('Cannot reset node for EVM wallet: chainId is null');
}
} else {
node = getDefaultNode(nodes: _nodeSource, type: walletType)!;
node = (await Node.getDefaultForWalletType(_appStore.wallet!.type))!;
}
await setAsCurrent(node);
@@ -126,11 +199,15 @@ 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
void _bindNodes() {
Future<void> bindNodes() async {
nodes.clear();
final wallet = _appStore.wallet!;
final walletType = wallet.type;
@@ -141,11 +218,8 @@ abstract class NodeListViewModelBase with Store {
if (chainId != null) {
final nodeWalletType = evm!.getWalletTypeByChainId(chainId);
if (nodeWalletType != null) {
_nodeSource.bindToList(
nodes,
filter: (val) => val.type == nodeWalletType,
initialFire: true,
);
nodes.addAll(await Node.getAllForWalletType(nodeWalletType));
return;
}
}
@@ -154,10 +228,8 @@ abstract class NodeListViewModelBase with Store {
}
// For non-EVM wallets, use the wallet type directly
_nodeSource.bindToList(
nodes,
filter: (val) => val.type == walletType,
initialFire: true,
);
nodes.addAll(isPow
? await Node.getAllForWalletTypePow(walletType)
: await Node.getAllForWalletType(walletType));
}
}
@@ -1,87 +1 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/utils/mobx.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:hash_wallet/store/settings_store.dart';
import 'package:cw_core/node.dart';
import 'package:hash_wallet/entities/node_list.dart';
import 'package:hash_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._nodeSource, 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 Box<Node> _nodeSource;
final AppStore _appStore;
Future<void> reset() async {
await resetPowToDefault(_nodeSource);
Node node;
switch (_appStore.wallet!.type) {
case WalletType.nano:
node = getNanoDefaultPowNode(nodes: _nodeSource)!;
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
void _bindNodes() {
nodes.clear();
_nodeSource.bindToList(
nodes,
filter: (val) => val.type == _appStore.wallet!.type,
initialFire: true,
);
}
}