Merge branch 'dev' into cw-1440-new-buy-sell-flow

This commit is contained in:
malik1004x
2026-08-20 16:26:13 +02:00
committed by GitHub
220 changed files with 11956 additions and 9280 deletions
+4 -14
View File
@@ -177,13 +177,7 @@ class AddressValidator extends TextValidator {
if (ZcashNetworkType.isDevNetwork(network)) {
return null;
}
pattern = '(?:'
't1[0-9A-Za-z]{33}'
'|t3[0-9A-Za-z]{33}'
'|zs[a-z0-9]{76}'
'|zxviews[a-z0-9]{278}'
'|u1[a-z0-9]{1,300}'
')';
pattern = "(?:$zcashAddressPattern|zxviews[a-z0-9]{278})";
case CryptoCurrency.dcr:
pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+';
case CryptoCurrency.rvn:
@@ -355,6 +349,8 @@ class AddressValidator extends TextValidator {
static String get silentPaymentAddressPatternMainnet => 'sp1[0-9a-zA-Z]{113}';
static String get silentPaymentAddressPatternTestnet => '(tsp|sprt)1[0-9a-zA-Z]{113}';
static String get mWebAddressPattern => MwebAddress.regex.pattern;
static String get zcashAddressPattern =>
"t1[0-9A-Za-z]{33}|t3[0-9A-Za-z]{33}|zs[a-z0-9]{76}|u1[a-z0-9]{1,300}";
static const String bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$';
static const String bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$';
@@ -401,13 +397,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.zano:
pattern = '([1-9A-HJ-NP-Za-km-z]{90,200})|(@[\w\d.-]+)';
case CryptoCurrency.zec:
pattern = '(?:'
't1[0-9A-Za-z]{33}'
'|t3[0-9A-Za-z]{33}'
'|zs[a-z0-9]{76}'
'|zxviews[a-z0-9]{278}'
'|u1[a-z0-9]{1,300}'
')';
pattern = "(?:$zcashAddressPattern|zxviews[a-z0-9]{278})";
default:
if (type.tag == CryptoCurrency.eth.title) {
pattern = '0x[0-9a-zA-Z]+';
+174
View File
@@ -0,0 +1,174 @@
import "package:cake_wallet/core/universal_address_detector.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
sealed class ChainBinding {
const ChainBinding();
}
class ExplicitEvmChain extends ChainBinding {
const ExplicitEvmChain(this.chainId);
final int chainId;
}
class ChainlessEvm extends ChainBinding {
const ChainlessEvm();
}
class NoEvmBinding extends ChainBinding {
const NoEvmBinding();
}
class AnyPayRequest {
AnyPayRequest({
required this.rawInput,
required this.paymentRequest,
required this.detection,
required this.chainBinding,
});
final String rawInput;
final PaymentRequest paymentRequest;
final AddressDetectionResult detection;
final ChainBinding chainBinding;
String get address => paymentRequest.address;
String get amount => paymentRequest.amount;
String? get contractAddress => paymentRequest.contractAddress;
bool get hasContract => contractAddress != null && contractAddress!.isNotEmpty;
bool get isLightning => detection.detectedCurrency == CryptoCurrency.btcln;
}
class WalletSnapshot {
WalletSnapshot({
required this.type,
required this.currentWalletIsEvm,
required this.currentChainId,
required this.wallets,
required this.supportedEvmChains,
required this.hasEvmProxy,
required this.hasSolanaProxy,
required this.hasTronProxy,
});
final WalletType type;
final bool currentWalletIsEvm;
final int? currentChainId;
final List<WalletInfo> wallets;
final Map<int, WalletType> supportedEvmChains;
final bool hasEvmProxy;
final bool hasSolanaProxy;
final bool hasTronProxy;
List<WalletInfo> walletsOfType(WalletType walletType) =>
wallets.where((wallet) => wallet.type == walletType).toList();
}
sealed class AnyPayResolution {
const AnyPayResolution();
}
class NoTokenRequested extends AnyPayResolution {
const NoTokenRequested();
}
class TokenResolved extends AnyPayResolution {
const TokenResolved({required this.token, this.chainId, this.amountOverride});
final CryptoCurrency token;
final int? chainId;
final String? amountOverride;
}
class TokenUnknown extends AnyPayResolution {
const TokenUnknown(this.contract);
final String contract;
}
sealed class AnyPayDecision {
const AnyPayDecision();
}
class AnyPayApplyToCurrentWallet extends AnyPayDecision {
const AnyPayApplyToCurrentWallet({
this.token,
this.amountOverride,
this.fallbackCurrency,
});
final CryptoCurrency? token;
final String? amountOverride;
final CryptoCurrency? fallbackCurrency;
}
class AnyPayEvmNetworkChoice extends AnyPayDecision {
const AnyPayEvmNetworkChoice();
}
class AnyPayCrossChainPayment extends AnyPayDecision {
const AnyPayCrossChainPayment({
required this.targetWalletType,
required this.wallets,
this.targetChainId,
this.token,
this.amountOverride,
this.canSwap = true,
});
final WalletType targetWalletType;
final int? targetChainId;
final List<WalletInfo> wallets;
final CryptoCurrency? token;
final String? amountOverride;
final bool canSwap;
bool get hasCompatibleWallet => wallets.isNotEmpty;
}
class AnyPayUnsupportedNetwork extends AnyPayDecision {
const AnyPayUnsupportedNetwork(this.chainId);
final int chainId;
}
class AnyPayUnsupportedToken extends AnyPayDecision {
const AnyPayUnsupportedToken(this.contract);
final String contract;
}
class AnyPayEmptyInput extends AnyPayDecision {
const AnyPayEmptyInput();
}
class AnyPayEvaluation {
AnyPayEvaluation({required this.request, required this.decision});
final AnyPayRequest request;
final AnyPayDecision decision;
}
class AnyPaySwapIntent {
AnyPaySwapIntent({
required this.request,
required this.receiveCurrency,
required this.targetWalletType,
this.receiveAmount,
});
final AnyPayRequest request;
final CryptoCurrency receiveCurrency;
final WalletType targetWalletType;
final Money? receiveAmount;
String get recipientAddress => request.address;
}
+71
View File
@@ -0,0 +1,71 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/universal_address_detector.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/payment_uris.dart";
class AnyPayParser {
static AnyPayRequest fromRaw(String input) {
final paymentRequest = PaymentRequest.fromString(input);
return AnyPayRequest(
rawInput: input,
paymentRequest: paymentRequest,
detection: UniversalAddressDetector.detectAddress(input),
chainBinding: _bindingFor(paymentRequest),
);
}
static AnyPayRequest fromPaymentRequest(PaymentRequest request) {
final raw = _reconstructRawInput(request);
return AnyPayRequest(
rawInput: raw,
paymentRequest: request,
detection: UniversalAddressDetector.detectAddress(raw),
chainBinding: _bindingFor(request),
);
}
static String _reconstructRawInput(PaymentRequest request) {
final scheme = request.scheme.toLowerCase();
if (scheme.isEmpty || scheme == "lightning") {
return request.address;
}
if (scheme == "ethereum") {
return ERC681URI(
address: request.address,
amount: request.amount,
contractAddress: request.contractAddress,
chainId: request.chainId ?? 1,
rawTokenAmount: request.rawTokenAmount,
).toString();
}
final amount = request.amount.isNotEmpty ? "?amount=${request.amount}" : "";
return "${request.scheme}:${request.address}$amount";
}
static ChainBinding _bindingFor(PaymentRequest request) {
final scheme = request.scheme.toLowerCase();
if (scheme == "ethereum") {
final chainId = request.chainId;
return chainId != null ? ExplicitEvmChain(chainId) : const ChainlessEvm();
}
if (scheme.isNotEmpty) {
try {
final chainId = getChainIdByCryptoCurrency(CryptoCurrency.fromString(scheme));
if (chainId != null) {
return ExplicitEvmChain(chainId);
}
} catch (_) {}
}
return const NoEvmBinding();
}
}
+109
View File
@@ -0,0 +1,109 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_router.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/utils/token_utilities.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayResolver {
AnyPayResolver({AnyPayTokenLookup? tokenLookup})
: _tokenLookup = tokenLookup ?? const TokenUtilitiesLookup();
final AnyPayTokenLookup _tokenLookup;
Future<AnyPayResolution> resolve(AnyPayRequest request, WalletSnapshot snapshot) async {
if (!request.hasContract) {
return const NoTokenRequested();
}
final contract = request.contractAddress!;
final detectedType = request.detection.detectedWalletType;
if (detectedType != null && isEVMCompatibleChain(detectedType)) {
return _resolveEvm(request, snapshot, contract);
}
final token = await _tokenLookup.findTokenByAddress(
walletType: detectedType ?? snapshot.type,
address: contract,
);
if (token == null) {
return TokenUnknown(contract);
}
return TokenResolved(
token: token,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
Future<AnyPayResolution> _resolveEvm(
AnyPayRequest request,
WalletSnapshot snapshot,
String contract,
) async {
final requestedChainId = AnyPayRouter.requestedEvmChainId(request, snapshot) ?? 1;
final lookupType = snapshot.supportedEvmChains[requestedChainId];
if (lookupType != null) {
final token =
await _tokenLookup.findTokenByAddress(walletType: lookupType, address: contract);
if (token != null) {
return TokenResolved(
token: token,
chainId: requestedChainId,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
}
// The QRs from our old app versions omit the chainId on mainnet, so a contract the target
// network does not know may still belong to another EVM network.
if (request.chainBinding is ChainlessEvm) {
final reboundChainId = await _tokenLookup.findEvmChainIdForContract(
contract,
excludingChainId: requestedChainId,
);
final reboundType =
reboundChainId != null ? snapshot.supportedEvmChains[reboundChainId] : null;
if (reboundType != null) {
final token =
await _tokenLookup.findTokenByAddress(walletType: reboundType, address: contract);
if (token != null) {
return TokenResolved(
token: token,
chainId: reboundChainId,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
}
}
return TokenUnknown(contract);
}
}
abstract class AnyPayTokenLookup {
Future<CryptoCurrency?> findTokenByAddress({
required WalletType walletType,
required String address,
});
Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId});
}
class TokenUtilitiesLookup implements AnyPayTokenLookup {
const TokenUtilitiesLookup();
@override
Future<CryptoCurrency?> findTokenByAddress({
required WalletType walletType,
required String address,
}) =>
TokenUtilities.findTokenByAddress(walletType: walletType, address: address);
@override
Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId}) =>
TokenUtilities.findEvmChainIdForContract(contractAddress, excludingChainId: excludingChainId);
}
+141
View File
@@ -0,0 +1,141 @@
import "package:cake_wallet/core/address_validator.dart";
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayRouter {
static final List<RegExp> _swapIncompatibleAddressPatterns = [
RegExp("^(?:${AddressValidator.silentPaymentAddressPatternMainnet})\$"),
RegExp("^(?:${AddressValidator.silentPaymentAddressPatternTestnet})\$"),
RegExp("^(?:${AddressValidator.mWebAddressPattern})\$"),
];
static bool _isSwapIncompatibleAddress(String address) =>
_swapIncompatibleAddressPatterns.any((pattern) => pattern.hasMatch(address));
static AnyPayDecision route(
AnyPayRequest request,
WalletSnapshot snapshot,
AnyPayResolution resolution,
) {
if (request.rawInput.trim().isEmpty) {
return const AnyPayEmptyInput();
}
final detection = request.detection;
final detectedType = detection.detectedWalletType;
if (!detection.isValid || detectedType == null) {
return const AnyPayApplyToCurrentWallet();
}
if (detectedType == WalletType.solana && !snapshot.hasSolanaProxy) {
return const AnyPayApplyToCurrentWallet();
}
if (detectedType == WalletType.tron && !snapshot.hasTronProxy) {
return const AnyPayApplyToCurrentWallet();
}
if (isEVMCompatibleChain(detectedType)) {
if (!snapshot.hasEvmProxy) {
return const AnyPayApplyToCurrentWallet();
}
return _routeEvm(request, snapshot, resolution);
}
if (resolution is TokenUnknown) {
return AnyPayUnsupportedToken(resolution.contract);
}
if (detectedType == snapshot.type) {
return AnyPayApplyToCurrentWallet(
token: resolution is TokenResolved ? resolution.token : null,
amountOverride: resolution is TokenResolved ? resolution.amountOverride : null,
fallbackCurrency: _nativeRequestCurrency(request),
);
}
return AnyPayCrossChainPayment(
targetWalletType: detectedType,
wallets: snapshot.walletsOfType(detectedType),
token: resolution is TokenResolved ? resolution.token : null,
amountOverride: resolution is TokenResolved ? resolution.amountOverride : null,
canSwap: !_isSwapIncompatibleAddress(request.address),
);
}
static AnyPayDecision _routeEvm(
AnyPayRequest request,
WalletSnapshot snapshot,
AnyPayResolution resolution,
) {
final requestedChainId = requestedEvmChainId(request, snapshot);
if (requestedChainId == null) {
if (snapshot.currentWalletIsEvm) {
return const AnyPayApplyToCurrentWallet();
}
return const AnyPayEvmNetworkChoice();
}
if (!snapshot.supportedEvmChains.containsKey(requestedChainId)) {
return AnyPayUnsupportedNetwork(requestedChainId);
}
if (resolution is TokenUnknown) {
return AnyPayUnsupportedToken(resolution.contract);
}
int targetChainId = requestedChainId;
if (resolution is TokenResolved && resolution.chainId != null) {
targetChainId = resolution.chainId!;
}
final token = resolution is TokenResolved ? resolution.token : null;
final amountOverride = resolution is TokenResolved ? resolution.amountOverride : null;
if (snapshot.currentWalletIsEvm && snapshot.currentChainId == targetChainId) {
return AnyPayApplyToCurrentWallet(
token: token,
amountOverride: amountOverride,
fallbackCurrency: _nativeRequestCurrency(request),
);
}
final targetWalletType = snapshot.supportedEvmChains[targetChainId]!;
return AnyPayCrossChainPayment(
targetWalletType: targetWalletType,
targetChainId: targetChainId,
wallets: snapshot.walletsOfType(targetWalletType),
token: token,
amountOverride: amountOverride,
);
}
static CryptoCurrency? _nativeRequestCurrency(AnyPayRequest request) {
if (request.paymentRequest.scheme.isEmpty || request.hasContract || request.amount.isEmpty) {
return null;
}
return request.detection.detectedCurrency;
}
static int? requestedEvmChainId(AnyPayRequest request, WalletSnapshot snapshot) {
final binding = request.chainBinding;
if (binding is ExplicitEvmChain) {
return binding.chainId;
}
if (binding is ChainlessEvm) {
// the eip 681 spec says we should use the current chain id if there is no chainId in the incoming request
return snapshot.currentWalletIsEvm ? snapshot.currentChainId ?? 1 : 1;
}
return null;
}
}
+140
View File
@@ -0,0 +1,140 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_parser.dart";
import "package:cake_wallet/core/anypay/anypay_resolver.dart";
import "package:cake_wallet/core/anypay/anypay_router.dart";
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/new-ui/services/wallet_switch_service.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/solana/solana.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cake_wallet/tron/tron.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayService {
AnyPayService({
required this.appStore,
required this.walletSwitchService,
AnyPayResolver? resolver,
}) : resolver = resolver ?? AnyPayResolver();
final AppStore appStore;
final WalletSwitchService walletSwitchService;
final AnyPayResolver resolver;
Future<AnyPayEvaluation> evaluateRawInput(String input) => _evaluate(AnyPayParser.fromRaw(input));
Future<AnyPayEvaluation> evaluatePaymentRequest(PaymentRequest request) =>
_evaluate(AnyPayParser.fromPaymentRequest(request));
Future<AnyPayEvaluation> evaluateForEvmChain(AnyPayRequest request, int chainId) => _evaluate(
AnyPayRequest(
rawInput: request.rawInput,
paymentRequest: request.paymentRequest,
detection: request.detection,
chainBinding: ExplicitEvmChain(chainId),
),
);
Future<AnyPayEvaluation> _evaluate(AnyPayRequest request) async {
try {
final snapshot = await _buildWalletSnapshot();
final resolution = await resolver.resolve(request, snapshot);
return AnyPayEvaluation(
request: request,
decision: AnyPayRouter.route(request, snapshot, resolution),
);
} catch (e) {
printV("anypay evaluation failed: $e");
return AnyPayEvaluation(
request: request,
decision: const AnyPayApplyToCurrentWallet(),
);
}
}
Future<WalletSnapshot> _buildWalletSnapshot() async {
final wallet = appStore.wallet!;
final currentWalletIsEvm = isEVMCompatibleChain(wallet.type);
final supportedEvmChains = <int, WalletType>{};
if (evm != null) {
for (final chain in evm!.getAllChains()) {
final walletType = evm!.getWalletTypeByChainId(chain.chainId);
if (walletType != null) {
supportedEvmChains[chain.chainId] = walletType;
}
}
}
return WalletSnapshot(
type: wallet.type,
currentWalletIsEvm: currentWalletIsEvm,
currentChainId: currentWalletIsEvm && evm != null
? evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type)
: null,
wallets: await WalletInfo.getAll(),
supportedEvmChains: supportedEvmChains,
hasEvmProxy: evm != null,
hasSolanaProxy: solana != null,
hasTronProxy: tron != null,
);
}
AnyPaySwapIntent buildSwapIntent(
AnyPayRequest request, {
required WalletType targetWalletType,
int? targetChainId,
CryptoCurrency? token,
}) {
final receiveCurrency = token ??
(targetChainId != null
? walletTypeToCryptoCurrency(targetWalletType, chainId: targetChainId)
: request.detection.detectedCurrency ?? walletTypeToCryptoCurrency(targetWalletType));
final amountValue = token != null
? request.paymentRequest.resolveTokenAmount(token)
: (request.amount.isNotEmpty ? request.amount : null);
return AnyPaySwapIntent(
request: request,
receiveCurrency: receiveCurrency,
targetWalletType: targetWalletType,
receiveAmount: amountValue != null ? Money.tryParse(amountValue, receiveCurrency) : null,
);
}
Future<bool> switchWalletForPayment(WalletInfo walletInfo, {int? chainId}) async {
try {
await walletSwitchService.switchToWallet(walletInfo);
} catch (e) {
printV("wallet switch failed: $e");
return false;
}
final wallet = appStore.wallet!;
if (chainId != null && evm != null && isEVMCompatibleChain(wallet.type)) {
try {
final node = appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId);
await evm!.selectChain(wallet, chainId, node: node);
} catch (e, s) {
printV("switchWalletForPayment chain select failed: $e\n$s");
return false;
}
}
try {
await wallet.updateBalance();
} catch (e) {
printV("balance refresh after wallet switch failed: $e");
}
return true;
}
}
+7
View File
@@ -18,6 +18,9 @@ import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/entities/encrypt.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cw_core/erc20_token_legacy.dart' show performErc20TokenHiveMigration;
import 'package:cw_core/spl_token_legacy.dart' show performSplTokenHiveMigration;
import 'package:cw_core/tron_token_legacy.dart' show performTronTokenHiveMigration;
import 'package:cw_core/wallet_info.dart';
import 'package:cake_wallet/exchange/trade_legacy.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
@@ -113,6 +116,10 @@ class $BackupService {
Future<void> verifyWallets() async {
await performHiveMigration(); // for backups made before sqlite migration
await performTradeHiveMigration(_secureStorage);
await performErc20TokenHiveMigration();
await performSplTokenHiveMigration();
await performTronTokenHiveMigration();
correctWallets = (await WalletInfo.getAll())
.where((info) => availableWalletTypes.contains(info.type))
.toList();
+1 -1
View File
@@ -42,7 +42,7 @@ String syncStatusTitle(SyncStatus syncStatus, SyncStatusDisplayMode syncStatusDi
}
if (syncStatus is SyncedSyncStatus) {
return "";
return S.current.sync_synced;
}
if (syncStatus is FailedSyncStatus) {
+6
View File
@@ -238,6 +238,12 @@ class UniversalAddressDetector {
currency: CryptoCurrency.doge,
),
// Zcash (transparent, Sapling shielded, unified)
_DetectionPattern(
pattern: RegExp("^(?:${AddressValidator.zcashAddressPattern})\$"),
currency: CryptoCurrency.zec,
),
// Solana (Base58 format)
_DetectionPattern(
pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{43,44}$'),
+14 -4
View File
@@ -7,6 +7,7 @@ import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/core/anypay/anypay_service.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
@@ -62,6 +63,7 @@ import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
import 'package:cake_wallet/new-ui/pages/home_page.dart';
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import "package:cake_wallet/new-ui/services/wallet_switch_service.dart";
import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
import 'package:cake_wallet/new-ui/pages/receive_page.dart';
import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart';
@@ -171,7 +173,6 @@ import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
@@ -195,7 +196,6 @@ import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
import 'package:cake_wallet/store/bridge_transfers_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
import 'package:cake_wallet/store/node_list_store.dart';
import 'package:cake_wallet/store/seed_settings_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
@@ -252,7 +252,6 @@ import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_cr
import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_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:cake_wallet/view_model/order_details_view_model.dart';
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
@@ -928,7 +927,8 @@ Future<void> setup({
authService: getIt.get<AuthService>(),
params: params,
contactListViewModel: getIt.get<ContactListViewModel>(),
paymentViewModel: getIt.get<PaymentViewModel>(),
anyPayService: getIt.get<AnyPayService>(),
linkViewModel: getIt.get<LinkViewModel>(),
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
);
});
@@ -1293,6 +1293,16 @@ Future<void> setup({
appStore: getIt.get<AppStore>(),
));
getIt.registerFactory(() => WalletSwitchService(
walletLoadingService: getIt.get<WalletLoadingService>(),
appStore: getIt.get<AppStore>(),
));
getIt.registerFactory(() => AnyPayService(
appStore: getIt.get<AppStore>(),
walletSwitchService: getIt.get<WalletSwitchService>(),
));
getIt.registerFactory(() => WalletSwitcherViewModel(
appStore: getIt.get<AppStore>(),
walletLoadingService: getIt.get<WalletLoadingService>(),
+19 -36
View File
@@ -27,7 +27,6 @@ import 'package:cw_core/wallet_type.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:hive/hive.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/erc20_token.dart';
const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081';
@@ -1270,22 +1269,22 @@ Future<void> _addXautTokenToExistingEthereumWallets() async {
final ethereumWallets =
allWallets.where((wallet) => wallet.type == WalletType.ethereum).toList();
const ethereumChainId = 1;
for (final walletInfo in ethereumWallets) {
final sanitizedName = walletInfo.name.replaceAll(' ', '_');
final boxName = '${sanitizedName}_${Erc20Token.ethereumBoxName}';
final existingToken = await Erc20Token.getByContract(
walletInfo.name,
ethereumChainId,
xautToken.contractAddress,
);
Box<Erc20Token> tokenBox;
if (CakeHive.isBoxOpen(boxName)) {
tokenBox = CakeHive.box<Erc20Token>(boxName);
} else {
tokenBox = await CakeHive.openBox<Erc20Token>(boxName);
}
if (existingToken != null) continue;
final xautAddress = xautToken.contractAddress;
if (!tokenBox.containsKey(xautAddress)) {
await tokenBox.put(xautAddress, xautToken);
}
await Erc20Token.copyWith(
xautToken,
walletName: walletInfo.name,
chainId: ethereumChainId,
).save();
}
} catch (e) {
printV('Error in XAUT migration: $e');
@@ -1309,20 +1308,11 @@ Future<void> _addXaut0TokenToExistingSolanaWallets() async {
final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList();
for (final walletInfo in solanaWallets) {
final sanitizedName = walletInfo.name.replaceAll(' ', '_');
final boxName = '${sanitizedName}_${SPLToken.boxName}';
final existingToken = await SPLToken.getByMint(walletInfo.name, xaut0Token.mintAddress);
Box<SPLToken> tokenBox;
if (CakeHive.isBoxOpen(boxName)) {
tokenBox = CakeHive.box<SPLToken>(boxName);
} else {
tokenBox = await CakeHive.openBox<SPLToken>(boxName);
}
if (existingToken != null) continue;
final xaut0Address = xaut0Token.mintAddress;
if (!tokenBox.containsKey(xaut0Address)) {
await tokenBox.put(xaut0Address, xaut0Token);
}
await SPLToken.copyWith(xaut0Token, walletName: walletInfo.name).save();
}
} catch (e) {
printV('Error in XAUT0 migration: $e');
@@ -1346,20 +1336,13 @@ Future<void> _addTbbTokenToExistingSolanaWallets() async {
final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList();
for (final walletInfo in solanaWallets) {
final sanitizedName = walletInfo.name.replaceAll(" ", "_");
final boxName = "${sanitizedName}_${SPLToken.boxName}";
final existingToken = await SPLToken.getByMint(walletInfo.name, tbbToken.mintAddress);
Box<SPLToken> tokenBox;
if (CakeHive.isBoxOpen(boxName)) {
tokenBox = CakeHive.box<SPLToken>(boxName);
} else {
tokenBox = await CakeHive.openBox<SPLToken>(boxName);
if (existingToken != null) {
continue;
}
final tbbAddress = tbbToken.mintAddress;
if (!tokenBox.containsKey(tbbAddress)) {
await tokenBox.put(tbbAddress, tbbToken);
}
await SPLToken.copyWith(tbbToken, walletName: walletInfo.name).save();
}
} catch (e) {
printV("Error in TBB migration: $e");
@@ -534,6 +534,13 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
final symbol = currency.title.toUpperCase();
final blockchain = _normalizeTagToNearBlockchain(currency.tag);
// Use the native Bitcoin asset routed through Omni Bridge.
if (currency == CryptoCurrency.btc) {
return supported.firstWhereOrNull(
(t) => t.assetId == '1cs_v1:btc:native:coin',
);
}
// Native asset (no contract)
final native = supported.firstWhereOrNull((t) =>
t.symbol.toUpperCase() == symbol &&
@@ -572,7 +579,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider {
String _buildDeadline() {
return DateTime.now()
.toUtc()
.add(const Duration(hours: 2))
.add(const Duration(hours: 1))
.toIso8601String()
.replaceFirst(RegExp(r'\.\d+Z$'), 'Z');
}
+6 -14
View File
@@ -43,7 +43,7 @@ import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cw_core/address_info.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/db/sqlite.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/erc20_token_legacy.dart' show performErc20TokenHiveMigration;
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/key.dart';
import 'package:cw_core/mweb_utxo.dart';
@@ -51,8 +51,8 @@ import 'package:cw_core/node.dart';
import 'package:cw_core/node_legacy.dart' show performNodeHiveMigration;
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/root_dir.dart';
import 'package:cw_core/spl_token.dart';
import 'package:cw_core/tron_token.dart';
import 'package:cw_core/spl_token_legacy.dart' show performSplTokenHiveMigration;
import 'package:cw_core/tron_token_legacy.dart' show performTronTokenHiveMigration;
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart';
@@ -265,18 +265,10 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
CakeHive.registerAdapter(PayjoinSessionAdapter());
}
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
CakeHive.registerAdapter(Erc20TokenAdapter());
}
if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
CakeHive.registerAdapter(SPLTokenAdapter());
}
if (!CakeHive.isAdapterRegistered(TronToken.typeId)) {
CakeHive.registerAdapter(TronTokenAdapter());
}
await performHiveMigration();
await performErc20TokenHiveMigration();
await performSplTokenHiveMigration();
await performTronTokenHiveMigration();
final secureStorage = secureStorageShared;
final transactionDescriptionsBoxKey =
File diff suppressed because it is too large Load Diff
+36 -8
View File
@@ -7,6 +7,7 @@ import "package:cake_wallet/exchange/exchange_trade_state.dart";
import "package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart";
import "package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
@@ -27,6 +28,7 @@ import "package:cake_wallet/src/widgets/primary_button.dart";
import "package:cake_wallet/utils/debounce.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/dashboard/balance_view_model.dart";
import "package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart";
import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
@@ -51,6 +53,7 @@ class NewSwapPage extends StatefulWidget {
required this.walletSwitcherViewModel,
CryptoCurrency? initialCurrency,
this.fromSend,
this.balanceViewModel,
}) {
depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
? exchangeViewModel.wallet.name
@@ -69,6 +72,7 @@ class NewSwapPage extends StatefulWidget {
final AddressResolverService adrResService;
final PaymentRequest? initialPaymentRequest;
final SwapFromSendArgs? fromSend;
final BalanceViewModel? balanceViewModel;
late final String? depositWalletName;
late final String? receiveWalletName;
@@ -93,6 +97,25 @@ class _NewSwapPageState extends State<NewSwapPage> {
.contains(widget.exchangeViewModel.depositCurrency) &&
!(widget.exchangeViewModel.status is SyncedSyncStatus);
Map<CryptoCurrency, CurrencyPickerBalance>? _depositBalanceByAsset() {
final balanceViewModel = widget.balanceViewModel;
if (balanceViewModel == null) {
return null;
}
return {
for (final r in balanceViewModel.formattedBalances)
r.asset: CurrencyPickerBalance(
amount: "${r.availableBalance} ${r.asset.title}",
fiat: balanceViewModel.isFiatDisabled
? null
: "${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}",
fiatValue:
balanceViewModel.isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw),
),
};
}
@override
void initState() {
super.initState();
@@ -258,8 +281,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
(bool isReceiveAmountEditable) {},
));
_disposers
.add(reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) {
_disposers.add(
reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) async {
if (state is TradeIsCreatedFailure) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
@@ -298,11 +321,15 @@ class _NewSwapPageState extends State<NewSwapPage> {
exchangeTradeViewModel: vm,
receiveAmount: receiveAmount,
);
showMaterialModalBottomSheet(
context: context,
builder: (context) => page,
backgroundColor: Colors.transparent,
);
try {
await showMaterialModalBottomSheet<void>(
context: context,
builder: (context) => page,
backgroundColor: Colors.transparent,
);
} finally {
widget.exchangeViewModel.tradeStarted = false;
}
}
}));
@@ -629,7 +656,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
walletName: fromSend != null
? widget.exchangeViewModel.wallet.name
: null,
balanceByAsset: fromSend?.depositBalanceByAsset,
balanceByAsset:
fromSend != null ? _depositBalanceByAsset() : null,
useSingleNetworkLayout: fromSend != null,
filteredNetwork: fromSend != null
? widget.exchangeViewModel.wallet.type
@@ -0,0 +1,29 @@
import "package:cake_wallet/core/wallet_loading_service.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cw_core/exceptions.dart";
import "package:cw_core/wallet_info.dart";
class WalletSwitchService {
WalletSwitchService({
required WalletLoadingService walletLoadingService,
required AppStore appStore,
}) : _walletLoadingService = walletLoadingService,
_appStore = appStore;
final AppStore _appStore;
final WalletLoadingService _walletLoadingService;
Future<void> switchToWallet(WalletInfo walletInfo) async {
final wallet = await _walletLoadingService.load(walletInfo.type, walletInfo.name);
// load() recovers from a corrupted wallet by opening any other wallet it can,
// so the one it returns may not be the one that was requested.
if (wallet.name != walletInfo.name || wallet.type != walletInfo.type) {
throw WalletSwitchException(
"wallet switch loaded ${wallet.name} instead of ${walletInfo.name}",
);
}
await _appStore.changeCurrentWallet(wallet);
}
}
+391
View File
@@ -0,0 +1,391 @@
import "package:cake_wallet/core/address_resolver/address_resolver_service.dart";
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_service.dart";
import "package:cake_wallet/core/auth_service.dart";
import "package:cake_wallet/di.dart";
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/pages/swap_page.dart";
import "package:cake_wallet/new-ui/widgets/anypay/evm_address_detected_sheet.dart";
import "package:cake_wallet/new-ui/widgets/anypay/network_decision_page.dart";
import "package:cake_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
import "package:cake_wallet/new-ui/widgets/anypay/switch_network_wallet_page.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
import "package:cake_wallet/view_model/send/send_view_model.dart";
import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/unspent_coin_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_info.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
class AnyPayFormFill {
const AnyPayFormFill({
required this.request,
this.token,
this.fallbackCurrency,
this.amountOverride,
});
final AnyPayRequest request;
final CryptoCurrency? token;
final CryptoCurrency? fallbackCurrency;
final String? amountOverride;
}
class AnyPayFlow {
AnyPayFlow({
required this.anyPayService,
required this.sendViewModel,
required this.authService,
required this.walletSwitcherViewModel,
});
final AnyPayService anyPayService;
final SendViewModel sendViewModel;
final AuthService authService;
final WalletSwitcherViewModel walletSwitcherViewModel;
bool _isSwitchingWallet = false;
bool get isSwitchingWallet => _isSwitchingWallet;
Future<AnyPayFormFill?> handleEvaluation(
BuildContext context,
AnyPayEvaluation evaluation, {
CryptoCurrency? fallbackCurrency,
}) async {
final request = evaluation.request;
if (!request.rawInput.toLowerCase().startsWith("ethereum:") &&
(request.rawInput.contains("@") || request.address.contains("@"))) {
return null;
}
try {
return await _handleDecision(context, evaluation, fallbackCurrency);
} catch (e) {
printV("Payment flow error: $e");
return AnyPayFormFill(request: request);
}
}
Future<AnyPayFormFill?> _handleDecision(
BuildContext context,
AnyPayEvaluation evaluation,
CryptoCurrency? fallbackCurrency,
) async {
final request = evaluation.request;
switch (evaluation.decision) {
case final AnyPayApplyToCurrentWallet decision:
if (request.isLightning) {
_enterLightningMode();
}
return AnyPayFormFill(
request: request,
token: decision.token,
fallbackCurrency: fallbackCurrency ?? decision.fallbackCurrency,
amountOverride: decision.amountOverride,
);
case AnyPayEvmNetworkChoice():
return _pickEvmNetwork(context, request);
case final AnyPayCrossChainPayment decision:
return _handleCrossChain(context, request, decision, fallbackCurrency);
case final AnyPayUnsupportedNetwork decision:
_showError(
context,
S.of(context).unsupported_network_requested(decision.chainId.toString()),
);
return null;
case AnyPayUnsupportedToken():
_showError(context, S.of(context).unsupported_token_requested);
return null;
case AnyPayEmptyInput():
return null;
}
}
Future<AnyPayFormFill?> _pickEvmNetwork(BuildContext context, AnyPayRequest request) async {
final chains = evm!.getAllChains();
final networks = chains.map((chain) {
final walletType = evm!.getWalletTypeByChainId(chain.chainId) ?? sendViewModel.wallet.type;
return RecipientNetworkItem(
chainId: chain.chainId,
name: chain.name,
iconPath: getCryptoCurrencyIconForWalletListItem(walletType, chainId: chain.chainId),
chainBadgeIconPath: symbolIconPathForWalletType(walletType),
);
}).toList();
final selectedChainId =
await EvmAddressDetectedSheet.show(context: context, networks: networks);
if (!context.mounted || selectedChainId == null) {
return null;
}
final target = chains.firstWhere(
(chain) => chain.chainId == selectedChainId,
orElse: () => chains.first,
);
final evaluation = await anyPayService.evaluateForEvmChain(request, target.chainId);
if (!context.mounted) {
return null;
}
return handleEvaluation(context, evaluation, fallbackCurrency: target.currency);
}
Future<AnyPayFormFill?> _handleCrossChain(
BuildContext context,
AnyPayRequest request,
AnyPayCrossChainPayment decision,
CryptoCurrency? fallbackCurrency,
) async {
final destinationType = decision.targetWalletType;
final isEvmTarget = isEVMCompatibleChain(destinationType);
final destinationNetworkName = networkDisplayName(destinationType, decision.targetChainId);
final destinationNetworkIcon = symbolIconPathForWalletType(destinationType) ?? "";
if (!decision.canSwap && !decision.hasCompatibleWallet) {
_showError(
context,
S.of(context).swap_unavailable_needs_network_wallet(destinationNetworkName),
);
return null;
}
final currentType = sendViewModel.wallet.type;
final currentChainId = isEVMCompatibleChain(currentType) ? _currentEvmChainId() : null;
final currentNetworkName = networkDisplayName(currentType, currentChainId);
final currentNetworkIcon = symbolIconPathForWalletType(currentType) ?? "";
if (!decision.hasCompatibleWallet) {
final swapConfirmTitle = isEvmTarget
? S.of(context).swap_from_network(currentNetworkName)
: S.of(context).network_address_detected(destinationNetworkName);
final swapConfirmPrimary = isEvmTarget
? S.of(context).continue_to_swap
: S.of(context).swap_from_network(currentNetworkName);
await Navigator.of(context).push<void>(
CupertinoPageRoute(
builder: (pageContext) => NetworkDecisionPage(
title: swapConfirmTitle,
description: S.of(context).swap_from_network_description(
destinationNetworkName,
currentNetworkName,
),
destinationIconPath: destinationNetworkIcon,
currentIconPath: currentNetworkIcon,
primaryText: swapConfirmPrimary,
primaryIconPath: isEvmTarget ? null : "assets/new-ui/swap_arrows.svg",
onPrimary: () => _openSwap(pageContext, request, decision),
secondaryText: S.of(context).cancel,
),
),
);
return null;
}
final decisionTitle = isEvmTarget
? S.of(context).send_to_network(destinationNetworkName)
: S.of(context).network_address_detected(destinationNetworkName);
final selectedWallet = await Navigator.of(context).push<WalletInfo>(
CupertinoPageRoute(
builder: (pageContext) => NetworkDecisionPage(
title: decisionTitle,
description: decision.canSwap
? S.of(context).send_to_network_description(
destinationNetworkName,
currentNetworkName,
)
: S.of(context).swap_unavailable_switch_to_network(destinationNetworkName),
destinationIconPath: destinationNetworkIcon,
primaryText: S.of(context).switch_to_x_wallet(destinationNetworkName),
primaryIconPath: "assets/new-ui/wallet_filled.svg",
onPrimary: () => _pickDestinationWallet(pageContext, decision, destinationNetworkName),
secondaryText: decision.canSwap
? S.of(context).swap_from_network(currentNetworkName)
: S.of(context).cancel,
secondaryIconPath: decision.canSwap ? "assets/new-ui/swap_arrows.svg" : null,
onSecondary: decision.canSwap ? () => _openSwap(pageContext, request, decision) : null,
),
),
);
if (selectedWallet == null || !context.mounted) {
return null;
}
return _switchWallet(context, selectedWallet, request, decision, fallbackCurrency);
}
Future<void> _pickDestinationWallet(
BuildContext pageContext,
AnyPayCrossChainPayment decision,
String networkName,
) async {
WalletInfo? destinationWalletInfo;
if (decision.wallets.length > 1) {
destinationWalletInfo = await SwitchNetworkWalletPage.push(
context: pageContext,
networkName: networkName,
targetIconPath: symbolIconPathForWalletType(decision.targetWalletType) ?? "",
wallets: decision.wallets,
);
} else {
destinationWalletInfo = decision.wallets.first;
}
if (destinationWalletInfo == null || !pageContext.mounted) {
return;
}
Navigator.of(pageContext).pop(destinationWalletInfo);
}
Future<AnyPayFormFill?> _switchWallet(
BuildContext context,
WalletInfo walletInfo,
AnyPayRequest request,
AnyPayCrossChainPayment decision,
CryptoCurrency? fallbackCurrency,
) async {
if (request.isLightning) {
_enterLightningMode();
}
_isSwitchingWallet = true;
BuildContext? loadingSheetContext;
bool completedFlow = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!context.mounted || completedFlow) {
return;
}
showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (sheetContext) {
if (completedFlow) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (Navigator.canPop(sheetContext)) {
Navigator.of(sheetContext).pop();
}
});
return const SizedBox.shrink();
}
loadingSheetContext = sheetContext;
return LoadingBottomSheet(titleText: S.of(sheetContext).loading_your_wallet);
},
);
});
bool success = false;
try {
success = await anyPayService.switchWalletForPayment(
walletInfo,
chainId: decision.targetChainId,
);
} finally {
_isSwitchingWallet = false;
completedFlow = true;
if (loadingSheetContext != null &&
loadingSheetContext!.mounted &&
Navigator.canPop(loadingSheetContext!)) {
Navigator.of(loadingSheetContext!).pop();
}
loadingSheetContext = null;
}
if (!context.mounted) {
return null;
}
if (!success) {
_showError(context, S.of(context).network_switch_failed);
return null;
}
return AnyPayFormFill(
request: request,
token: decision.token,
fallbackCurrency: fallbackCurrency ?? request.detection.detectedCurrency,
amountOverride: decision.amountOverride,
);
}
Future<void> _openSwap(
BuildContext presentContext,
AnyPayRequest request,
AnyPayCrossChainPayment decision,
) async {
if (!presentContext.mounted) {
return;
}
if (request.hasContract && decision.token == null) {
_showError(presentContext, S.of(presentContext).unsupported_token_requested);
return;
}
final intent = anyPayService.buildSwapIntent(
request,
targetWalletType: decision.targetWalletType,
targetChainId: decision.targetChainId,
token: decision.token,
);
final page = NewSwapPage(
getIt.get<ExchangeViewModel>(),
authService,
getIt.get<AddressResolverService>(),
null,
walletSwitcherViewModel: walletSwitcherViewModel,
fromSend: SwapFromSendArgs.fromIntent(intent),
balanceViewModel: sendViewModel.balanceViewModel,
);
await Navigator.of(presentContext).push<void>(
CupertinoPageRoute(
builder: (context) => Material(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(bottom: false, child: page),
),
),
);
}
void _showError(BuildContext context, String message) {
if (!context.mounted) {
return;
}
showPopUp<void>(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: message,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
),
);
}
void _enterLightningMode() {
sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
}
int _currentEvmChainId() {
final wallet = sendViewModel.wallet;
return evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
}
}
@@ -5,31 +5,37 @@ import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:flutter/material.dart";
class SwapFromNetworkPage extends StatelessWidget {
const SwapFromNetworkPage({
class NetworkDecisionPage extends StatelessWidget {
const NetworkDecisionPage({
required this.title,
required this.primaryButtonText,
required this.destinationNetworkName,
required this.destinationNetworkIconPath,
required this.currentNetworkName,
required this.currentNetworkIconPath,
required this.onProceed,
required this.description,
required this.destinationIconPath,
required this.primaryText,
required this.onPrimary,
required this.secondaryText,
this.currentIconPath,
this.primaryIconPath,
this.secondaryIconPath,
this.onSecondary,
super.key,
this.primaryHasSwapIcon = false,
});
final String title;
final String primaryButtonText;
final bool primaryHasSwapIcon;
final String destinationNetworkName;
final String destinationNetworkIconPath;
final String currentNetworkName;
final String currentNetworkIconPath;
final VoidCallback onProceed;
final String description;
final String destinationIconPath;
final String? currentIconPath;
final String primaryText;
final VoidCallback onPrimary;
final String? primaryIconPath;
final String secondaryText;
final String? secondaryIconPath;
final VoidCallback? onSecondary;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final primaryIcon = primaryIconPath;
final secondaryIcon = secondaryIconPath;
return Material(
color: colors.surface,
child: SafeArea(
@@ -40,9 +46,6 @@ class SwapFromNetworkPage extends StatelessWidget {
leadingIcon: const Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).maybePop(),
trailingIcon: const Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => Navigator.of(context).maybePop(),
),
Expanded(
child: Center(
@@ -51,30 +54,11 @@ class SwapFromNetworkPage extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
CakeImageWidget(
imageUrl: currentNetworkIconPath,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(currentNetworkIconPath)
? colors.primary
: null,
),
Icon(Icons.arrow_forward, color: colors.primary, size: 28),
CakeImageWidget(
imageUrl: destinationNetworkIconPath,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationNetworkIconPath)
? colors.primary
: null,
),
],
ExcludeSemantics(
child: _DecisionHeader(
destinationIconPath: destinationIconPath,
currentIconPath: currentIconPath,
),
),
const SizedBox(height: 24),
Text(
@@ -88,10 +72,7 @@ class SwapFromNetworkPage extends StatelessWidget {
),
const SizedBox(height: 16),
Text(
S.of(context).swap_from_network_description(
destinationNetworkName,
currentNetworkName,
),
description,
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
@@ -109,23 +90,31 @@ class SwapFromNetworkPage extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
NewPrimaryButton(
onPressed: onProceed,
image: primaryHasSwapIcon
onPressed: onPrimary,
image: primaryIcon != null
? CakeImageWidget(
imageUrl: "assets/new-ui/swap_arrows.svg",
imageUrl: primaryIcon,
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
)
: null,
text: primaryButtonText,
text: primaryText,
color: colors.primary,
textColor: colors.onPrimary,
),
const SizedBox(height: 12),
NewPrimaryButton(
onPressed: () => Navigator.of(context).maybePop(),
text: S.of(context).cancel,
onPressed: onSecondary ?? () => Navigator.of(context).maybePop(),
image: secondaryIcon != null
? CakeImageWidget(
imageUrl: secondaryIcon,
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
)
: null,
text: secondaryText,
color: colors.surfaceContainer,
textColor: colors.primary,
),
@@ -138,3 +127,48 @@ class SwapFromNetworkPage extends StatelessWidget {
);
}
}
class _DecisionHeader extends StatelessWidget {
const _DecisionHeader({required this.destinationIconPath, this.currentIconPath});
final String destinationIconPath;
final String? currentIconPath;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final current = currentIconPath;
if (current == null) {
return CakeImageWidget(
imageUrl: destinationIconPath,
width: 75,
height: 75,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
);
}
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
CakeImageWidget(
imageUrl: current,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(current) ? colors.primary : null,
),
Icon(Icons.arrow_forward, color: colors.primary, size: 28),
CakeImageWidget(
imageUrl: destinationIconPath,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
),
],
);
}
}
@@ -1,46 +1,14 @@
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import "package:flutter/material.dart";
class RecipientNetworkSelector extends StatelessWidget {
const RecipientNetworkSelector({
super.key,
required this.wallet,
required this.onTap,
});
final WalletBase wallet;
final void Function(ChainInfo currentChain) onTap;
@override
Widget build(BuildContext context) {
if (evm == null) return const SizedBox.shrink();
final currentChainId =
evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
final currentChain = evm!.getChainInfoByChainId(currentChainId);
if (currentChain == null) {
printV("RecipientNetworkSelector: no chain info for chainId $currentChainId");
return const SizedBox.shrink();
}
return _RecipientNetworkRow(
networkName: currentChain.name,
networkIconPath: symbolIconPathForWalletType(wallet.type) ?? "",
onTap: () => onTap(currentChain),
);
}
}
class _RecipientNetworkRow extends StatelessWidget {
const _RecipientNetworkRow({
required this.networkName,
required this.networkIconPath,
required this.onTap,
super.key,
});
final String networkName;
@@ -50,50 +18,56 @@ class _RecipientNetworkRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
S.of(context).network_prefix_on,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
),
const SizedBox(width: 12),
CakeImageWidget(
imageUrl: networkIconPath,
width: 16,
height: 16,
fit: BoxFit.cover,
color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
),
const SizedBox(width: 8),
Text(
networkName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,
letterSpacing: -0.07,
color: colors.primary,
return Semantics(
button: true,
label: "${S.of(context).network_prefix_on} $networkName",
child: GestureDetector(
onTap: onTap,
child: ExcludeSemantics(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
S.of(context).network_prefix_on,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
),
const SizedBox(width: 12),
CakeImageWidget(
imageUrl: networkIconPath,
width: 16,
height: 16,
fit: BoxFit.cover,
color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
),
const SizedBox(width: 8),
Text(
networkName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,
letterSpacing: -0.07,
color: colors.primary,
),
),
const SizedBox(width: 8),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: colors.surfaceContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.keyboard_arrow_down,
size: 18,
color: colors.onSurfaceVariant,
),
),
],
),
const SizedBox(width: 8),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: colors.surfaceContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.keyboard_arrow_down,
size: 18,
color: colors.onSurfaceVariant,
),
),
],
),
),
);
}
@@ -1,122 +0,0 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:flutter/material.dart";
class SendToNetworkPage extends StatelessWidget {
const SendToNetworkPage({
required this.title,
required this.destinationNetworkName,
required this.destinationIconPath,
required this.currentNetworkName,
required this.onSwitchWallet,
required this.onSwap,
super.key,
});
final String title;
final String destinationNetworkName;
final String destinationIconPath;
final String currentNetworkName;
final VoidCallback onSwitchWallet;
final VoidCallback onSwap;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Material(
color: colors.surface,
child: SafeArea(
child: Column(
children: [
ModalTopBar(
title: "",
leadingIcon: const Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).maybePop(),
trailingIcon: const Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => Navigator.of(context).maybePop(),
),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CakeImageWidget(
imageUrl: destinationIconPath,
width: 75,
height: 75,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
),
const SizedBox(height: 24),
Text(
title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: -0.1,
),
),
const SizedBox(height: 16),
Text(
S.of(context).send_to_network_description(
destinationNetworkName,
currentNetworkName,
),
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NewPrimaryButton(
onPressed: onSwitchWallet,
image: CakeImageWidget(
imageUrl: "assets/new-ui/wallet_filled.svg",
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
),
text: S.of(context).switch_to_x_wallet(destinationNetworkName),
color: colors.primary,
textColor: colors.onPrimary,
),
const SizedBox(height: 12),
NewPrimaryButton(
onPressed: onSwap,
image: CakeImageWidget(
imageUrl: "assets/new-ui/swap_arrows.svg",
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
),
text: S.of(context).swap_from_network(currentNetworkName),
color: colors.surfaceContainer,
textColor: colors.primary,
),
],
),
),
],
),
),
);
}
}
+18 -13
View File
@@ -181,23 +181,28 @@ class ChangelogItemWidget extends StatelessWidget {
width: 36,
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
if (item.title.isNotEmpty)
Text(
item.title,
style: TextStyle(fontWeight: FontWeight.w500),
),
if (item.description.isNotEmpty)
Text(item.description,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
if (item.title.isNotEmpty)
Text(
item.title,
style: TextStyle(fontWeight: FontWeight.w500),
),
if (item.description.isNotEmpty)
Text(
item.description,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
))
],
),
)
],
),
)
],
);
}
}
@@ -162,7 +162,8 @@ class CoinActionRow extends StatelessWidget {
if (code == null || code.isEmpty) return;
late final PaymentRequest req;
PaymentRequest? req;
String? rawInput;
var unspentCoinType = UnspentCoinType.any;
if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) {
unspentCoinType = UnspentCoinType.lightning;
@@ -172,8 +173,6 @@ class CoinActionRow extends StatelessWidget {
unspentCoinType = UnspentCoinType.lightning;
final amount = (await LNURL.getPayRequestAmount(code))?.toString() ?? "0";
req = PaymentRequest(code, amount, "", "", "");
} else if (OpenCryptoPayService.isOpenCryptoPayQR(code)) {
req = PaymentRequest(code, "", "", "", "");
} else if (Uri.tryParse(code)?.scheme == "wc") {
if (!isWalletConnectCompatibleChain(walletType)) {
showPopUp<void>(
@@ -190,19 +189,19 @@ class CoinActionRow extends StatelessWidget {
Navigator.of(context)
.pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
return;
} else if (["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
} else if (!OpenCryptoPayService.isOpenCryptoPayQR(code) &&
["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);
rawInput = code;
}
final sendPage = getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: req,
initialRawInput: rawInput,
unspentCoinType: unspentCoinType,
),
);
@@ -10,18 +10,20 @@ class ChainIcon extends StatelessWidget {
{super.key,
required this.iconPath,
required this.dashboardViewModel,
required this.isSyncHeavy});
required this.isSyncHeavy,
required this.showSyncedMessage});
final String iconPath;
final bool isSyncHeavy;
final DashboardViewModel dashboardViewModel;
final bool showSyncedMessage;
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) {
final progress = dashboardViewModel.status.progress();
final done = !isSyncHeavy || progress >= 1;
final done = !showSyncedMessage && (!isSyncHeavy || progress >= 1);
return Stack(
children: [
@@ -30,10 +32,10 @@ class ChainIcon extends StatelessWidget {
opacity: done ? 0 : 1,
// Faded out means "nothing to report", so it must leave the tree too.
child: ExcludeSemantics(
excluding: done,
excluding: done || showSyncedMessage,
child: CircularProgressIndicator(
value: progress,
color: Color(0xFFFFB84E),
color: showSyncedMessage ? Color(0xFFFF12A439) : Color(0xFFFFB84E),
strokeWidth: 2,
semanticsLabel: S.of(context).synchronizing,
semanticsValue: "${(progress * 100).round()}%",
@@ -1,21 +1,26 @@
import 'package:cake_wallet/core/sync_status_title.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class SyncBar extends StatelessWidget {
SyncBar({super.key, required this.dashboardViewModel, required this.isSyncHeavy});
SyncBar({
super.key,
required this.dashboardViewModel,
required this.isSyncHeavy,
required this.showSyncedMessage,
});
final DashboardViewModel dashboardViewModel;
final bool isSyncHeavy;
final bool showSyncedMessage;
static const failStatuses = [
FailedSyncStatus,
@@ -37,9 +42,10 @@ class SyncBar extends StatelessWidget {
ConnectedSyncStatus,
];
static const syncedColor = Color(0xFF12A439);
@override
Widget build(BuildContext context) {
return Observer(
Widget build(BuildContext context) => Observer(
builder: (_) {
final status = dashboardViewModel.status;
final Widget? icon = _getIcon(context, status.runtimeType);
@@ -106,7 +112,6 @@ class SyncBar extends StatelessWidget {
);
},
);
}
void _openNodeManagement(BuildContext context) =>
CupertinoScaffold.showCupertinoModalBottomSheet(
@@ -130,8 +135,9 @@ class SyncBar extends StatelessWidget {
width: 20,
height: 20,
),
if (_showDot()) PulsingDot(),
],
if (_showDot()) const CupertinoActivityIndicator(radius: 8),
if(_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18),
],
);
final label = _joinLabels([
@@ -144,6 +150,9 @@ class SyncBar extends StatelessWidget {
return Semantics(label: label, child: ExcludeSemantics(child: row));
}
bool get _isShowingSyncedMessage =>
showSyncedMessage && dashboardViewModel.status.runtimeType == SyncedSyncStatus;
String _statusSemanticsLabel(BuildContext context, SyncStatus status) {
final isFailure = failStatuses.contains(status.runtimeType);
@@ -166,7 +175,7 @@ class SyncBar extends StatelessWidget {
}
Border? _getBorder(BuildContext context, Type status) {
if (progressStatuses.contains(status)) {
if (progressStatuses.contains(status) || _isShowingSyncedMessage) {
return Border.all(color: Theme.of(context).colorScheme.surfaceContainerHigh, width: 1);
}
@@ -174,19 +183,26 @@ class SyncBar extends StatelessWidget {
}
TextStyle? _getTextStyle(BuildContext context, Type status) {
final Color color;
if (failStatuses.contains(status)) {
return TextStyle(
fontSize: 12, fontWeight: FontWeight.w400, color: Theme.of(context).colorScheme.error);
color = Theme.of(context).colorScheme.error;
} else if (status == SyncedSyncStatus) {
color = syncedColor;
} else {
return TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant);
color = Theme.of(context).colorScheme.onSurfaceVariant;
}
return TextStyle(
fontSize: 12, fontWeight: FontWeight.w400, color: color);
}
Widget? _getIcon(BuildContext context, Type status) {
if (status == LostConnectionSyncStatus || status == FailedSyncStatus) {
if(status == SyncedSyncStatus) {
return Icon(Icons.check, color: syncedColor, size: 12);
}
if (status == LostConnectionSyncStatus) {
return CakeImageWidget(
imageUrl: "assets/new-ui/offline.svg",
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn),
@@ -228,11 +244,14 @@ class SyncBar extends StatelessWidget {
}
bool _showFullBar() {
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus) return false;
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus)
return isSyncHeavy && _isShowingSyncedMessage;
return isSyncHeavy || failStatuses.contains(dashboardViewModel.status.runtimeType);
}
bool _showDot() {
return !isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType);
}
bool _showDot() =>
!isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType);
bool _showLightSyncCheck() =>
!isSyncHeavy && _isShowingSyncedMessage;
}
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:cake_wallet/generated/i18n.dart';
@@ -6,11 +7,13 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_s
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class TopBar extends StatelessWidget {
class TopBar extends StatefulWidget {
const TopBar({
super.key,
required this.lightningMode,
@@ -25,33 +28,76 @@ class TopBar extends StatelessWidget {
final DashboardViewModel dashboardViewModel;
@override
Widget build(BuildContext context) {
return Padding(
State<TopBar> createState() => _TopBarState();
}
class _TopBarState extends State<TopBar> {
static const syncedMessageDuration = Duration(seconds: 3);
bool showSyncedMessage = false;
Timer? syncedMessageTimer;
late final ReactionDisposer? _statusReactionDisposer;
@override
void initState() {
super.initState();
_statusReactionDisposer = reaction(
(_) => widget.dashboardViewModel.status.runtimeType,
(status) {
syncedMessageTimer?.cancel();
if (status == SyncedSyncStatus) {
if(mounted) {
setState(() => showSyncedMessage = true);
}
syncedMessageTimer =
Timer(syncedMessageDuration, () => setState(() => showSyncedMessage = false));
} else {
if(mounted) {
setState(() => showSyncedMessage = false);
}
}
},
);
}
@override
void dispose() {
syncedMessageTimer?.cancel();
_statusReactionDisposer?.reaction.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.only(
bottom: 10, left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
child: Observer(
builder: (_) => Row(
spacing: 12,
children: [
(dashboardViewModel.hasLightning)
(widget.dashboardViewModel.hasLightning)
? LightningSwitcher(
lightningMode: lightningMode,
onLightningSwitchPress: onLightningSwitchPress,
lightningMode: widget.lightningMode,
onLightningSwitchPress: widget.onLightningSwitchPress,
)
: ChainIcon(
iconPath: dashboardViewModel.wallet.currency.flatIconPath ?? "",
dashboardViewModel: dashboardViewModel,
isSyncHeavy: dashboardViewModel.isSyncHeavy),
iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage),
SyncBar(
dashboardViewModel: dashboardViewModel,
isSyncHeavy: dashboardViewModel.isSyncHeavy,
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
ModernButton.svg(
iconColor: Theme.of(context).colorScheme.primary,
size: 36,
onPressed: () {
HapticFeedback.mediumImpact();
onSettingsButtonPress();
widget.onSettingsButtonPress();
},
svgPath: "assets/new-ui/top-settings.svg",
semanticLabel: S.of(context).settings_title,
@@ -60,7 +106,6 @@ class TopBar extends StatelessWidget {
),
),
);
}
//FIXME remove after this gets fixed flutter-side
double _additionalTopPadding(BuildContext context) {
@@ -18,6 +18,22 @@ class AnyPaySwapFooter extends StatelessWidget {
final textTheme = Theme.of(context).textTheme;
return Observer(
builder: (_) {
if (exchangeViewModel.noProviderForPair) {
return Container(
alignment: Alignment.center,
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
border: Border.all(color: colors.error),
),
child: Text(
S.of(context).no_providers_available,
style: textTheme.bodyMedium?.copyWith(color: colors.error, letterSpacing: -0.07),
),
);
}
if (exchangeViewModel.depositAmount.isEmpty) {
return const SizedBox.shrink();
}
+157 -205
View File
@@ -8,6 +8,7 @@ import "package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart";
import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/refund_address_modal.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/swap_address_selection_modal.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/swap_source_selector.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/utils/decimal_input_formatter.dart";
import "package:cake_wallet/utils/permission_handler.dart";
@@ -82,10 +83,14 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
final fiatAmountController = TextEditingController();
final amountFocusNode = FocusNode();
final memoController = TextEditingController();
final _amountFieldKey = GlobalKey<FormFieldState<String>>();
mobx.ReactionDisposer? _memoReactionDisposer;
@override
void initState() {
amountController
.addListener(() => _amountFieldKey.currentState?.didChange(amountController.text));
if (widget.isReceiverCard) {
memoController.text = widget.exchangeViewModel.receiveAddressExtraId;
@@ -145,95 +150,13 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
final colors = Theme.of(context).colorScheme;
if (widget.sourceSelectorMode) {
final available = _sourceBalance()?.amount.split(" ").first ?? "—";
return Column(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: _presentCurrencyPicker,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
CakeImageWidget(
imageUrl: widget.currency.iconPath ?? "",
width: 24,
height: 24,
),
const SizedBox(width: 8),
Text(
currencyToShow,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(letterSpacing: -0.08),
),
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIconPath,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
const Spacer(),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/wallet_filled.svg",
width: 16,
height: 16,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
const SizedBox(width: 4),
Flexible(
child: Text(
widget.walletName ?? "",
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.onSurfaceVariant,
),
),
),
],
),
),
const SizedBox(width: 8),
Text(
"${S.of(context).avl}: $available",
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.primary,
),
),
],
),
),
],
return SwapSourceSelector(
currencyIconPath: widget.currency.iconPath ?? "",
currencyLabel: currencyToShow,
chainIconPath: chainIconPath,
walletName: widget.walletName,
availableBalance: _sourceBalance()?.amount.split(" ").first ?? "—",
onTap: _presentCurrencyPicker,
);
}
@@ -260,139 +183,168 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
child: Column(
spacing: 12,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: amountFocusNode.requestFocus,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 4,
FormField<String>(
key: _amountFieldKey,
initialValue: amountController.text,
validator: _fiatInputMode ? null : widget.currencyValueValidator,
builder: (state) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: amountFocusNode.requestFocus,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: IntrinsicWidth(
child: Observer(
builder: (_) {
final showFetching = !widget.isReceiverCard &&
widget.exchangeViewModel.isFixedRateMode &&
widget.exchangeViewModel.receiveAmount.isNotEmpty &&
widget.exchangeViewModel.depositAmount.isEmpty;
return TextFormField(
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: !widget.useBaseUnit,
Expanded(
child: Row(
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: IntrinsicWidth(
child: Observer(
builder: (_) {
final showFetching = !widget.isReceiverCard &&
widget.exchangeViewModel.isFixedRateMode &&
widget.exchangeViewModel.receiveAmount.isNotEmpty &&
widget.exchangeViewModel.depositAmount.isEmpty;
return TextField(
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: !widget.useBaseUnit,
),
onChanged: state.didChange,
controller: _fiatInputMode
? fiatAmountController
: amountController,
focusNode: amountFocusNode,
style: TextStyle(
fontSize: 28,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
isDense: true,
hintText: showFetching ? S.of(context).fetching : "0",
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
inputFormatters: <TextInputFormatter>[
DecimalInputFormatter(
maxDecimals: widget.useBaseUnit
? 0
: widget.currency.decimals,
),
],
);
},
),
validator:
_fiatInputMode ? null : widget.currencyValueValidator,
controller:
_fiatInputMode ? fiatAmountController : amountController,
focusNode: amountFocusNode,
style: TextStyle(
fontSize: 28,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
isDense: true,
hintText: showFetching ? S.of(context).fetching : "0",
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
inputFormatters: <TextInputFormatter>[
DecimalInputFormatter(
maxDecimals:
widget.useBaseUnit ? 0 : widget.currency.decimals,
),
),
if (_fiatInputMode)
Center(
child: Text(
widget.exchangeViewModel.fiat.title,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
},
),
),
),
],
),
),
if (_fiatInputMode)
Center(
child: Text(
widget.exchangeViewModel.fiat.title,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
GestureDetector(
onTap: _presentCurrencyPicker,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(999999),
),
child: Padding(
padding:
const EdgeInsets.only(top: 4, bottom: 4, left: 4, right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CakeImageWidget(
imageUrl: widget.currency.iconPath ?? "",
width: 28,
height: 28,
),
const SizedBox(width: 10),
Text(
currencyToShow,
textAlign: TextAlign.center,
),
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIconPath,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
const SizedBox(width: 6),
] else
const SizedBox(width: 10),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999999999),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: Padding(
padding: const EdgeInsets.all(4),
child: RotatedBox(
quarterTurns: 2,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg",
width: 4,
height: 4,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
),
),
const SizedBox(width: 4),
],
),
),
),
),
],
),
),
GestureDetector(
onTap: _presentCurrencyPicker,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(999999),
),
child: Padding(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 4, right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CakeImageWidget(
imageUrl: widget.currency.iconPath ?? "",
width: 28,
height: 28,
),
const SizedBox(width: 10),
Text(
currencyToShow,
textAlign: TextAlign.center,
),
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIconPath,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
const SizedBox(width: 6),
] else
const SizedBox(width: 10),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999999999),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: Padding(
padding: const EdgeInsets.all(4),
child: RotatedBox(
quarterTurns: 2,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg",
width: 4,
height: 4,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
),
),
const SizedBox(width: 4),
],
if (state.hasError)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Semantics(
container: true,
liveRegion: true,
label: "${S.of(context).amount}${state.errorText!}",
excludeSemantics: true,
child: Text(
state.errorText!,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.error,
),
),
),
),
),
],
),
),
@@ -1,4 +1,4 @@
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
@@ -8,13 +8,18 @@ class SwapFromSendArgs {
required this.recipientAddress,
required this.receiveCurrency,
required this.targetWalletType,
this.depositBalanceByAsset,
this.receiveAmount,
});
factory SwapFromSendArgs.fromIntent(AnyPaySwapIntent intent) => SwapFromSendArgs(
recipientAddress: intent.recipientAddress,
receiveCurrency: intent.receiveCurrency,
targetWalletType: intent.targetWalletType,
receiveAmount: intent.receiveAmount,
);
final String recipientAddress;
final CryptoCurrency receiveCurrency;
final WalletType targetWalletType;
final Map<CryptoCurrency, CurrencyPickerBalance>? depositBalanceByAsset;
final Money? receiveAmount;
}
@@ -0,0 +1,118 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:flutter/material.dart";
class SwapSourceSelector extends StatelessWidget {
const SwapSourceSelector({
required this.currencyIconPath,
required this.currencyLabel,
required this.availableBalance,
required this.onTap,
this.chainIconPath,
this.walletName,
super.key,
});
final String currencyIconPath;
final String currencyLabel;
final String availableBalance;
final VoidCallback onTap;
final String? chainIconPath;
final String? walletName;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final chainIcon = chainIconPath;
return Column(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: onTap,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
CakeImageWidget(
imageUrl: currencyIconPath,
width: 24,
height: 24,
),
const SizedBox(width: 8),
Text(
currencyLabel,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(letterSpacing: -0.08),
),
if (chainIcon != null && chainIcon.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIcon,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
const Spacer(),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/wallet_filled.svg",
width: 16,
height: 16,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
const SizedBox(width: 4),
Flexible(
child: Text(
walletName ?? "",
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.onSurfaceVariant,
),
),
),
],
),
),
const SizedBox(width: 8),
Text(
S.of(context).available_balance_short(availableBalance),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.primary,
),
),
],
),
),
],
);
}
}
+12 -8
View File
@@ -28,20 +28,24 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore
try {
final connectivityResult = await (Connectivity().checkConnectivity());
int? chainId;
if (isEVMCompatibleChain(wallet.type)) {
chainId = evm!.getSelectedChainId(wallet);
}
final node = settingsStore.getCurrentNode(wallet.type, chainId: chainId);
if (connectivityResult.contains(ConnectivityResult.none)) {
wallet.syncStatus = FailedSyncStatus();
return;
// Still try node even when OS says no network - it might be a LAN node.
if (!await node.requestNode()) {
wallet.syncStatus = FailedSyncStatus();
return;
}
}
if (wallet.type != WalletType.bitcoin &&
(wallet.syncStatus is LostConnectionSyncStatus ||
wallet.syncStatus is FailedSyncStatus)) {
int? chainId;
if (isEVMCompatibleChain(wallet.type)) {
chainId = evm!.getSelectedChainId(wallet);
}
final node = settingsStore.getCurrentNode(wallet.type, chainId: chainId);
final alive = await node.requestNode();
if (alive) {
+4 -5
View File
@@ -149,7 +149,6 @@ import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_mode
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
@@ -445,6 +444,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.send:
final args = settings.arguments as Map<String, dynamic>?;
final initialPaymentRequest = args?['paymentRequest'] as PaymentRequest?;
final initialRawInput = args?["rawLink"] as String?;
final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?;
return handleRouteWithPlatformAwareness(
@@ -452,6 +452,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
child: getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: initialPaymentRequest,
initialRawInput: initialRawInput,
unspentCoinType: coinTypeToSpendFrom ?? UnspentCoinType.any),
),
),
@@ -473,7 +474,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.newReceivePage:
if (FeatureFlag.hasNewUi) {
return handleRouteWithPlatformAwareness(
(context) => Material(child: getIt.get<NewReceivePage>(param1: false, param2: null)),
(context) => Material(child: getIt.get<NewReceivePage>(param1: false, param2: null)),
settings: settings,
);
}
@@ -835,9 +836,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
final useTestnet = args['useTestnet'] as bool;
final toggleTestnet = args['toggleTestnet'] as Function(bool? val);
final zcashNetwork = args['zcashNetwork'] as int? ?? ZcashNetworkType.mainnet;
final setZcashNetwork =
args['setZcashNetwork'] as void Function(int network)? ?? (_) {};
final restoredWallet = args['restoredWallet'] as RestoredWallet?;
final setZcashNetwork = args['setZcashNetwork'] as void Function(int network)? ?? (_) {};
final viewModelParam = {'type': type, 'isPow': false};
@@ -120,6 +120,8 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
}
if (widget.hardwareWalletVM.hasBluetooth) {
widget.hardwareWalletVM.updateBleState();
_longWaitTimer = Timer(const Duration(seconds: 10), () {
if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) {
setState(() => longWait = true);
+25
View File
@@ -125,6 +125,31 @@ class PaymentRequest {
);
}
PaymentRequest copyWith({
String? address,
String? amount,
String? note,
String? scheme,
String? pjUri,
String? callbackUrl,
String? callbackMessage,
String? contractAddress,
int? chainId,
String? rawTokenAmount,
}) =>
PaymentRequest(
address ?? this.address,
amount ?? this.amount,
note ?? this.note,
scheme ?? this.scheme,
pjUri ?? this.pjUri,
callbackUrl: callbackUrl ?? this.callbackUrl,
callbackMessage: callbackMessage ?? this.callbackMessage,
contractAddress: contractAddress ?? this.contractAddress,
chainId: chainId ?? this.chainId,
rawTokenAmount: rawTokenAmount ?? this.rawTokenAmount,
);
final String address;
final String amount;
final String note;
+121 -89
View File
@@ -8,6 +8,7 @@ import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/spl_token.dart';
import 'package:cw_core/tron_token.dart';
import "package:cw_core/utils/print_verbose.dart";
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
@@ -25,10 +26,10 @@ class TokenUtilities {
for (final wallet in evmWallets) {
final chain = getTokenNameBasedOnWalletType(wallet.type);
final box = await _openEvmTokensBoxFor(wallet);
final tokens = await Erc20Token.getAllForWallet(wallet.name, _getDefaultChainId(wallet.type));
for (final t in box.values.where((t) => t.enabled)) {
final key = '$chain|${t.contractAddress.toLowerCase()}';
for (final t in tokens.where((t) => t.enabled)) {
final key = "$chain|${t.contractAddress.toLowerCase()}";
if (seen.add(key)) {
unique.add(t);
}
@@ -46,15 +47,17 @@ class TokenUtilities {
final tokens = <SPLToken>[];
for (final wallet in solWallets) {
final box = await _openSolTokensBoxFor(wallet);
tokens.addAll(box.values.where((t) => t.enabled));
final walletTokens = await SPLToken.getAllForWallet(wallet.name);
tokens.addAll(walletTokens.where((t) => t.enabled));
}
final seen = <String>{};
final unique = <SPLToken>[];
for (final token in tokens) {
final key = token.mintAddress.toLowerCase();
if (seen.add(key)) unique.add(token);
if (seen.add(key)) {
unique.add(token);
}
}
return unique;
}
@@ -68,25 +71,31 @@ class TokenUtilities {
final seen = <String>{};
final unique = <TronToken>[];
for (final wallet in tronWallets) {
final box = await _openTronTokensBoxFor(wallet);
for (final t in box.values.where((t) => t.enabled)) {
final walletTokens = await TronToken.getAllForWallet(wallet.name);
for (final t in walletTokens.where((t) => t.enabled)) {
final key = t.contractAddress.toLowerCase();
if (seen.add(key)) unique.add(t);
if (seen.add(key)) {
unique.add(t);
}
}
}
return unique;
}
static List<Erc20Token> loadDefaultEvmTokensForSwap() {
if (evm == null) return [];
if (evm == null) {
return [];
}
final tokens = <Erc20Token>[];
final seen = <String>{};
for (final chain in evm!.getAllChains()) {
for (final token in evm!.getDefaultTokensByChainId(chain.chainId)) {
final key = '${chain.chainId}|${token.contractAddress.toLowerCase()}';
if (seen.add(key)) tokens.add(token);
final key = "${chain.chainId}|${token.contractAddress.toLowerCase()}";
if (seen.add(key)) {
tokens.add(token);
}
}
}
@@ -108,7 +117,9 @@ class TokenUtilities {
for (final t in [...defaultTokens, ...userTokens]) {
final key = '${t.tag ?? 'ETH'}|${t.contractAddress.toLowerCase()}';
if (seen.add(key)) result.add(t);
if (seen.add(key)) {
result.add(t);
}
}
return result;
@@ -123,7 +134,9 @@ class TokenUtilities {
for (final t in [...defaultTokens, ...userTokens]) {
final key = t.mintAddress.toLowerCase();
if (seen.add(key)) result.add(t);
if (seen.add(key)) {
result.add(t);
}
}
return result;
@@ -138,7 +151,9 @@ class TokenUtilities {
for (final t in [...defaultTokens, ...userTokens]) {
final key = t.contractAddress.toLowerCase();
if (seen.add(key)) result.add(t);
if (seen.add(key)) {
result.add(t);
}
}
return result;
@@ -152,56 +167,69 @@ class TokenUtilities {
required WalletType walletType,
required String address,
}) async {
if (address.isEmpty) return null;
if (address.isEmpty) {
return null;
}
final lower = address.toLowerCase();
final tokens = await getAvailableTokensForNetwork(walletType);
for (final t in tokens) {
if (t is Erc20Token && t.contractAddress.toLowerCase() == lower) return t;
if (t is SPLToken && t.mintAddress.toLowerCase() == lower) return t;
if (t is TronToken && t.contractAddress.toLowerCase() == lower) return t;
if (t is Erc20Token && t.contractAddress.toLowerCase() == lower) {
return t;
}
if (t is SPLToken && t.mintAddress.toLowerCase() == lower) {
return t;
}
if (t is TronToken && t.contractAddress.toLowerCase() == lower) {
return t;
}
}
return null;
}
static Future<Box<Erc20Token>> _openEvmTokensBoxFor(WalletInfo walletInfo) async {
final walletKey = walletInfo.name.replaceAll(' ', '_');
final boxName = _getErc20TokensBoxName(walletKey, walletInfo.type);
if (CakeHive.isBoxOpen(boxName)) {
return CakeHive.box<Erc20Token>(boxName);
}
return CakeHive.openBox<Erc20Token>(boxName);
}
static String _getErc20TokensBoxName(String walletKey, WalletType walletType) {
return switch (walletType) {
WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}',
WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}',
WalletType.base => '${walletKey}_${Erc20Token.baseBoxName}',
WalletType.arbitrum => '${walletKey}_${Erc20Token.arbitrumBoxName}',
WalletType.bsc => '${walletKey}_${Erc20Token.bscBoxName}',
_ => '${walletKey}_${Erc20Token.ethereumBoxName}',
static int _getDefaultChainId(WalletType walletType) => switch (walletType) {
WalletType.ethereum => 1,
WalletType.polygon => 137,
WalletType.base => 8453,
WalletType.arbitrum => 42161,
WalletType.bsc => 56,
_ => 1,
};
}
static Future<Box<SPLToken>> _openSolTokensBoxFor(WalletInfo wallet) async {
final boxName = '${wallet.name.replaceAll(' ', '_')}_${SPLToken.boxName}';
if (CakeHive.isBoxOpen(boxName)) {
return CakeHive.box<SPLToken>(boxName);
static Future<int?> findEvmChainIdForContract(
String contractAddress, {
int? excludingChainId,
}) async {
if (evm == null || contractAddress.isEmpty) {
return null;
}
return CakeHive.openBox<SPLToken>(boxName);
}
static Future<Box<TronToken>> _openTronTokensBoxFor(WalletInfo walletInfo) async {
final boxName = '${walletInfo.name.replaceAll(' ', '_')}_${TronToken.boxName}';
if (CakeHive.isBoxOpen(boxName)) {
return CakeHive.box<TronToken>(boxName);
try {
for (final chain in evm!.getAllChains()) {
if (chain.chainId == excludingChainId) {
continue;
}
final walletType = evm!.getWalletTypeByChainId(chain.chainId);
if (walletType == null) {
continue;
}
final token = await findTokenByAddress(walletType: walletType, address: contractAddress);
if (token != null) {
return chain.chainId;
}
}
} catch (e) {
printV("findEvmChainIdForContract failed: $e");
}
return CakeHive.openBox<TronToken>(boxName);
return null;
}
static Erc20Token? findErc20Token(CryptoCurrency currency, WalletBase wallet) {
if (currency is Erc20Token) return currency;
if (currency is Erc20Token) {
return currency;
}
// More of a fallback for us
for (final balanceCurrency in wallet.balance.keys) {
@@ -214,10 +242,14 @@ class TokenUtilities {
}
static Erc20Token? findErc20TokenForSwap(CryptoCurrency currency) {
if (currency is Erc20Token) return currency;
if (currency is Erc20Token) {
return currency;
}
for (final token in loadDefaultEvmTokensForSwap()) {
if (_matchesCurrency(token, currency)) return token;
if (_matchesCurrency(token, currency)) {
return token;
}
}
return null;
}
@@ -244,19 +276,19 @@ class TokenUtilities {
final title = currency.title.toLowerCase();
final tag = currency.tag?.toLowerCase();
return title == 'eth' ||
title == 'ethereum' ||
title == 'matic' ||
title == 'polygon' ||
title == 'base' ||
title == 'arbitrum' ||
title == 'bnb' ||
title == 'bsc' ||
title == 'avax' ||
title == 'avalanche' ||
tag == 'polygon' ||
tag == 'bsc' ||
tag == 'avalanche';
return title == "eth" ||
title == "ethereum" ||
title == "matic" ||
title == "polygon" ||
title == "base" ||
title == "arbitrum" ||
title == "bnb" ||
title == "bsc" ||
title == "avax" ||
title == "avalanche" ||
tag == "polygon" ||
tag == "bsc" ||
tag == "avalanche";
}
static int getChainId(CryptoCurrency currency) {
@@ -266,37 +298,37 @@ class TokenUtilities {
// Only check EVM registry for currencies that might be EVM-related
final isPotentialEVM = isNativeToken(currency) ||
(tag != null &&
(tag == 'ETH' || tag == 'POL' || tag == 'BASE' || tag == 'ARB' || tag == 'BSC'));
(tag == "ETH" || tag == "POL" || tag == "BASE" || tag == "ARB" || tag == "BSC"));
if (isPotentialEVM) {
// Try by tag first if available (e.g., 'POL', 'BASE', 'ARB')
if (tag != null) {
final chainId = evm?.getChainIdByTag(tag);
if (chainId != null) return chainId;
if (chainId != null) {
return chainId;
}
}
// Try by title (case-insensitive)
final titleChainId = evm?.getChainIdByTitle(title);
if (titleChainId != null) return titleChainId;
if (titleChainId != null) {
return titleChainId;
}
}
// Fallback to hardcoded values for chains not in registry yet
// Avalanche C-Chain
if (title == 'avalanche' || title == 'avax' || tag == 'AVALANCHE') {
if (title == "avalanche" || title == "avax" || tag == "AVALANCHE") {
return 43114;
}
// Optimism
if (title == 'optimism' || title == 'op' || tag == 'OPTIMISM') {
if (title == "optimism" || title == "op" || tag == "OPTIMISM") {
return 10;
}
// Fantom Opera
if (title == 'fantom' || title == 'ftm' || tag == 'FANTOM') {
if (title == "fantom" || title == "ftm" || tag == "FANTOM") {
return 250;
}
// Default to Ethereum mainnet
return 1;
}
@@ -363,7 +395,7 @@ class TokenUtilities {
}
for (final currency in CryptoCurrency.all) {
if (currency.tag?.toLowerCase() == 'sol') {
if (currency.tag?.toLowerCase() == "sol") {
if (currency is SPLToken) {
final mintAddress = currency.mintAddress.toLowerCase();
if (addedAddresses.add(mintAddress)) {
@@ -374,10 +406,7 @@ class TokenUtilities {
}
}
}
}
// Handle Tron network
else if (network == WalletType.tron) {
} else if (network == WalletType.tron) {
final userTronTokens = await loadAllUniqueTronTokens();
for (final token in userTronTokens) {
final contractAddress = token.contractAddress.toLowerCase();
@@ -394,7 +423,7 @@ class TokenUtilities {
}
for (final currency in CryptoCurrency.all) {
if (currency.tag?.toLowerCase() == 'trx') {
if (currency.tag?.toLowerCase() == "trx") {
if (currency is TronToken) {
final contractAddress = currency.contractAddress.toLowerCase();
if (addedAddresses.add(contractAddress)) {
@@ -410,31 +439,34 @@ class TokenUtilities {
return allTokens;
}
static bool _matchesCurrency(CryptoCurrency a, CryptoCurrency b) {
return a.title.toUpperCase() == b.title.toUpperCase() &&
(a.tag?.toUpperCase() == b.tag?.toUpperCase());
}
static bool _matchesCurrency(CryptoCurrency a, CryptoCurrency b) =>
a.title.toUpperCase() == b.title.toUpperCase() &&
(a.tag?.toUpperCase() == b.tag?.toUpperCase());
static Future<List<CryptoCurrency>> _getUserTokensForNetwork(CryptoCurrency baseCurrency) async {
final walletType = cryptoCurrencyOrTokenToWalletType(baseCurrency);
if (walletType == null) return [];
if (walletType == null) {
return [];
}
if (isEVMCompatibleChain(walletType)) {
final tokens = await TokenUtilities.loadAllUniqueEvmTokens();
return tokens.where((token) {
if (baseCurrency.tag == null) return token.tag == baseCurrency.title;
if (baseCurrency.tag == null) {
return token.tag == baseCurrency.title;
}
return token.tag?.toLowerCase() == baseCurrency.tag?.toLowerCase();
}).toList();
}
if (walletType == WalletType.solana) {
return await loadAllUniqueSolTokens();
return loadAllUniqueSolTokens();
}
if (walletType == WalletType.tron) {
return await loadAllUniqueTronTokens();
return loadAllUniqueTronTokens();
}
return [];
@@ -203,7 +203,7 @@ class TransactionListItem extends ActionListItem with Keyable {
case WalletType.decred:
case WalletType.zcash:
amount = calculateFiatAmountRaw(
cryptoAmount: transaction.amount.toDouble(),
cryptoAmount: double.parse(transaction.amount.toString()),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
case WalletType.ethereum:
@@ -214,15 +214,15 @@ class TransactionListItem extends ActionListItem with Keyable {
final asset = assetOfTransaction;
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: transaction.amount.toDouble(),
cryptoAmount: double.parse(transaction.amount.toString()),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
case WalletType.solana:
final asset = assetOfTransaction;
final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction);
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: transaction.amount.toDouble(),
cryptoAmount: double.parse(transaction.amount.toString()),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
@@ -230,7 +230,7 @@ class TransactionListItem extends ActionListItem with Keyable {
final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction);
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: transaction.amount.toDouble(),
cryptoAmount: double.parse(transaction.amount.toString()),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
@@ -242,7 +242,7 @@ class TransactionListItem extends ActionListItem with Keyable {
}
final price = balanceViewModel.fiatConversionStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: transaction.amount.toDouble(),
cryptoAmount: double.parse(transaction.amount.toString()),
price: price,
).withLocalSeperator(_appStore.settingsStore.languageCode);
break;
@@ -2,12 +2,10 @@ import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:developer';
import 'dart:math' show min;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/create_trade_result.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
@@ -415,6 +413,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@observable
bool tradeStarted = false;
@observable
bool noProviderForPair = false;
@observable
bool isSendFromExternal = false;
@@ -980,6 +981,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
bestRate = _sortedAvailableProviders.keys.first;
bestRateProvider = _sortedAvailableProviders.values.first;
}
noProviderForPair = _sortedAvailableProviders.isEmpty;
}
@action
@@ -1603,24 +1605,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
}
String? _addressTypeValidation(String refundAddress, String receiveAddress) {
final isRefundAddressSP =
RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(refundAddress);
if (isRefundAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_refund}';
final isReceiveAddressSP =
RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(receiveAddress);
if (isReceiveAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_receive}';
final isRefundAddressMWEB = RegExp(AddressValidator.mWebAddressPattern).hasMatch(refundAddress);
if (isRefundAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_refund}';
final isReceiveAddressMWEB =
RegExp(AddressValidator.mWebAddressPattern).hasMatch(receiveAddress);
if (isReceiveAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_receive}';
return null;
}
void _defineIsReceiveAmountEditable() {
/*if ((provider is ChangeNowExchangeProvider)
&&(depositCurrency == CryptoCurrency.xmr)
@@ -42,8 +42,6 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store {
}
});
updateBleState();
if (!Platform.isIOS) {
ledgerPlusUSB = sdk.LedgerInterface.usb();
}
@@ -39,7 +39,6 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S
_initBLE();
}
});
updateBleState();
if (!Platform.isIOS) {
trezorUSB = sdk.TrezorInterface.usb();
+21 -2
View File
@@ -25,6 +25,10 @@ class LinkViewModel {
Uri? currentLink;
bool readyToOpenPage = false;
final StreamController<Uri> _incomingPaymentLinksController = StreamController<Uri>.broadcast();
Stream<Uri> get incomingPaymentLinks => _incomingPaymentLinksController.stream;
bool get _isValidPaymentUri => currentLink?.path.isNotEmpty ?? false;
bool get isWalletConnectLink => currentLink?.authority == 'wc';
bool get isNanoGptLink => currentLink?.scheme == 'nano-gpt';
@@ -96,7 +100,10 @@ class LinkViewModel {
}
if (_isValidPaymentUri) {
return {"paymentRequest": PaymentRequest.fromUri(currentLink)};
return {
"paymentRequest": PaymentRequest.fromUri(currentLink),
"rawLink": currentLink.toString(),
};
}
return null;
@@ -132,8 +139,20 @@ class LinkViewModel {
return;
}
// Prevent navigating to the same route again.
if (route == Routes.send &&
!isQuickActionLink &&
currentLink != null &&
_incomingPaymentLinksController.hasListener) {
final link = currentLink!;
currentLink = null;
_incomingPaymentLinksController.add(link);
return;
}
// Prevent navigating to the same route again and clear the link so a later auth
// event cannot refire it.
if (appStore.currentRouteName == route) {
currentLink = null;
return;
}
@@ -11,6 +11,7 @@ import 'package:cake_wallet/view_model/restore/restore_mode.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:collection/collection.dart';
@@ -89,9 +90,7 @@ class WalletRestoreFromQRCode {
try {
return AddressResolverUtils.extractAddressByType(
raw: rawString,
type: walletTypeToCryptoCurrency(
type,
),
type: walletTypeToCryptoCurrency(type),
requireSurroundingWhitespaces: false,
);
} catch (_) {
@@ -127,8 +126,8 @@ class WalletRestoreFromQRCode {
final prefix = code.startsWith('xpub')
? 'xpub'
: code.startsWith('zpub')
? 'zpub'
: '????';
? 'zpub'
: '????';
if (walletType == null) {
await _specifyWalletAssets(context, "Can't determine wallet type, please pick it manually");
walletType =
@@ -140,8 +139,8 @@ class WalletRestoreFromQRCode {
formattedUri = seedPhrase != null
? '$walletType:?seed=$seedPhrase'
: code.startsWith(prefix)
? '$walletType:?$prefix=$code'
: throw Exception('Failed to determine valid seed phrase');
? '$walletType:?$prefix=$code'
: throw Exception('Failed to determine valid seed phrase');
} else {
final index = code.indexOf(':');
final query = code.substring(index + 1).replaceAll('?', '&');
@@ -202,9 +201,15 @@ class WalletRestoreFromQRCode {
seedValue.split(' ').forEach((element) {
if (!words.contains(element)) {
throw Exception(
"Unexpected restore mode: mnemonic_seed is invalid or doesn't match wallet type");
"Unexpected restore mode: mnemonic_seed is invalid or doesn't match wallet type",
);
}
});
if (isEVMCompatibleChain(type) && !bip39.validateMnemonic(seedValue)) {
throw Exception(
'EVM mnemonic has an invalid checksum. Please check the seed phrase for typos.',
);
}
return WalletRestoreMode.seed;
}
@@ -268,12 +273,14 @@ class WalletRestoreFromQRCode {
Future<void> _specifyWalletAssets(BuildContext context, String error) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.error,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.current.error,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
},
);
}
+10 -5
View File
@@ -126,9 +126,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
super(appStore: _appStore) {
outputs.add(Output(wallet, _appStore, _fiatConversationStore, _outputCryptoCurrencyHandler));
unspentCoinsListViewModel
.initialSetup();
// .then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections());
unspentCoinsListViewModel.initialSetup();
// .then((_) => unspentCoinsListViewModel.resetUnspentCoinsInfoSelections());
reaction((_) {
if (isEVMCompatibleChain(wallet.type)) {
@@ -1718,9 +1717,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
String? payjoinUri;
@action
Future<void> fetchTokenForContractAddress(String contractAddress) async {
Future<void> fetchTokenForContractAddress(String contractAddress,
{WalletType? walletType}) async {
final token = await TokenUtilities.findTokenByAddress(
walletType: wallet.type,
walletType: walletType ?? wallet.type,
address: contractAddress,
);
@@ -1729,6 +1729,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
}
@action
void applyAnyPayCurrency(CryptoCurrency currency) {
selectedCryptoCurrency = currency;
}
String _decodeMethodSelector(String s) =>
(s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
}