mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Merge branch 'dev' into cw-1440-new-buy-sell-flow
This commit is contained in:
@@ -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]+';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -42,7 +42,7 @@ String syncStatusTitle(SyncStatus syncStatus, SyncStatusDisplayMode syncStatusDi
|
||||
}
|
||||
|
||||
if (syncStatus is SyncedSyncStatus) {
|
||||
return "";
|
||||
return S.current.sync_synced;
|
||||
}
|
||||
|
||||
if (syncStatus is FailedSyncStatus) {
|
||||
|
||||
@@ -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
@@ -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>(),
|
||||
|
||||
@@ -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
@@ -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 =
|
||||
|
||||
+203
-659
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+87
-53
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) : '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user