CW1612: AnyPay Service Layer (#3516)
What changed, and why it matters
This is a large feature commit that introduces a new 'AnyPay' service layer for routing cryptocurrency payments and swaps inside Cake Wallet. It refactors how the app parses payment requests, detects recipient networks, switches wallets, and handles deep links. The changes are mostly architectural and user-facing, but because they touch sensitive flows such as wallet switching, address parsing, and cross-chain swaps, they could introduce security bugs if the new routing logic makes incorrect decisions. The commit does not appear to be a disclosed security fix, and no CVE or vendor security statement is present.
Treat this as a high-touch refactor of payment-critical code. Review the AnyPay routing decisions against a matrix of wallet types, chains, and URI schemes; verify that fallback paths cannot be abused to send funds to the wrong network or wallet; test deep-link re-application under race conditions; and confirm that wallet switching failures are handled safely without leaving the user on an unintended wallet/chain. No immediate patch is indicated, but regression and security testing is warranted before release.
Security signals we found
New automatic wallet switching and chain selection logic in AnyPayService and WalletSwitchService
Address/network detection now centralised in AnyPayRouter/AnyPayResolver with fallback behaviours
Deep links can re-apply to an open send flow, including popping navigation
Swap flow receives recipient address and currency from parsed payment request
Broad exception catch in AnyPayService._evaluate falls back to applying request to current wallet
bitcoin_base dependency pinned to a specific commit hash
New Solana URL scheme handlers registered in Android manifest and iOS plist
Evidence from the diff
The commit adds a new AnyPay core module (models, parser, resolver, router, service), a WalletSwitchService, and refactors the send page and swap page to route payment requests through AnyPay. It registers additional Solana URL schemes, pins the bitcoin_base dependency to a specific git ref, improves Zcash address detection, and adds tests for the new components. Security-relevant areas include: (1) address/network detection logic that decides which wallet/chain should receive a payment; (2) automatic wallet switching with chain selection and balance refresh; (3) deep-link handling that can re-apply a payment link to an already-open send flow; (4) swap flow construction that passes recipient address and currency; and (5) exception handling that falls back to applying the request to the current wallet on any failure. The diff does not show obvious injection or theft bugs, but the complexity and broad reach mean regressions or logic errors could have security impact.
Changed components
lib/core/anypay/anypay_service.dartlib/core/anypay/anypay_router.dartlib/core/anypay/anypay_resolver.dartlib/core/anypay/anypay_parser.dartlib/core/anypay/anypay_models.dartlib/new-ui/services/wallet_switch_service.dartlib/new-ui/pages/send_page.dartlib/new-ui/pages/swap_page.dartlib/new-ui/widgets/anypay/anypay_flow.dartandroid/app/src/main/AndroidManifestBase.xmlios/Runner/InfoBase.plistInspect captured patch +2739 / −1277
diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml
index 0f15c75..a1d72a6 100644
--- a/android/app/src/main/AndroidManifestBase.xml
+++ b/android/app/src/main/AndroidManifestBase.xml
@@ -90,7 +90,9 @@
<data android:scheme="polygon" />
<data android:scheme="polygon-wallet" />
<data android:scheme="polygon_wallet" />
+ <data android:scheme="solana" />
<data android:scheme="solana-wallet" />
+ <data android:scheme="solana_wallet" />
<data android:scheme="tron" />
<data android:scheme="tron-wallet" />
<data android:scheme="tron_wallet" />
diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml
index 174bf4a..46a8992 100644
--- a/cw_bitcoin/pubspec.yaml
+++ b/cw_bitcoin/pubspec.yaml
@@ -88,7 +88,7 @@ dependency_overrides:
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
- ref: cake-update-v11
+ ref: bda0e2904effa799ec835b4578cc3fc5d31caa78
pointycastle: 3.7.4
ffi: 2.1.0
intl: any
diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml
index ab6c595..5d0c46d 100644
--- a/cw_bitcoin_cash/pubspec.yaml
+++ b/cw_bitcoin_cash/pubspec.yaml
@@ -43,7 +43,7 @@ dependency_overrides:
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
- ref: cake-update-v11
+ ref: bda0e2904effa799ec835b4578cc3fc5d31caa78
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart
index b3f711b..4954cad 100644
--- a/cw_core/lib/crypto_currency.dart
+++ b/cw_core/lib/crypto_currency.dart
@@ -1007,6 +1007,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
'bitcoin': btc,
'bitcoincash': bch,
'polygon': maticpoly,
+ 'solana': sol,
'nano-gpt': nano,
'secret': scrt,
'stellar': xlm,
diff --git a/cw_core/lib/exceptions.dart b/cw_core/lib/exceptions.dart
index 8460569..cc0729e 100644
--- a/cw_core/lib/exceptions.dart
+++ b/cw_core/lib/exceptions.dart
@@ -70,3 +70,12 @@ class WalletDeprecationException implements Exception {
WalletDeprecationException({required this.seed, required this.curr});
}
+
+class WalletSwitchException implements Exception {
+ WalletSwitchException(this.message);
+
+ final String message;
+
+ @override
+ String toString() => message;
+}
diff --git a/cw_dogecoin/pubspec.yaml b/cw_dogecoin/pubspec.yaml
index e217596..a93ea37 100644
--- a/cw_dogecoin/pubspec.yaml
+++ b/cw_dogecoin/pubspec.yaml
@@ -40,7 +40,7 @@ dependency_overrides:
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
- ref: cake-update-v11
+ ref: bda0e2904effa799ec835b4578cc3fc5d31caa78
# For information on the generic Dart part of this file, see the
diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist
index ff0f43b..a29c3d5 100644
--- a/ios/Runner/InfoBase.plist
+++ b/ios/Runner/InfoBase.plist
@@ -199,7 +199,9 @@
<string>solana-wallet</string>
<key>CFBundleURLSchemes</key>
<array>
+ <string>solana</string>
<string>solana-wallet</string>
+ <string>solana_wallet</string>
</array>
</dict>
<dict>
diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart
index e5905bb..1c4c6d3 100644
--- a/lib/core/address_validator.dart
+++ b/lib/core/address_validator.dart
@@ -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]+';
diff --git a/lib/core/anypay/anypay_models.dart b/lib/core/anypay/anypay_models.dart
new file mode 100644
index 0000000..70f334f
--- /dev/null
+++ b/lib/core/anypay/anypay_models.dart
@@ -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;
+}
diff --git a/lib/core/anypay/anypay_parser.dart b/lib/core/anypay/anypay_parser.dart
new file mode 100644
index 0000000..04afeb0
--- /dev/null
+++ b/lib/core/anypay/anypay_parser.dart
@@ -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();
+ }
+}
diff --git a/lib/core/anypay/anypay_resolver.dart b/lib/core/anypay/anypay_resolver.dart
new file mode 100644
index 0000000..18a0400
--- /dev/null
+++ b/lib/core/anypay/anypay_resolver.dart
@@ -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);
+}
diff --git a/lib/core/anypay/anypay_router.dart b/lib/core/anypay/anypay_router.dart
new file mode 100644
index 0000000..cf24876
--- /dev/null
+++ b/lib/core/anypay/anypay_router.dart
@@ -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;
+ }
+}
diff --git a/lib/core/anypay/anypay_service.dart b/lib/core/anypay/anypay_service.dart
new file mode 100644
index 0000000..80b558f
--- /dev/null
+++ b/lib/core/anypay/anypay_service.dart
@@ -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;
+ }
+}
diff --git a/lib/core/universal_address_detector.dart b/lib/core/universal_address_detector.dart
index 1a88e6e..74b632c 100644
--- a/lib/core/universal_address_detector.dart
+++ b/lib/core/universal_address_detector.dart
@@ -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}$'),
diff --git a/lib/di.dart b/lib/di.dart
index da00561..cd27c53 100644
--- a/lib/di.dart
+++ b/lib/di.dart
@@ -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';
@@ -55,12 +56,11 @@ import 'package:cake_wallet/new-ui/new_dashboard.dart';
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
-import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
-import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
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';
@@ -169,7 +169,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';
@@ -193,7 +192,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';
@@ -250,7 +248,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';
@@ -926,7 +923,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>(),
);
});
@@ -1291,6 +1289,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>(),
diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart
index b9a83c7..0b0c51d 100644
--- a/lib/new-ui/pages/send_page.dart
+++ b/lib/new-ui/pages/send_page.dart
@@ -1,11 +1,13 @@
-import "package:cake_wallet/core/address_resolver/address_resolver_service.dart";
+import "dart:async";
+
import "package:cake_wallet/core/address_resolver/parsed_address.dart";
import "package:cake_wallet/core/address_validator.dart";
+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_service.dart";
import "package:cake_wallet/core/auth_service.dart";
import "package:cake_wallet/core/execution_state.dart";
import "package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart";
-import "package:cake_wallet/core/universal_address_detector.dart";
-import "package:cake_wallet/di.dart";
import "package:cake_wallet/entities/contact_record.dart";
import "package:cake_wallet/entities/priority_for_wallet_type.dart";
import "package:cake_wallet/evm/evm.dart";
@@ -14,14 +16,10 @@ import "package:cake_wallet/main.dart";
import "package:cake_wallet/monero/monero.dart";
import "package:cake_wallet/new-ui/modal_navigator.dart";
import "package:cake_wallet/new-ui/pages/coin_control_page.dart";
-import "package:cake_wallet/new-ui/pages/swap_page.dart";
import "package:cake_wallet/new-ui/widgets/animated_dropdown.dart";
-import "package:cake_wallet/new-ui/widgets/anypay/evm_address_detected_sheet.dart";
+import "package:cake_wallet/new-ui/widgets/anypay/anypay_flow.dart";
import "package:cake_wallet/new-ui/widgets/anypay/recipient_network_row.dart";
import "package:cake_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
-import "package:cake_wallet/new-ui/widgets/anypay/send_to_network_page.dart";
-import "package:cake_wallet/new-ui/widgets/anypay/swap_from_network_page.dart";
-import "package:cake_wallet/new-ui/widgets/anypay/switch_network_wallet_page.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart";
@@ -40,22 +38,17 @@ import "package:cake_wallet/new-ui/widgets/send_page/send_amount_input.dart";
import "package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart";
import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart";
import "package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.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/routes.dart" show Routes;
import "package:cake_wallet/src/screens/connect_device/connect_device_page.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/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart";
import "package:cake_wallet/src/widgets/standard_checkbox.dart";
-import "package:cake_wallet/store/app_store.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
-import "package:cake_wallet/utils/token_utilities.dart";
import "package:cake_wallet/view_model/contact_list/contact_list_view_model.dart";
-import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
-import "package:cake_wallet/view_model/payment/payment_view_model.dart";
+import "package:cake_wallet/view_model/link_view_model.dart";
import "package:cake_wallet/view_model/send/output.dart";
import "package:cake_wallet/view_model/send/send_view_model.dart";
import "package:cake_wallet/view_model/send/send_view_model_state.dart";
@@ -66,11 +59,9 @@ import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/erc20_token.dart";
import "package:cw_core/lnurl.dart";
-import "package:cw_core/payment_uris.dart";
import "package:cw_core/transaction_priority.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:cw_core/wallet_type.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
@@ -169,12 +160,14 @@ class SendPageModes {
class SendPageParams {
SendPageParams({
this.initialPaymentRequest,
+ this.initialRawInput,
SendPageModes? mode,
this.unspentCoinType = UnspentCoinType.any,
this.initialCurrency,
}) : mode = mode ?? SendPageModes.normal;
final PaymentRequest? initialPaymentRequest;
+ final String? initialRawInput;
final SendPageModes mode;
final CryptoCurrency? initialCurrency;
final UnspentCoinType unspentCoinType;
@@ -183,13 +176,15 @@ class SendPageParams {
class NewSendPage extends StatefulWidget {
NewSendPage({
required this.sendViewModel,
- required this.paymentViewModel,
+ required this.anyPayService,
+ required this.linkViewModel,
required this.walletSwitcherViewModel,
required this.contactListViewModel,
required this.authService,
required SendPageParams params,
super.key,
}) : initialPaymentRequest = params.initialPaymentRequest,
+ initialRawInput = params.initialRawInput,
mode = params.mode {
if (params.initialCurrency != null) {
sendViewModel.selectedCryptoCurrency = params.initialCurrency!;
@@ -197,11 +192,13 @@ class NewSendPage extends StatefulWidget {
}
final SendViewModel sendViewModel;
- final PaymentViewModel paymentViewModel;
+ final AnyPayService anyPayService;
+ final LinkViewModel linkViewModel;
final WalletSwitcherViewModel walletSwitcherViewModel;
final ContactListViewModel contactListViewModel;
final AuthService authService;
final PaymentRequest? initialPaymentRequest;
+ final String? initialRawInput;
final SendPageModes mode;
@override
@@ -216,8 +213,14 @@ class _NewSendPageState extends State<NewSendPage> {
final _memoControllers = <TextEditingController>[];
final _formKey = GlobalKey<FormState>();
final _addressFocusNode = FocusNode();
- BuildContext? loadingBottomSheetContext;
- BuildContext? dialogContext;
+ final _disposers = <ReactionDisposer>[];
+ StreamSubscription<Uri>? _deepLinkSubscription;
+ late final AnyPayFlow _anyPayFlow = AnyPayFlow(
+ anyPayService: widget.anyPayService,
+ sendViewModel: widget.sendViewModel,
+ authService: widget.authService,
+ walletSwitcherViewModel: widget.walletSwitcherViewModel,
+ );
ContactRecord? newContactAddress;
bool _justHandledPasteButton = false;
@@ -227,67 +230,64 @@ class _NewSendPageState extends State<NewSendPage> {
super.initState();
_addInputControllers();
- reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, (all) {
- if (all) {
- widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = false;
- _amountControllers[_selectedOutput].text = S.current.all;
- }
- });
+ _disposers.add(
+ reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, (all) {
+ if (all) {
+ widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = false;
+ _amountControllers[_selectedOutput].text = S.current.all;
+ }
+ }),
+ );
- reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, (address) {
- if (_addressControllers[_selectedOutput].text != address) {
- _addressControllers[_selectedOutput].text = address;
- }
- });
+ _disposers.add(
+ reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, (address) {
+ if (_addressControllers[_selectedOutput].text != address) {
+ _addressControllers[_selectedOutput].text = address;
+ }
+ }),
+ );
- reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (memo) {
- if (memo != _memoControllers[_selectedOutput].text) {
- _memoControllers[_selectedOutput].text = memo;
- }
- });
+ _disposers.add(
+ reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (memo) {
+ if (memo != _memoControllers[_selectedOutput].text) {
+ _memoControllers[_selectedOutput].text = memo;
+ }
+ }),
+ );
- if (widget.initialPaymentRequest != null) {
- if (_isInitialRequestTypeSameAsCurrentWallet()) {
- final contractAddress = widget.initialPaymentRequest!.contractAddress;
- if (contractAddress != null && contractAddress.isNotEmpty) {
- WidgetsBinding.instance.addPostFrameCallback((_) async {
- if (!mounted) {
- return;
- }
- await _applyPaymentSelectingCurrency(widget.initialPaymentRequest!, null);
- });
- } else {
- _addressControllers[0].text = widget.initialPaymentRequest!.address;
- _applyNote(widget.initialPaymentRequest!.note, 0);
- _amountControllers[0].text = widget.initialPaymentRequest!.amount;
+ if (widget.initialRawInput != null || widget.initialPaymentRequest != null) {
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
+ if (!mounted) {
+ return;
}
- } else {
- WidgetsBinding.instance.addPostFrameCallback(
- (timeStamp) {
- if (mounted) {
- final paymentRequest = widget.initialPaymentRequest!;
- final scheme = paymentRequest.scheme.toLowerCase();
- final String uri;
- if (scheme.isEmpty || scheme == "lightning") {
- uri = paymentRequest.address;
- } else if (scheme == "ethereum") {
- uri = ERC681URI(
- address: paymentRequest.address,
- amount: paymentRequest.amount,
- contractAddress: paymentRequest.contractAddress,
- chainId: paymentRequest.chainId ?? _currentEvmChainIdOrMainnet(),
- rawTokenAmount: paymentRequest.rawTokenAmount,
- ).toString();
- } else {
- final amount =
- paymentRequest.amount.isNotEmpty ? "?amount=${paymentRequest.amount}" : "";
- uri = "${paymentRequest.scheme}:${paymentRequest.address}$amount";
- }
- _handlePaymentFlow(uri, paymentRequest);
- }
- },
+ await _runAnyPayFlow(
+ rawInput: widget.initialRawInput,
+ paymentRequest: widget.initialPaymentRequest,
);
- }
+ });
+ }
+
+ if (widget.mode == SendPageModes.normal) {
+ _deepLinkSubscription = widget.linkViewModel.incomingPaymentLinks.listen((link) async {
+ if (!mounted) {
+ return;
+ }
+
+ final state = widget.sendViewModel.state;
+ if (_anyPayFlow.isSwitchingWallet ||
+ state is IsExecutingState ||
+ state is TransactionCommitting) {
+ printV("dropping incoming payment link while the send flow is busy");
+ return;
+ }
+
+ final route = ModalRoute.of(context);
+ if (route != null && !route.isCurrent) {
+ Navigator.of(context).popUntil((r) => r == route);
+ }
+
+ await _runAnyPayFlow(rawInput: link.toString());
+ });
}
_addressFocusNode.addListener(() async {
@@ -298,6 +298,25 @@ class _NewSendPageState extends State<NewSendPage> {
});
}
+ @override
+ void dispose() {
+ _deepLinkSubscription?.cancel();
+ for (final disposer in _disposers) {
+ disposer();
+ }
+ for (final controller in _amountControllers) {
+ controller.dispose();
+ }
+ for (final controller in _addressControllers) {
+ controller.dispose();
+ }
+ for (final controller in _memoControllers) {
+ controller.dispose();
+ }
+ _addressFocusNode.dispose();
+ super.dispose();
+ }
+
Future<void> _resolveAddressForOutput(Output output) async {
final result = await widget.sendViewModel.resolveAddressForOutput(output);
if (result == null || !mounted) {
@@ -326,6 +345,9 @@ class _NewSendPageState extends State<NewSendPage> {
Widget build(BuildContext context) => Observer(
builder: (_) {
final output = widget.sendViewModel.outputs[_selectedOutput];
+ final recipientChain = widget.sendViewModel.isEVMWallet
+ ? evm!.getChainInfoByChainId(_currentEvmChainIdOrMainnet())
+ : null;
return SafeArea(
bottom: false,
child: KeyboardHideOverlay(
@@ -451,11 +473,7 @@ class _NewSendPageState extends State<NewSendPage> {
output.resetParsedAddress();
await _resolveAddressForOutput(output);
- // Process the payment through the new flow
- await _handlePaymentFlow(
- uri.toString(),
- PaymentRequest.fromString(uri.toString()),
- );
+ await _runAnyPayFlow(rawInput: uri.toString());
},
onEditingComplete: _addressFocusNode.unfocus,
onPushAddressBookButton: (_) {
@@ -477,16 +495,7 @@ class _NewSendPageState extends State<NewSendPage> {
? output.extractedAddress
: output.address;
- await _handlePaymentFlow(
- address,
- PaymentRequest(
- address,
- _amountControllers[_selectedOutput].text,
- _memoControllers[_selectedOutput].text,
- "",
- null,
- ),
- );
+ await _runAnyPayPaste(address);
} finally {
_justHandledPasteButton = false;
}
@@ -497,10 +506,17 @@ class _NewSendPageState extends State<NewSendPage> {
widget.sendViewModel.selectedCryptoCurrency,
),
if (widget.sendViewModel.isEVMWallet &&
- _hasEvmRecipient(output))
+ _hasEvmRecipient(output) &&
+ recipientChain != null)
RecipientNetworkSelector(
- wallet: widget.sendViewModel.wallet,
- onTap: _presentRecipientNetworkPicker,
+ networkName: recipientChain.name,
+ networkIconPath: symbolIconPathForWalletType(
+ widget.sendViewModel.wallet.type,
+ ) ??
+ "",
+ onTap: () => _presentRecipientNetworkPicker(
+ recipientChain,
+ ),
),
],
),
@@ -1031,555 +1047,124 @@ class _NewSendPageState extends State<NewSendPage> {
}
Future<void> _handleManualNetworkSelection(ChainInfo target) async {
- final address = _addressControllers[_selectedOutput].text.trim();
+ final output = widget.sendViewModel.outputs[_selectedOutput];
+ final address = output.isParsedAddress
+ ? output.extractedAddress
+ : _addressControllers[_selectedOutput].text.trim();
final note = _memoControllers[_selectedOutput].text;
final isTokenSelected = widget.sendViewModel.selectedCryptoCurrency is Erc20Token;
final amount = isTokenSelected ? "" : _amountControllers[_selectedOutput].text;
- final paymentRequest = PaymentRequest(address, amount, note, "", null);
- await _handleEvmNetworkFlow(target, paymentRequest);
- }
-
- bool _isInitialRequestTypeSameAsCurrentWallet() {
- final req = widget.initialPaymentRequest;
- if (req == null) {
- return false;
- }
-
- final currentType = widget.sendViewModel.wallet.type;
- if (evm != null && isEVMCompatibleChain(currentType)) {
- final targetChainId = _evmTargetChainId(req);
- if (targetChainId != null) {
- return targetChainId == _currentEvmChainIdOrMainnet();
- }
- }
- return widget.sendViewModel.walletCurrencyName == req.scheme.toLowerCase();
- }
+ final request =
+ AnyPayParser.fromPaymentRequest(PaymentRequest(address, amount, note, "", null));
- int? _evmTargetChainId(PaymentRequest req) {
- final scheme = req.scheme.toLowerCase();
- if (scheme == "ethereum") {
- if (req.chainId != null) {
- return req.chainId;
- }
-
- return _currentEvmChainIdOrMainnet();
- }
- try {
- return getChainIdByCryptoCurrency(CryptoCurrency.fromString(scheme));
- } catch (_) {
- return null;
+ final evaluation = await widget.anyPayService.evaluateForEvmChain(request, target.chainId);
+ if (!mounted) {
+ return;
}
+ await _handleEvaluation(evaluation, fallbackCurrency: target.currency);
}
int _currentEvmChainIdOrMainnet() {
final wallet = widget.sendViewModel.wallet;
- if (evm != null && isEVMCompatibleChain(wallet.type)) {
+ if (isEVMCompatibleChain(wallet.type)) {
return evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
}
return 1;
}
- Future<void> _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async {
- final isEip681 = uri.toLowerCase().startsWith("ethereum:");
- if (!isEip681 && (uri.contains("@") || paymentRequest.address.contains("@"))) {
- return;
- }
-
- if (OpenCryptoPayService.isOpenCryptoPayQR(uri) &&
- widget.sendViewModel.selectedCryptoCurrency != CryptoCurrency.btcln) {
- final request = await widget.sendViewModel.getOpenCryptoPayRequest(uri);
- if (request == null) {
- return;
- }
- _applyPaymentRequest(request);
- return;
- }
-
- try {
- final result = await widget.paymentViewModel.processAddress(uri);
-
- if (paymentRequest.contractAddress != null) {
- await widget.sendViewModel.fetchTokenForContractAddress(paymentRequest.contractAddress!);
- }
-
- // This automatically switches to lightning mode if you are in a bitcoin wallet
- if (result.addressDetectionResult?.detectedCurrency == CryptoCurrency.btcln) {
- widget.sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
- widget.sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
- }
-
- switch (result.type) {
- case PaymentFlowType.singleWallet:
- case PaymentFlowType.multipleWallets:
- case PaymentFlowType.noWallets:
- await _showPaymentConfirmation(
- widget.paymentViewModel,
- widget.walletSwitcherViewModel,
- paymentRequest,
- result,
- );
- break;
- case PaymentFlowType.evmNetworkSelection:
- if (result.chainId != null && evm!.getChainInfoByChainId(result.chainId!) == null) {
- _showUnsupportedNetworkAlert(result.chainId!);
- return;
- }
-
- final targetChainId =
- paymentRequest.scheme.isNotEmpty ? _evmTargetChainId(paymentRequest) : null;
- final targetChain =
- targetChainId != null ? evm!.getChainInfoByChainId(targetChainId) : null;
- final currentChainId = isEVMCompatibleChain(widget.sendViewModel.wallet.type)
- ? _currentEvmChainIdOrMainnet()
- : null;
- final isCrossChain = targetChain != null && targetChain.chainId != currentChainId;
-
- if (isCrossChain) {
- await _handleEvmNetworkFlow(targetChain, paymentRequest);
- } else if (widget.sendViewModel.isEVMWallet) {
- await _applyPaymentSelectingCurrency(paymentRequest, null);
- } else {
- await _showEvmNetworkPicker(paymentRequest, result.walletType);
- }
- break;
- case PaymentFlowType.solanaTokenSelection:
- case PaymentFlowType.tronTokenSelection:
- await _showPaymentConfirmation(
- widget.paymentViewModel,
- widget.walletSwitcherViewModel,
- paymentRequest,
- result,
- );
- break;
- case PaymentFlowType.currentWalletCompatible:
- case PaymentFlowType.error:
- case PaymentFlowType.incompatible:
- _applyPaymentRequest(paymentRequest);
- break;
- }
- } catch (e) {
- printV("Payment flow error: $e");
- _applyPaymentRequest(paymentRequest);
- }
- }
-
- void _showUnsupportedNetworkAlert(int chainId) {
- if (!mounted) {
- return;
- }
-
- showPopUp<void>(
- context: context,
- builder: (context) => AlertWithOneAction(
- alertTitle: S.of(context).error,
- alertContent: S.of(context).unsupported_network_requested(chainId.toString()),
- buttonText: S.of(context).ok,
- buttonAction: () => Navigator.of(context).pop(),
- ),
- );
- }
-
- Future<void> _showPaymentConfirmation(
- PaymentViewModel paymentViewModel,
- WalletSwitcherViewModel walletSwitcherViewModel,
- PaymentRequest paymentRequest,
- PaymentFlowResult result,
- ) async {
- if (!mounted || result.walletType == null) {
- return;
- }
-
- final destinationType = result.walletType!;
- final isEvmTarget = isEVMCompatibleChain(destinationType);
- final destinationChainId = result.chainId;
- final destinationNetworkName = networkDisplayName(destinationType, destinationChainId);
- final destinationNetworkIcon = symbolIconPathForWalletType(destinationType) ?? "";
-
- final currentType = widget.sendViewModel.wallet.type;
- final currentChainId =
- isEVMCompatibleChain(currentType) && evm != null ? _currentEvmChainIdOrMainnet() : null;
- final currentNetworkName = networkDisplayName(currentType, currentChainId);
- final currentNetworkIcon = symbolIconPathForWalletType(currentType) ?? "";
-
- final hasSingleWallet =
- result.type == PaymentFlowType.singleWallet || result.wallets.length == 1;
- final hasMultipleWallets =
- result.type == PaymentFlowType.multipleWallets || result.wallets.length > 1;
- final hasWallet = hasSingleWallet || hasMultipleWallets || result.wallet != null;
-
- final decisionTitle = isEvmTarget
- ? S.of(context).send_to_network(destinationNetworkName)
- : S.of(context).network_address_detected(destinationNetworkName);
- 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);
-
- if (hasWallet) {
- await Navigator.of(context).push<void>(
- CupertinoPageRoute(
- builder: (pageContext) => SendToNetworkPage(
- title: decisionTitle,
- destinationNetworkName: destinationNetworkName,
- destinationIconPath: destinationNetworkIcon,
- currentNetworkName: currentNetworkName,
- onSwitchWallet: () => _onSwitchWalletSelected(
- pageContext,
- result,
- paymentRequest,
- destinationNetworkName,
- destinationNetworkIcon,
- hasMultipleWallets,
- ),
- onSwap: () => _onSwapSelected(pageContext, result, paymentRequest),
- ),
- ),
- );
- } else {
- await Navigator.of(context).push<void>(
- CupertinoPageRoute(
- builder: (pageContext) => SwapFromNetworkPage(
- title: swapConfirmTitle,
- primaryButtonText: swapConfirmPrimary,
- primaryHasSwapIcon: !isEvmTarget,
- destinationNetworkName: destinationNetworkName,
- destinationNetworkIconPath: destinationNetworkIcon,
- currentNetworkName: currentNetworkName,
- currentNetworkIconPath: currentNetworkIcon,
- onProceed: () => _onSwapSelected(pageContext, result, paymentRequest),
- ),
- ),
- );
- }
- }
-
- Future<void> _onSwapSelected(
- BuildContext pageContext,
- PaymentFlowResult result,
- PaymentRequest paymentRequest,
- ) async {
- await _handleSwapFlow(result, pageContext, paymentRequest);
- }
-
- Future<void> _onSwitchWalletSelected(
- BuildContext pageContext,
- PaymentFlowResult result,
- PaymentRequest paymentRequest,
- String destName,
- String destIcon,
- bool hasMultipleWallets,
- ) async {
- WalletInfo? destinationWalletInfo;
- if (hasMultipleWallets) {
- destinationWalletInfo = await SwitchNetworkWalletPage.push(
- context: pageContext,
- networkName: destName,
- targetIconPath: destIcon,
- wallets: result.wallets,
- );
- if (destinationWalletInfo == null) {
- return;
- }
- } else {
- destinationWalletInfo =
- result.wallet ?? (result.wallets.isNotEmpty ? result.wallets.first : null);
- }
-
- if (destinationWalletInfo == null) {
- return;
- }
-
- // We may have awaited the wallet picker above, so make sure the decision page
- // is still around before we reach into its navigator.
- if (!pageContext.mounted) {
- return;
- }
- if (Navigator.of(pageContext).canPop()) {
- Navigator.of(pageContext).pop();
- }
- if (!mounted) {
- return;
- }
- await _completeWalletSwitch(destinationWalletInfo, result, paymentRequest);
- }
-
- Future<void> _applyPaymentSelectingCurrency(
- PaymentRequest paymentRequest,
- CryptoCurrency? fallbackCurrency,
- ) async {
- String? amountOverride;
- final contract = paymentRequest.contractAddress;
- if (contract != null && contract.isNotEmpty) {
- final walletType = widget.sendViewModel.wallet.type;
- final lookupType = evm != null && isEVMCompatibleChain(walletType)
- ? (evm!.getWalletTypeByChainId(_currentEvmChainIdOrMainnet()) ?? walletType)
- : walletType;
- final token = await TokenUtilities.findTokenByAddress(
- walletType: lookupType,
- address: contract,
- );
- if (!mounted) {
- return;
- }
-
- if (token == null) {
- final rerouted = await _rerouteChainlessContractPayment(paymentRequest);
- if (rerouted || !mounted) {
- return;
- }
- _showUnsupportedTokenAlert();
- return;
- }
- await widget.sendViewModel.fetchTokenForContractAddress(contract, walletType: lookupType);
- amountOverride = paymentRequest.resolveTokenAmount(token);
- } else if (fallbackCurrency != null) {
- widget.sendViewModel.setSelectedCryptoCurrency(fallbackCurrency.title);
- if (paymentRequest.amount.isEmpty) {
- widget.sendViewModel.outputs[_selectedOutput].setCryptoAmount("");
- _amountControllers[_selectedOutput].clear();
- }
- }
- if (!mounted) {
- return;
- }
- _applyPaymentRequest(paymentRequest, amountOverride: amountOverride);
- }
-
- Future<bool> _rerouteChainlessContractPayment(PaymentRequest paymentRequest) async {
- if (evm == null || paymentRequest.chainId != null) {
+ Future<bool> _handleIfOpenCryptoPay(String input) async {
+ if (!OpenCryptoPayService.isOpenCryptoPayQR(input) ||
+ widget.sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln) {
return false;
}
- if (paymentRequest.scheme.toLowerCase() != "ethereum") {
- return false;
- }
-
- final contract = paymentRequest.contractAddress;
- if (contract == null || contract.isEmpty) {
- return false;
+ if (_selectedOutput != 0) {
+ setState(() => _selectedOutput = 0);
}
- final currentChainId = isEVMCompatibleChain(widget.sendViewModel.wallet.type)
- ? _currentEvmChainIdOrMainnet()
- : null;
-
- // QRs from old app versions omit the chainId on mainnet, so a contract the current
- // network does not know may still belong to another EVM network
- final chainId = await TokenUtilities.findEvmChainIdForContract(
- contract,
- excludingChainId: currentChainId,
- );
- if (chainId == null || !mounted) {
- return false;
- }
-
- final targetChain = evm!.getChainInfoByChainId(chainId);
- if (targetChain == null) {
- return false;
+ final request = await widget.sendViewModel.getOpenCryptoPayRequest(input);
+ if (request != null && mounted) {
+ _applyPaymentRequest(request);
}
-
- printV("chainless contract payment rerouted to chainId $chainId");
- await _handleEvmNetworkFlow(targetChain, paymentRequest);
return true;
}
- Future<void> _completeWalletSwitch(
- WalletInfo wallet,
- PaymentFlowResult result,
- PaymentRequest paymentRequest,
- ) async {
- widget.walletSwitcherViewModel.selectWallet(wallet);
- final success = await widget.walletSwitcherViewModel.switchToSelectedWallet();
- if (!success) {
- _showNetworkSwitchFailedAlert();
- return;
- }
- if (!mounted) {
- return;
- }
+ Future<void> _runAnyPayFlow({String? rawInput, PaymentRequest? paymentRequest}) async {
+ assert(rawInput != null || paymentRequest != null);
- bool completedFlow = false;
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (!mounted || completedFlow) {
- return;
- }
- showModalBottomSheet<void>(
- context: context,
- isDismissible: false,
- builder: (BuildContext sheetContext) {
- if (completedFlow) {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- if (Navigator.canPop(sheetContext)) {
- Navigator.of(sheetContext).pop();
- }
- });
- return const SizedBox.shrink();
- }
- loadingBottomSheetContext = sheetContext;
- return LoadingBottomSheet(titleText: S.of(sheetContext).loading_your_wallet);
- },
- );
- });
-
- bool chainSwitchFailed = false;
- try {
- if (evm != null &&
- isEVMCompatibleChain(widget.sendViewModel.wallet.type) &&
- result.chainId != null) {
- final appStore = getIt.get<AppStore>();
- final node = appStore.settingsStore
- .getCurrentNode(widget.sendViewModel.wallet.type, chainId: result.chainId);
- await evm!.selectChain(widget.sendViewModel.wallet, result.chainId!, node: node);
- }
- } catch (e, s) {
- chainSwitchFailed = true;
- printV("completeWalletSwitch failed: $e\n$s");
- }
-
- if (!chainSwitchFailed) {
- try {
- await widget.sendViewModel.wallet.updateBalance();
- } catch (e) {
- printV("balance refresh after network switch failed: $e");
- }
- }
-
- completedFlow = true;
- if (loadingBottomSheetContext != null &&
- loadingBottomSheetContext!.mounted &&
- Navigator.canPop(loadingBottomSheetContext!)) {
- Navigator.of(loadingBottomSheetContext!).pop();
- }
- loadingBottomSheetContext = null;
+ final evaluation = rawInput != null
+ ? await widget.anyPayService.evaluateRawInput(rawInput)
+ : await widget.anyPayService.evaluatePaymentRequest(paymentRequest!);
if (!mounted) {
return;
}
- if (chainSwitchFailed) {
- _showNetworkSwitchFailedAlert();
+ if (await _handleIfOpenCryptoPay(evaluation.request.rawInput)) {
return;
}
- await _applyPaymentSelectingCurrency(
- paymentRequest,
- result.addressDetectionResult?.detectedCurrency,
- );
+ await _handleEvaluation(evaluation);
}
- void _showNetworkSwitchFailedAlert() {
- if (!mounted) {
+ Future<void> _runAnyPayPaste(String pasted) async {
+ if (await _handleIfOpenCryptoPay(pasted)) {
return;
}
- showPopUp<void>(
- context: context,
- builder: (context) => AlertWithOneAction(
- alertTitle: S.of(context).error,
- alertContent: S.of(context).network_switch_failed,
- buttonText: S.of(context).ok,
- buttonAction: () => Navigator.of(context).pop(),
- ),
+ final parsed = PaymentRequest.fromString(pasted);
+ final parsedAmount = parsed.amount == "0" ? "" : parsed.amount;
+ final merged = parsed.copyWith(
+ amount: parsedAmount.isNotEmpty ? parsedAmount : _amountControllers[_selectedOutput].text,
+ note: parsed.note.isNotEmpty ? parsed.note : _memoControllers[_selectedOutput].text,
);
- }
- void _showUnsupportedTokenAlert() {
- if (!mounted) {
- return;
- }
-
- showPopUp<void>(
- context: context,
- builder: (context) => AlertWithOneAction(
- alertTitle: S.of(context).error,
- alertContent: S.of(context).unsupported_token_requested,
- buttonText: S.of(context).ok,
- buttonAction: () => Navigator.of(context).pop(),
- ),
- );
+ await _runAnyPayFlow(paymentRequest: merged);
}
- Future<void> _showEvmNetworkPicker(
- PaymentRequest paymentRequest,
- WalletType? fixedNetwork,
- ) async {
- if (!mounted || evm == null || fixedNetwork == null || !isEVMCompatibleChain(fixedNetwork)) {
+ Future<void> _handleEvaluation(
+ AnyPayEvaluation evaluation, {
+ CryptoCurrency? fallbackCurrency,
+ }) async {
+ final evaluationResult = await _anyPayFlow.handleEvaluation(
+ context,
+ evaluation,
+ fallbackCurrency: fallbackCurrency,
+ );
+ if (evaluationResult == null || !mounted) {
return;
}
- final chains = evm!.getAllChains();
- final networks = chains.map((chain) {
- final walletType = evm!.getWalletTypeByChainId(chain.chainId) ?? fixedNetwork;
- return RecipientNetworkItem(
- chainId: chain.chainId,
- name: chain.name,
- iconPath: getCryptoCurrencyIconForWalletListItem(walletType, chainId: chain.chainId),
- chainBadgeIconPath: symbolIconPathForWalletType(walletType),
+ try {
+ await _applyIntent(
+ evaluationResult.request,
+ token: evaluationResult.token,
+ fallbackCurrency: evaluationResult.fallbackCurrency,
+ amountOverride: evaluationResult.amountOverride,
);
- }).toList();
-
- final selectedChainId =
- await EvmAddressDetectedSheet.show(context: context, networks: networks);
- if (!mounted || selectedChainId == null) {
- return;
+ } catch (e) {
+ printV("Payment flow error: $e");
+ _applyPaymentRequest(evaluationResult.request.paymentRequest);
}
-
- final target = chains.firstWhere(
- (chain) => chain.chainId == selectedChainId,
- orElse: () => chains.first,
- );
- await _handleEvmNetworkFlow(target, paymentRequest);
}
- Future<void> _handleEvmNetworkFlow(ChainInfo target, PaymentRequest paymentRequest) async {
- if (evm == null || !mounted) {
- return;
- }
-
- final targetType = evm!.getWalletTypeByChainId(target.chainId);
- if (targetType == null) {
- _showUnsupportedNetworkAlert(target.chainId);
- return;
- }
-
- final detection = AddressDetectionResult(
- address: paymentRequest.address,
- detectedWalletType: targetType,
- detectedCurrency: target.currency,
- chainId: target.chainId,
- amount: paymentRequest.amount,
- note: paymentRequest.note,
- scheme: "",
- isValid: true,
- );
- widget.paymentViewModel.applyManualEvmSelection(detection);
-
- final currentType = widget.sendViewModel.wallet.type;
- final currentChainId = isEVMCompatibleChain(currentType) ? _currentEvmChainIdOrMainnet() : null;
- if (currentChainId != null && target.chainId == currentChainId) {
- await _applyPaymentSelectingCurrency(paymentRequest, target.currency);
- return;
+ Future<void> _applyIntent(
+ AnyPayRequest request, {
+ CryptoCurrency? token,
+ CryptoCurrency? fallbackCurrency,
+ String? amountOverride,
+ }) async {
+ final currency = token ?? fallbackCurrency;
+ if (currency != null) {
+ widget.sendViewModel.applyAnyPayCurrency(currency);
}
- final compatibleWallets = await widget.paymentViewModel.getWalletsByType(targetType);
- if (!mounted) {
- return;
+ if (token == null && fallbackCurrency != null && request.amount.isEmpty) {
+ widget.sendViewModel.outputs[_selectedOutput].setCryptoAmount("");
+ _amountControllers[_selectedOutput].clear();
}
- final result = PaymentFlowResult.evmNetworkSelection(
- detection,
- compatibleWallets: compatibleWallets,
- wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
- );
-
- await _showPaymentConfirmation(
- widget.paymentViewModel,
- widget.walletSwitcherViewModel,
- paymentRequest,
- result,
- );
+ _applyPaymentRequest(request.paymentRequest, amountOverride: amountOverride);
}
void _applyPaymentRequest(PaymentRequest paymentRequest, {String? amountOverride}) {
@@ -1604,81 +1189,6 @@ class _NewSendPageState extends State<NewSendPage> {
_applyNote(paymentRequest.note, _selectedOutput);
}
- Future<void> _handleSwapFlow(
- PaymentFlowResult result,
- BuildContext presentContext,
- PaymentRequest paymentRequest,
- ) async {
- if (!mounted) {
- return;
- }
-
- final destWalletType = result.walletType;
- if (destWalletType == null) {
- return;
- }
-
- CryptoCurrency? resolvedToken;
- final contract = paymentRequest.contractAddress;
- if (contract != null && contract.isNotEmpty) {
- resolvedToken = await TokenUtilities.findTokenByAddress(
- walletType: destWalletType,
- address: contract,
- );
- }
- if (!mounted || !presentContext.mounted) {
- return;
- }
-
- if (contract != null && contract.isNotEmpty && resolvedToken == null) {
- _showUnsupportedTokenAlert();
- return;
- }
-
- final receiveCurrency = resolvedToken ??
- result.detectedCurrency ??
- walletTypeToCryptoCurrency(destWalletType, chainId: result.chainId);
-
- final receiveAmountValue = resolvedToken != null
- ? paymentRequest.resolveTokenAmount(resolvedToken)
- : (paymentRequest.amount.isNotEmpty ? paymentRequest.amount : null);
- final receiveAmount =
- receiveAmountValue != null ? Money.tryParse(receiveAmountValue, receiveCurrency) : null;
-
- final isFiatDisabled = widget.sendViewModel.isFiatDisabled;
- final depositBalanceByAsset = <CryptoCurrency, CurrencyPickerBalance>{
- for (final r in widget.sendViewModel.balanceViewModel.formattedBalances)
- r.asset: CurrencyPickerBalance(
- amount: "${r.availableBalance} ${r.asset.title}",
- fiat: isFiatDisabled ? null : "${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}",
- fiatValue: isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw),
- ),
- };
-
- final page = NewSwapPage(
- getIt.get<ExchangeViewModel>(),
- widget.authService,
- getIt.get<AddressResolverService>(),
- null,
- walletSwitcherViewModel: widget.walletSwitcherViewModel,
- fromSend: SwapFromSendArgs(
- recipientAddress: result.addressDetectionResult?.address ?? "",
- receiveCurrency: receiveCurrency,
- targetWalletType: destWalletType,
- depositBalanceByAsset: depositBalanceByAsset,
- receiveAmount: receiveAmount,
- ),
- );
- await Navigator.of(presentContext).push<void>(
- CupertinoPageRoute(
- builder: (context) => Material(
- color: Theme.of(context).colorScheme.surface,
- child: SafeArea(bottom: false, child: page),
- ),
- ),
- );
- }
-
void showErrorValidationAlert(BuildContext context) {
int emptyAddressIndex = -1;
for (int i = 0; i < widget.sendViewModel.outputs.length; i++) {
diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart
index d174478..b59ef80 100644
--- a/lib/new-ui/pages/swap_page.dart
+++ b/lib/new-ui/pages/swap_page.dart
@@ -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
diff --git a/lib/new-ui/services/wallet_switch_service.dart b/lib/new-ui/services/wallet_switch_service.dart
new file mode 100644
index 0000000..9c77635
--- /dev/null
+++ b/lib/new-ui/services/wallet_switch_service.dart
@@ -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);
+ }
+}
diff --git a/lib/new-ui/widgets/anypay/anypay_flow.dart b/lib/new-ui/widgets/anypay/anypay_flow.dart
new file mode 100644
index 0000000..0a6077b
--- /dev/null
+++ b/lib/new-ui/widgets/anypay/anypay_flow.dart
@@ -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);
+ }
+}
diff --git a/lib/new-ui/widgets/anypay/network_decision_page.dart b/lib/new-ui/widgets/anypay/network_decision_page.dart
new file mode 100644
index 0000000..527f9c2
--- /dev/null
+++ b/lib/new-ui/widgets/anypay/network_decision_page.dart
@@ -0,0 +1,174 @@
+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 NetworkDecisionPage extends StatelessWidget {
+ const NetworkDecisionPage({
+ required this.title,
+ 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,
+ });
+
+ final String title;
+ 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(
+ 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(),
+ ),
+ Expanded(
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ExcludeSemantics(
+ child: _DecisionHeader(
+ destinationIconPath: destinationIconPath,
+ currentIconPath: currentIconPath,
+ ),
+ ),
+ 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(
+ description,
+ 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: onPrimary,
+ image: primaryIcon != null
+ ? CakeImageWidget(
+ imageUrl: primaryIcon,
+ width: 24,
+ height: 24,
+ colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
+ )
+ : null,
+ text: primaryText,
+ color: colors.primary,
+ textColor: colors.onPrimary,
+ ),
+ const SizedBox(height: 12),
+ NewPrimaryButton(
+ 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,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+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,
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/new-ui/widgets/anypay/recipient_network_row.dart b/lib/new-ui/widgets/anypay/recipient_network_row.dart
index 3d978fa..9df2954 100644
--- a/lib/new-ui/widgets/anypay/recipient_network_row.dart
+++ b/lib/new-ui/widgets/anypay/recipient_network_row.dart
@@ -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,
- ),
- ),
- ],
+ ),
),
);
}
diff --git a/lib/new-ui/widgets/anypay/send_to_network_page.dart b/lib/new-ui/widgets/anypay/send_to_network_page.dart
deleted file mode 100644
index f25cd6d..0000000
--- a/lib/new-ui/widgets/anypay/send_to_network_page.dart
+++ /dev/null
@@ -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,
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
diff --git a/lib/new-ui/widgets/anypay/swap_from_network_page.dart b/lib/new-ui/widgets/anypay/swap_from_network_page.dart
deleted file mode 100644
index 24e4173..0000000
--- a/lib/new-ui/widgets/anypay/swap_from_network_page.dart
+++ /dev/null
@@ -1,140 +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 SwapFromNetworkPage extends StatelessWidget {
- const SwapFromNetworkPage({
- required this.title,
- required this.primaryButtonText,
- required this.destinationNetworkName,
- required this.destinationNetworkIconPath,
- required this.currentNetworkName,
- required this.currentNetworkIconPath,
- required this.onProceed,
- 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;
-
- @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: [
- 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,
- ),
- ],
- ),
- 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).swap_from_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: onProceed,
- image: primaryHasSwapIcon
- ? CakeImageWidget(
- imageUrl: "assets/new-ui/swap_arrows.svg",
- width: 24,
- height: 24,
- colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
- )
- : null,
- text: primaryButtonText,
- color: colors.primary,
- textColor: colors.onPrimary,
- ),
- const SizedBox(height: 12),
- NewPrimaryButton(
- onPressed: () => Navigator.of(context).maybePop(),
- text: S.of(context).cancel,
- color: colors.surfaceContainer,
- textColor: colors.primary,
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
diff --git a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart
index c88d219..f085a2d 100644
--- a/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart
+++ b/lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart
@@ -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,
),
);
diff --git a/lib/new-ui/widgets/swap_page/anypay_swap_footer.dart b/lib/new-ui/widgets/swap_page/anypay_swap_footer.dart
index f0d03e6..57d78ba 100644
--- a/lib/new-ui/widgets/swap_page/anypay_swap_footer.dart
+++ b/lib/new-ui/widgets/swap_page/anypay_swap_footer.dart
@@ -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();
}
diff --git a/lib/new-ui/widgets/swap_page/swap_amount_box.dart b/lib/new-ui/widgets/swap_page/swap_amount_box.dart
index 9f7debf..784dbd4 100644
--- a/lib/new-ui/widgets/swap_page/swap_amount_box.dart
+++ b/lib/new-ui/widgets/swap_page/swap_amount_box.dart
@@ -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,
- ),
- 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,
+ 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,
+ ),
+ ],
+ );
+ },
),
- 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,
+ ),
),
),
),
- ),
],
),
),
diff --git a/lib/new-ui/widgets/swap_page/swap_from_send_args.dart b/lib/new-ui/widgets/swap_page/swap_from_send_args.dart
index fc939ac..41f3899 100644
--- a/lib/new-ui/widgets/swap_page/swap_from_send_args.dart
+++ b/lib/new-ui/widgets/swap_page/swap_from_send_args.dart
@@ -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;
}
diff --git a/lib/new-ui/widgets/swap_page/swap_source_selector.dart b/lib/new-ui/widgets/swap_page/swap_source_selector.dart
new file mode 100644
index 0000000..b5f9667
--- /dev/null
+++ b/lib/new-ui/widgets/swap_page/swap_source_selector.dart
@@ -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,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/router.dart b/lib/router.dart
index 39a28a4..2b01b07 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -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};
diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart
index d238f91..d4452bf 100644
--- a/lib/utils/payment_request.dart
+++ b/lib/utils/payment_request.dart
@@ -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;
diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart
index 595d70d..5b6ee9e 100644
--- a/lib/view_model/exchange/exchange_view_model.dart
+++ b/lib/view_model/exchange/exchange_view_model.dart
@@ -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)
diff --git a/lib/view_model/link_view_model.dart b/lib/view_model/link_view_model.dart
index f14718e..f7067c9 100644
--- a/lib/view_model/link_view_model.dart
+++ b/lib/view_model/link_view_model.dart
@@ -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;
}
diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart
index abfec5f..670bc5a 100644
--- a/lib/view_model/send/send_view_model.dart
+++ b/lib/view_model/send/send_view_model.dart
@@ -1730,6 +1730,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) : '';
}
diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb
index b83e1f5..5d3eb9f 100644
--- a/res/values/strings_ar.arb
+++ b/res/values/strings_ar.arb
@@ -93,6 +93,7 @@
"available": "متاح",
"available_balance": "الرصيد المتاح",
"available_balance_description": "يشير “Available Balance” أو “Confirmed Balance” إلى الأموال التي يمكن إنفاقها فورًا. إذا ظهرت الأموال في الرصيد السفلي ولكن ليس في الرصيد العلوي، فعليك الانتظار بضع دقائق حتى تحصل الأموال الواردة على المزيد من تأكيدات الشبكة. بعد حصولها على المزيد من التأكيدات، ستصبح قابلة للإنفاق.",
+ "available_balance_short": "المبلغ الإجمالي: ${amount}",
"avg_savings": "متوسط التوفير",
"avl": "AVL",
"awaitDAppProcessing": "يرجى الانتظار حتى ينتهي الـ dApp من المعالجة.",
@@ -1149,6 +1150,8 @@
"swap_id": "معرّف المبادلة",
"swap_provider_initial_desc": "اختر نوع مزوّد المبادلة الذي تفضّل استخدامه عند إجراء المبادلة داخل التطبيق",
"swap_providers": "مزودو المبادلة",
+ "swap_unavailable_needs_network_wallet": "لا يمكن لهذا النوع من العنوان تلقي الأموال من المبادلة. أنت بحاجة إلى محفظة ${network} لإرسالها إلى هذا العنوان.",
+ "swap_unavailable_switch_to_network": "لا يمكن لهذا النوع من العنوان تلقي الأموال من المبادلة. قم بالتبديل إلى محفظة ${network} الخاصة بك للمتابعة.",
"sweeping_wallet": "محفظة السحب الشامل",
"sweeping_wallet_alert": "لن يستغرق هذا وقتًا طويلًا. لا تغادر هذه الشاشة وإلا فقد تُفقد الأموال المُسحوبة.",
"swipe_to_bridge": "اسحب لإجراء الجسر",
diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb
index b7631c0..d14612f 100644
--- a/res/values/strings_bg.arb
+++ b/res/values/strings_bg.arb
@@ -93,6 +93,7 @@
"available": "Налично",
"available_balance": "Наличен баланс",
"available_balance_description": "„Наличният баланс“ или „Потвърденият баланс“ са средства, които могат да бъдат изразходвани веднага. Ако средствата се появяват в долния баланс, но не и в горния, трябва да изчакате няколко минути, докато входящите средства получат повече мрежови потвърждения. След като получат повече потвърждения, те ще могат да бъдат изразходвани.",
+ "available_balance_short": "Ср.: ${amount}",
"avg_savings": "Средни спестявания",
"avl": "Avl",
"awaitDAppProcessing": "Моля, изчакайте dApp да завърши обработката.",
@@ -1149,6 +1150,8 @@
"swap_id": "ID на суап",
"swap_provider_initial_desc": "Изберете типа доставчик за суап, който предпочитате да използвате при суапване в",
"swap_providers": "Доставчици за суап",
+ "swap_unavailable_needs_network_wallet": "Този тип адрес не може да получава средства от суап. Имате нужда от ${network} портфейл, за да изпратите на този адрес.",
+ "swap_unavailable_switch_to_network": "Този тип адрес не може да получава средства от суап. Превключете към вашия ${network} портфейл, за да продължите.",
"sweeping_wallet": "Изчистване на портфейл",
"sweeping_wallet_alert": "Това не би трябвало да отнеме много време. НЕ НАПУСКАЙТЕ ТОЗИ ЕКРАН, ИНАЧЕ ИЗМЕТЕНИТЕ СРЕДСТВА МОГАТ ДА БЪДАТ ЗАГУБЕНИ.",
"swipe_to_bridge": "Плъзнете, за да прехвърлите",
diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb
index 43d226c..c0c0815 100644
--- a/res/values/strings_cs.arb
+++ b/res/values/strings_cs.arb
@@ -93,6 +93,7 @@
"available": "Dostupné",
"available_balance": "Dostupný zůstatek",
"available_balance_description": "„Dostupný zůstatek“ nebo „Potvrzený zůstatek“ jsou prostředky, které lze okamžitě utratit. Pokud se prostředky zobrazují ve spodním zůstatku, ale ne v horním, musíte několik minut počkat, než příchozí prostředky získají více síťových potvrzení. Jakmile získají více potvrzení, bude možné je utratit.",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "Prům. úspora",
"avl": "Avl",
"awaitDAppProcessing": "Počkejte prosím, až dApp dokončí zpracování.",
@@ -1149,6 +1150,8 @@
"swap_id": "ID směny",
"swap_provider_initial_desc": "Vyberte typ poskytovatele swapu, kterého chcete používat při swapování v",
"swap_providers": "Poskytovatelé swapu",
+ "swap_unavailable_needs_network_wallet": "Tento typ adresy nemůže přijímat prostředky ze swapu. K odeslání na tuto adresu potřebujete peněženku ${network}.",
+ "swap_unavailable_switch_to_network": "Tento typ adresy nemůže přijímat prostředky ze swapu. Chcete-li pokračovat, přepněte na svou peněženku ${network}.",
"sweeping_wallet": "Peněženka pro sweep",
"sweeping_wallet_alert": "To by nemělo trvat dlouho. NEOPUŠTĚJTE TUTO OBRAZOVKU, JINAK MOHOU BÝT PŘESUNUTÉ PROSTŘEDKY ZTRACENY.",
"swipe_to_bridge": "Přemostění přejetím prstem",
diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb
index 10ee6a3..a0f89e7 100644
--- a/res/values/strings_de.arb
+++ b/res/values/strings_de.arb
@@ -96,6 +96,7 @@
"available": "Verfügbar",
"available_balance": "Verfügbares Guthaben",
"available_balance_description": "Der „Verfügbare Kontostand“ bzw. „Bestätigter Kontostand“ sind Guthaben, die sofort ausgegeben werden können. Wenn Guthaben im unteren Kontostand angezeigt wird, aber nicht im oberen, müssen Sie ein paar Minuten warten, bis die eingehenden Gelder weitere Netzwerkbestätigungen erhalten. Sobald sie mehr Bestätigungen haben, können sie ausgegeben werden.",
+ "available_balance_short": "Durchschnitt: ${amount}",
"avg_savings": "Durchschn. Ersparnis",
"avl": "Avl",
"awaitDAppProcessing": "Bitte warten Sie, bis die dApp die Verarbeitung abgeschlossen hat.",
@@ -1183,6 +1184,8 @@
"swap_provider_initial_desc": "Wählen Sie die Art des Swap-Anbieters aus, die Sie beim Tauschen bevorzugen.",
"swap_providers": "Swap-Anbieter",
"swap_reverse_direction": "Swap-Richtung umkehren",
+ "swap_unavailable_needs_network_wallet": "Dieser Adresstyp kann keine Gelder von einem Swap erhalten. Sie benötigen ein ${network}-Wallet, um es an diese Adresse zu senden.",
+ "swap_unavailable_switch_to_network": "Dieser Adresstyp kann keine Gelder von einem Swap erhalten. Wechseln Sie zu Ihrem ${network}-Wallet, um fortzufahren.",
"sweeping_wallet": "Wallet leeren",
"sweeping_wallet_alert": "Das sollte nicht lange dauern. Verlassen Sie diesen Bildschirm nicht, sonst können die geleerten Guthaben verloren gehen.",
"swipe_to_bridge": "Zum Bridging wischen",
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
index 46084e4..9b8e791 100644
--- a/res/values/strings_en.arb
+++ b/res/values/strings_en.arb
@@ -96,6 +96,7 @@
"available": "Available",
"available_balance": "Available Balance",
"available_balance_description": "The “Available Balance” or “Confirmed Balance” are funds that can be spent immediately. If funds appear in the lower balance but not the top balance, then you must wait a few minutes for the incoming funds to get more network confirmations. After they get more confirmations, they will be spendable.",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "Avg. Savings",
"avl": "Avl",
"awaitDAppProcessing": "Kindly wait for the dApp to finish processing.",
@@ -1180,6 +1181,8 @@
"swap_provider_initial_desc": "Select the type of swap provider you prefer to use when swapping in",
"swap_providers": "Swap providers",
"swap_reverse_direction": "Reverse swap direction",
+ "swap_unavailable_needs_network_wallet": "This address type can't receive funds from a swap. You need a ${network} wallet to send to this address.",
+ "swap_unavailable_switch_to_network": "This address type can't receive funds from a swap. Switch to your ${network} wallet to continue.",
"sweeping_wallet": "Sweeping wallet",
"sweeping_wallet_alert": "This shouldn’t take long. DO NOT LEAVE THIS SCREEN OR THE SWEPT FUNDS MAY BE LOST.",
"swipe_to_bridge": "Swipe to bridge",
@@ -1514,4 +1517,4 @@
"zcash_network_upgrade_desc_1": "Zcash has undergone a network upgrade called Ironwood that requires migration of funds before they can be spent. This migration is automatic, privacy-preserving, and starts as soon as you open your Zcash wallet.",
"zcash_network_upgrade_desc_2": "When the migration finishes, your Available Balance will return to normal, and you can continue to send and receive Zcash as usual.",
"zcash_network_upgrade_desc_3": "Please allow your Zcash wallet to finishing syncing, and then leave the app open until migration finishes which can take a few minutes depending on the balance."
-}
+}
\ No newline at end of file
diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb
index 122335d..1e29a5c 100644
--- a/res/values/strings_es.arb
+++ b/res/values/strings_es.arb
@@ -93,6 +93,7 @@
"available": "Disponible",
"available_balance": "Saldo disponible",
"available_balance_description": "El “Saldo Disponible” o “Saldo Confirmado” son fondos que pueden gastarse de inmediato. Si los fondos aparecen en el saldo inferior pero no en el saldo superior, deberás esperar unos minutos a que los fondos entrantes obtengan más confirmaciones de la red. Una vez que tengan más confirmaciones, podrás gastarlos.",
+ "available_balance_short": "Valor medio: ${amount}",
"avg_savings": "Ahorro prom.",
"avl": "Avl",
"awaitDAppProcessing": "Espere a que la dApp termine de procesar.",
@@ -1151,6 +1152,8 @@
"swap_id": "ID de intercambio",
"swap_provider_initial_desc": "Seleccione el tipo de proveedor de intercambio que prefiere usar al intercambiar",
"swap_providers": "Proveedores de intercambio",
+ "swap_unavailable_needs_network_wallet": "Este tipo de dirección no puede recibir fondos de un swap. Necesitas una billetera ${network} para enviar a esta dirección.",
+ "swap_unavailable_switch_to_network": "Este tipo de dirección no puede recibir fondos de un swap. Cambie a su billetera ${network} para continuar.",
"sweeping_wallet": "Cartera de barrido",
"sweeping_wallet_alert": "Esto no debería tardar mucho. NO SALGAS DE ESTA PANTALLA O LOS FONDOS BARRIDOS PODRÍAN PERDERSE.",
"swipe_to_bridge": "Desliza para transferir entre cadenas",
diff --git a/res/values/strings_fa.arb b/res/values/strings_fa.arb
index b607a56..f40a8e1 100644
--- a/res/values/strings_fa.arb
+++ b/res/values/strings_fa.arb
@@ -93,6 +93,7 @@
"available": "در دسترس",
"available_balance": "موجودی در دسترس",
"available_balance_description": "«موجودی قابلدسترس» یا «موجودی تأییدشده» وجوهی هستند که میتوان بلافاصله خرج کرد.\nاگر وجوه در موجودی پایینتر ظاهر شوند اما در موجودی بالا نباشند، باید چند دقیقه صبر کنید تا وجوه ورودی تأییدهای شبکه بیشتری دریافت کنند.\nپس از دریافت تأییدهای بیشتر، قابل خرج کردن خواهند بود.",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "میانگین صرفهجویی",
"avl": "Avl",
"awaitDAppProcessing": "لطفاً منتظر بمانید تا پردازش dApp به پایان برسد.",
@@ -1149,6 +1150,8 @@
"swap_id": "شناسهٔ سواپ",
"swap_provider_initial_desc": "نوع ارائهدهندهٔ سواپ را که ترجیح میدهید هنگام سواپ از آن استفاده کنید، انتخاب کنید",
"swap_providers": "ارائهدهندگان سواپ",
+ "swap_unavailable_needs_network_wallet": "این نوع آدرس نمیتواند وجوهی را از مبادله دریافت کند. برای ارسال به این آدرس به یک کیف پول ${network} نیاز دارید.",
+ "swap_unavailable_switch_to_network": "این نوع آدرس نمیتواند وجوهی را از مبادله دریافت کند. برای ادامه به کیف پول ${network} خود بروید.",
"sweeping_wallet": "در حال انتقال موجودی کیف پول",
"sweeping_wallet_alert": "این کار نباید طول بکشد.\nاین صفحه را ترک نکنید، وگرنه ممکن است وجوه انتقالیافته از دست برود.",
"swipe_to_bridge": "برای پلزدن، انگشت خود را بکشید",
diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb
index f028797..0ced5ec 100644
--- a/res/values/strings_fr.arb
+++ b/res/values/strings_fr.arb
@@ -93,6 +93,7 @@
"available": "Disponible",
"available_balance": "Solde disponible",
"available_balance_description": "Le « Solde disponible » ou le « Solde confirmé » correspond aux fonds que vous pouvez dépenser immédiatement. Si des fonds apparaissent dans le solde inférieur mais pas dans le solde supérieur, vous devez attendre quelques minutes pour que les fonds entrants obtiennent davantage de confirmations sur le réseau. Une fois qu’ils auront reçu plus de confirmations, ils pourront être dépensés.",
+ "available_balance_short": "Valeur moyenne : ${amount}",
"avg_savings": "Économies moy.",
"avl": "Avl",
"awaitDAppProcessing": "Veuillez patienter pendant que la dApp termine le traitement.",
@@ -1149,6 +1150,8 @@
"swap_id": "ID d’échange",
"swap_provider_initial_desc": "Sélectionnez le type de fournisseur de swap que vous préférez utiliser lors d’un swap",
"swap_providers": "Fournisseurs de swap",
+ "swap_unavailable_needs_network_wallet": "Ce type d'adresse ne peut pas recevoir de fonds provenant d'un swap. Vous avez besoin d'un portefeuille ${network} pour envoyer à cette adresse.",
+ "swap_unavailable_switch_to_network": "Ce type d'adresse ne peut pas recevoir de fonds provenant d'un swap. Basculez vers votre portefeuille ${network} pour continuer.",
"sweeping_wallet": "Balayage du portefeuille",
"sweeping_wallet_alert": "Cela ne devrait pas prendre longtemps. NE QUITTEZ PAS CET ÉCRAN, SINON LES FONDS BALAYÉS RISQUENT D’ÊTRE PERDUS.",
"swipe_to_bridge": "Glissez pour transférer (bridge)",
diff --git a/res/values/strings_gn.arb b/res/values/strings_gn.arb
index 7c44ed6..679d732 100644
--- a/res/values/strings_gn.arb
+++ b/res/values/strings_gn.arb
@@ -93,6 +93,7 @@
"available": "Oĩva",
"available_balance": "Saldo ojepurukuaáva",
"available_balance_description": "Pe “Available Balance” térã “Confirmed Balance” ha'e umi fondo ikatúva reipuru pya'e. Umi fondo ojehecháramo pe saldo yvýpe ha ndaha'éi pe saldo yvate gotyo, upéicharõ eha'arõ sapy'ami hag̃ua umi fondo og̃uahẽva ohupytyve hag̃ua confirmación de red. Oguerekóvo hetave confirmación, ikatúta reipuru.",
+ "available_balance_short": "Avl: ${amount}.",
"avg_savings": "Ñongatu mbytegua",
"avl": "Avl",
"awaitDAppProcessing": "Eha'arõmi pe dApp omohu'ã hag̃ua hembiapo.",
@@ -1155,6 +1156,8 @@
"swap_id": "ID ñemoambue rehegua",
"swap_provider_initial_desc": "Eiporavo mba’eichagua swap puruhára reipuruséva rembojopyru hag̃ua swap jave.",
"swap_providers": "Proveedor-kuéra intercambio rehegua",
+ "swap_unavailable_needs_network_wallet": "Ko tipo dirección ndikatúi ohupyty fondo peteî swap-gui. Tekotevẽ peteĩ ${network} billetera emondo hag̃ua ko dirección-pe.",
+ "swap_unavailable_switch_to_network": "Ko tipo dirección ndikatúi ohupyty fondo peteî swap-gui. Embohasa nde ${network} billetera-pe eñepyrũ hag̃ua.",
"sweeping_wallet": "Billetera ñemopotĩ",
"sweeping_wallet_alert": "Kóva ndoguerahámo’ãi heta aravo. ANI REHEJA KO PANTALLA, térã umi fondo oñemopotĩva ikatu okañy.",
"swipe_to_bridge": "Embosyryry embridge hag̃ua",
diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb
index 6b3c7ec..1df9ce6 100644
--- a/res/values/strings_ha.arb
+++ b/res/values/strings_ha.arb
@@ -93,6 +93,7 @@
"available": "Akwai",
"available_balance": "Ma’aunin da ake da shi",
"available_balance_description": "“Ma'aunin da ake da shi” ko “Tabbataccen Ma'auni” su ne kuɗaɗen da za a iya kashewa nan take. Idan kuɗaɗe sun bayyana a ƙananan ma'auni amma ba a babban ma'auni ba, to dole ne ku jira 'yan mintoci kaɗan domin kuɗaɗen da ke shigowa su sami ƙarin tabbaci daga hanyar sadarwa. Bayan sun sami ƙarin tabbaci, za a iya kashe su.",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "Matsakaitan Ajiya",
"avl": "Avl",
"awaitDAppProcessing": "Da fatan za a jira dApp ya kammala sarrafawa.",
@@ -1151,6 +1152,8 @@
"swap_id": "ID na Musanya",
"swap_provider_initial_desc": "Zaɓi nau'in mai ba da sabis na musanyawa da ka fi so ka yi amfani da shi lokacin musanyawa",
"swap_providers": "Masu samar da Swap",
+ "swap_unavailable_needs_network_wallet": "Wannan nau'in adireshin ba zai iya karɓar kuɗi daga musanyawa ba. Kuna buƙatar walat ɗin ${network} don aikawa zuwa wannan adireshin.",
+ "swap_unavailable_switch_to_network": "Wannan nau'in adireshin ba zai iya karɓar kuɗi daga musanyawa ba. Canja zuwa jakar kuɗin ku na ${network} don ci gaba.",
"sweeping_wallet": "Ana kwashe kuɗi daga walat",
"sweeping_wallet_alert": "Wannan bai kamata ya ɗauki lokaci mai tsawo ba. KADA KA BAR WANNAN ALLON KO KUDADEN DA AKA KWASO NA IYA ɓACEWA.",
"swipe_to_bridge": "Zame don gada",
diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb
index 79c2d2c..0aa4600 100644
--- a/res/values/strings_hi.arb
+++ b/res/values/strings_hi.arb
@@ -93,6 +93,7 @@
"available": "उपलब्ध",
"available_balance": "उपलब्ध शेष राशि",
"available_balance_description": "“Available Balance” या “Confirmed Balance” वे धनराशि हैं जिन्हें तुरंत खर्च किया जा सकता है। यदि फंड नीचे वाले बैलेंस में दिखाई देते हैं, लेकिन ऊपर वाले बैलेंस में नहीं, तो आने वाले फंड को नेटवर्क पर और अधिक पुष्टिकरण मिलने के लिए आपको कुछ मिनट इंतज़ार करना होगा। अधिक पुष्टिकरण मिलने के बाद, वे खर्च करने योग्य हो जाएंगे।",
+ "available_balance_short": "एवीएल: ${amount}",
"avg_savings": "औसत बचत",
"avl": "अवल",
"awaitDAppProcessing": "कृपया dApp की प्रोसेसिंग पूरी होने तक प्रतीक्षा करें।",
@@ -1151,6 +1152,8 @@
"swap_id": "स्वैप आईडी",
"swap_provider_initial_desc": "स्वैप करते समय आप जिस प्रकार के स्वैप प्रदाता का उपयोग करना पसंद करते हैं, उसका चयन करें",
"swap_providers": "स्वैप प्रदाता",
+ "swap_unavailable_needs_network_wallet": "यह पता प्रकार स्वैप से धनराशि प्राप्त नहीं कर सकता। इस पते पर भेजने के लिए आपको एक ${network} वॉलेट की आवश्यकता है।",
+ "swap_unavailable_switch_to_network": "यह पता प्रकार स्वैप से धनराशि प्राप्त नहीं कर सकता। जारी रखने के लिए अपने ${network} वॉलेट पर स्विच करें।",
"sweeping_wallet": "वॉलेट स्वीप करना",
"sweeping_wallet_alert": "इसमें ज़्यादा समय नहीं लगना चाहिए। इस स्क्रीन को न छोड़ें, वरना स्वीप किए गए फंड खो सकते हैं।",
"swipe_to_bridge": "ब्रिज करने के लिए स्वाइप करें",
diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb
index ae02df5..4f52711 100644
--- a/res/values/strings_hr.arb
+++ b/res/values/strings_hr.arb
@@ -93,6 +93,7 @@
"available": "Dostupno",
"available_balance": "Raspoloživo stanje",
"available_balance_description": "“Dostupno stanje” ili “Potvrđeno stanje” su sredstva koja možete odmah potrošiti. Ako se sredstva prikazuju u donjem stanju, ali ne i u gornjem, morate pričekati nekoliko minuta da dolazna sredstva dobiju više mrežnih potvrda. Nakon što dobiju više potvrda, moći ćete ih potrošiti.",
+ "available_balance_short": "Prosjek: ${amount}",
"avg_savings": "Prosj. ušteda",
"avl": "Avl",
"awaitDAppProcessing": "Molimo pričekajte da dApp završi s obradom.",
@@ -1149,6 +1150,8 @@
"swap_id": "ID zamjene",
"swap_provider_initial_desc": "Odaberite vrstu pružatelja zamjene koju želite koristiti pri zamjeni",
"swap_providers": "Pružatelji usluga zamjene",
+ "swap_unavailable_needs_network_wallet": "Ova vrsta adrese ne može primiti sredstva od zamjene. Potreban vam je ${network} novčanik za slanje na ovu adresu.",
+ "swap_unavailable_switch_to_network": "Ova vrsta adrese ne može primiti sredstva od zamjene. Za nastavak prebacite se na svoj ${network} novčanik.",
"sweeping_wallet": "Prebacivanje sredstava u novčanik",
"sweeping_wallet_alert": "Ovo ne bi trebalo dugo trajati. NE NAPUŠTAJTE OVAJ ZASLON ILI PREBAČENA SREDSTVA MOGU BITI IZGUBLJENA.",
"swipe_to_bridge": "Prijeđite prstom za premošćivanje",
diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb
index 344bf1a..a593037 100644
--- a/res/values/strings_hy.arb
+++ b/res/values/strings_hy.arb
@@ -93,6 +93,7 @@
"available": "Հասանելի",
"available_balance": "Հասանելի մնացորդ",
"available_balance_description": "«Հասանելի մնացորդը» կամ «Հաստատված մնացորդը» միջոցներ են, որոնք կարելի է անմիջապես ծախսել։ Եթե միջոցները երևում են ստորին մնացորդում, բայց ոչ վերևի մնացորդում, ապա պետք է մի քանի րոպե սպասեք, մինչև մուտքագրվող միջոցները ստանան ավելի շատ ցանցային հաստատումներ։ Ավելի շատ հաստատումներ ստանալուց հետո դրանք հնարավոր կլինի ծախսել։",
+ "available_balance_short": "Ավլ՝ ${amount}",
"avg_savings": "Միջին խնայողություն",
"avl": "Ավլ",
"awaitDAppProcessing": "Խնդրում ենք սպասել, մինչև dApp-ը ավարտի մշակումը։",
@@ -1149,6 +1150,8 @@
"swap_id": "Փոխանակման ID",
"swap_provider_initial_desc": "Ընտրեք փոխանակման մատակարարի տեսակը, որը նախընտրում եք օգտագործել փոխանակում կատարելիս",
"swap_providers": "Փոխանակման մատակարարներ",
+ "swap_unavailable_needs_network_wallet": "Այս հասցեի տեսակը չի կարող գումարներ ստանալ փոխանակումից: Այս հասցեին ուղարկելու համար ձեզ հարկավոր է ${network} դրամապանակ:",
+ "swap_unavailable_switch_to_network": "Այս հասցեի տեսակը չի կարող գումարներ ստանալ փոխանակումից: Շարունակելու համար անցեք ձեր ${network} դրամապանակին:",
"sweeping_wallet": "Դրամապանակի սվիպինգ",
"sweeping_wallet_alert": "Սա երկար չի տևի։ ՄԻ ԼՔԵՔ ԱՅՍ ԷԿՐԱՆԸ, ՀԱԿԱՌԱԿ ԴԵՊՔՈՒՄ ՄԱՔՐՎՈՂ ՄԻՋՈՑՆԵՐԸ ԿԱՐՈՂ ԵՆ ԿՈՐՉԵԼ։",
"swipe_to_bridge": "Սահեցրեք՝ կամրջելու համար",
diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb
index 5909cb8..16fcb48 100644
--- a/res/values/strings_id.arb
+++ b/res/values/strings_id.arb
@@ -93,6 +93,7 @@
"available": "Tersedia",
"available_balance": "Saldo Tersedia",
"available_balance_description": "“Saldo Tersedia” atau “Saldo Terkonfirmasi” adalah dana yang dapat langsung dibelanjakan. Jika dana muncul di saldo bawah tetapi tidak di saldo atas, maka Anda harus menunggu beberapa menit agar dana masuk mendapatkan konfirmasi jaringan tambahan. Setelah mendapatkan lebih banyak konfirmasi, dana tersebut akan dapat dibelanjakan.",
+ "available_balance_short": "Rata-rata: ${amount}",
"avg_savings": "Rata-rata Penghematan",
"avl": "Rata-rata",
"awaitDAppProcessing": "Mohon tunggu hingga dApp selesai memproses.",
@@ -1152,6 +1153,8 @@
"swap_id": "ID Swap",
"swap_provider_initial_desc": "Pilih jenis penyedia swap yang Anda inginkan untuk digunakan saat melakukan swap",
"swap_providers": "Penyedia swap",
+ "swap_unavailable_needs_network_wallet": "Jenis alamat ini tidak dapat menerima dana dari swap. Anda memerlukan dompet ${network} untuk mengirim ke alamat ini.",
+ "swap_unavailable_switch_to_network": "Jenis alamat ini tidak dapat menerima dana dari swap. Beralih ke dompet ${network} Anda untuk melanjutkan.",
"sweeping_wallet": "Dompet sweep",
"sweeping_wallet_alert": "Ini seharusnya tidak memakan waktu lama. JANGAN TINGGALKAN LAYAR INI ATAU DANA YANG DISAPU DAPAT HILANG.",
"swipe_to_bridge": "Geser untuk menjembatani",
diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb
index f8028e1..a7f73b6 100644
--- a/res/values/strings_it.arb
+++ b/res/values/strings_it.arb
@@ -93,6 +93,7 @@
"available": "Disponibile",
"available_balance": "Saldo disponibile",
"available_balance_description": "Il “Saldo disponibile” o “Saldo confermato” sono fondi che possono essere spesi immediatamente. Se i fondi compaiono nel saldo in basso ma non in quello in alto, devi attendere qualche minuto affinché i fondi in arrivo ottengano ulteriori conferme di rete. Dopo aver ricevuto più conferme, saranno spendibili.",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "Risparmio medio",
"avl": "Avl",
"awaitDAppProcessing": "Attendi che la dApp completi l'elaborazione.",
@@ -1150,6 +1151,8 @@
"swap_id": "ID swap",
"swap_provider_initial_desc": "Seleziona il tipo di fornitore di swap che preferisci utilizzare quando effettui uno swap in",
"swap_providers": "Provider di swap",
+ "swap_unavailable_needs_network_wallet": "Questo tipo di indirizzo non può ricevere fondi da uno swap. Hai bisogno di un portafoglio ${network} per inviare a questo indirizzo.",
+ "swap_unavailable_switch_to_network": "Questo tipo di indirizzo non può ricevere fondi da uno swap. Passa al tuo portafoglio ${network} per continuare.",
"sweeping_wallet": "Svuotamento del portafoglio",
"sweeping_wallet_alert": "Questo non dovrebbe richiedere molto tempo. NON LASCIARE QUESTA SCHERMATA O I FONDI SPOSTATI POTREBBERO ANDARE PERSI.",
"swipe_to_bridge": "Scorri per il bridge",
diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb
index 8d8e507..8acfdaf 100644
--- a/res/values/strings_ja.arb
+++ b/res/values/strings_ja.arb
@@ -93,6 +93,7 @@
"available": "利用可能",
"available_balance": "利用可能残高",
"available_balance_description": "「利用可能な残高」または「承認済み残高」は、すぐに使用できる資金です。下の残高には表示されているのに上の残高に表示されない場合は、受信した資金がネットワークでさらに承認(確認)されるまで数分待つ必要があります。承認数が増えると、使用できるようになります。",
+ "available_balance_short": "平均: ${amount}",
"avg_savings": "平均節約額",
"avl": "アヴル",
"awaitDAppProcessing": "dAppの処理が完了するまでお待ちください。",
@@ -1150,6 +1151,8 @@
"swap_id": "スワップID",
"swap_provider_initial_desc": "スワップイン時に使用するスワッププロバイダーの種類を選択してください",
"swap_providers": "スワッププロバイダー",
+ "swap_unavailable_needs_network_wallet": "このアドレスタイプはスワップから資金を受け取ることができません。このアドレスに送信するには ${network} ウォレットが必要です。",
+ "swap_unavailable_switch_to_network": "このアドレスタイプはスワップから資金を受け取ることができません。 ${network} ウォレットに切り替えて続行してください。",
"sweeping_wallet": "スイーピングウォレット",
"sweeping_wallet_alert": "すぐに完了します。この画面から離れないでください。離れると、スイープした資金が失われる可能性があります。",
"swipe_to_bridge": "スワイプしてブリッジ",
diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb
index 393bf17..2777da0 100644
--- a/res/values/strings_ko.arb
+++ b/res/values/strings_ko.arb
@@ -93,6 +93,7 @@
"available": "사용 가능",
"available_balance": "사용 가능 잔액",
"available_balance_description": "“사용 가능 잔액” 또는 “확정 잔액”은 즉시 사용할 수 있는 자금입니다. 자금이 아래 잔액에는 표시되지만 위 잔액에는 표시되지 않는 경우, 입금된 자금이 네트워크에서 더 많은 컨펌(확인)을 받을 때까지 몇 분 정도 기다려야 합니다. 더 많은 컨펌을 받은 후에는 사용할 수 있습니다.",
+ "available_balance_short": "평균: ${amount}",
"avg_savings": "평균 절감액",
"avl": "Avl",
"awaitDAppProcessing": "dApp의 처리가 완료될 때까지 잠시 기다려 주세요.",
@@ -1149,6 +1150,8 @@
"swap_id": "스왑 ID",
"swap_provider_initial_desc": "스왑 인할 때 사용하려는 스왑 제공자 유형을 선택하세요.",
"swap_providers": "스왑 제공업체",
+ "swap_unavailable_needs_network_wallet": "이 주소 유형은 스왑에서 자금을 받을 수 없습니다. 이 주소로 보내려면 ${network} 지갑이 필요합니다.",
+ "swap_unavailable_switch_to_network": "이 주소 유형은 스왑에서 자금을 받을 수 없습니다. 계속하려면 ${network} 지갑으로 전환하세요.",
"sweeping_wallet": "지갑 스윕",
"sweeping_wallet_alert": "오래 걸리지 않습니다. 이 화면을 떠나지 마세요. 그렇지 않으면 스윕된 자금이 손실될 수 있습니다.",
"swipe_to_bridge": "스와이프하여 브리지",
diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb
index 92a5b09..e8e9eac 100644
--- a/res/values/strings_my.arb
+++ b/res/values/strings_my.arb
@@ -93,6 +93,7 @@
"available": "ရရှိနိုင်သည်",
"available_balance": "ရရှိနိုင်သော လက်ကျန်",
"available_balance_description": "“Available Balance” သို့မဟုတ် “Confirmed Balance” သည် ချက်ချင်းသုံးစွဲနိုင်သော လက်ကျန်ငွေဖြစ်သည်။ လက်ကျန်အောက်ပိုင်းတွင်သာ ဝင်လာသော်လည်း အပေါ်ပိုင်းလက်ကျန်တွင် မပေါ်သေးပါက ကွန်ယက်အတည်ပြုချက် (network confirmations) ပိုမိုရရှိရန် မိနစ်အနည်းငယ် စောင့်ရပါမည်။ အတည်ပြုချက်များ ပိုမိုရရှိပြီးနောက် ထိုငွေများကို သုံးစွဲနိုင်ပါလိမ့်မည်။",
+ "available_balance_short": "Avl- ${amount}",
"avg_savings": "ပျမ်းမျှ ချွေတာမှု",
"avl": "Avl",
"awaitDAppProcessing": "ကျေးဇူးပြု၍ dApp ၏ စီမံဆောင်ရွက်မှု ပြီးဆုံးသည်အထိ စောင့်ပါ။",
@@ -1149,6 +1150,8 @@
"swap_id": "Swap ID",
"swap_provider_initial_desc": "Swap လုပ်ရာတွင် သင်အသုံးပြုလိုသည့် swap provider အမျိုးအစားကို ရွေးချယ်ပါ။",
"swap_providers": "Swap ဝန်ဆောင်မှုပေးသူများ",
+ "swap_unavailable_needs_network_wallet": "ဤလိပ်စာအမျိုးအစားသည် လဲလှယ်မှုတစ်ခုမှ ရံပုံငွေများကို လက်ခံမရနိုင်ပါ။ ဤလိပ်စာသို့ ပေးပို့ရန် ${network} ပိုက်ဆံအိတ်တစ်ခု လိုအပ်ပါသည်။",
+ "swap_unavailable_switch_to_network": "ဤလိပ်စာအမျိုးအစားသည် လဲလှယ်မှုတစ်ခုမှ ရံပုံငွေများကို လက်ခံမရနိုင်ပါ။ ရှေ့ဆက်ရန် သင်၏ ${network} ပိုက်ဆံအိတ်သို့ ပြောင်းပါ။",
"sweeping_wallet": "ပိုက်ဆံအိတ် ရှင်းလင်းခြင်း",
"sweeping_wallet_alert": "ဒါက ကြာကြာမခံသင့်ပါဘူး။ ဒီစခရင်ကနေ မထွက်ပါနဲ့၊ မဟုတ်ရင် sweep လုပ်ထားတဲ့ ရန်ပုံငွေတွေ ပျောက်ဆုံးနိုင်ပါတယ်။",
"swipe_to_bridge": "Bridge လုပ်ရန် ပွတ်ဆွဲပါ",
diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb
index 715a2a7..d9d677a 100644
--- a/res/values/strings_nl.arb
+++ b/res/values/strings_nl.arb
@@ -93,6 +93,7 @@
"available": "Beschikbaar",
"available_balance": "Beschikbaar saldo",
"available_balance_description": "Het “Beschikbare saldo” of “Bevestigde saldo” zijn fondsen die direct kunnen worden uitgegeven. Als fondsen in het lagere saldo verschijnen maar niet in het bovenste saldo, dan moet je een paar minuten wachten tot de inkomende fondsen meer netwerkbevestigingen krijgen. Daarna kan het worden uitgegeven.",
+ "available_balance_short": "Gem.: ${amount}",
"avg_savings": "Gem. besparingen",
"avl": "Gem",
"awaitDAppProcessing": "Wacht tot de dApp klaar is met verwerken.",
@@ -1149,6 +1150,8 @@
"swap_id": "Swap-ID",
"swap_provider_initial_desc": "Selecteer het type swapprovider dat je wilt gebruiken bij het swappen",
"swap_providers": "Swapproviders",
+ "swap_unavailable_needs_network_wallet": "Dit adrestype kan geen geld ontvangen van een swap. U heeft een portemonnee van ${network} nodig om naar dit adres te kunnen verzenden.",
+ "swap_unavailable_switch_to_network": "Dit adrestype kan geen geld ontvangen van een swap. Schakel over naar uw ${network}-portemonnee om door te gaan.",
"sweeping_wallet": "Wallet wordt geveegd",
"sweeping_wallet_alert": "Dit duurt niet lang. VERLAAT DIT SCHERM NIET, ANDERS KUNNEN DE FONDSEN VERLOREN GAAN.",
"swipe_to_bridge": "Veeg om te bridgen",
diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb
index 5a308be..8650bd0 100644
--- a/res/values/strings_pl.arb
+++ b/res/values/strings_pl.arb
@@ -93,6 +93,7 @@
"available": "Dostępne",
"available_balance": "Dostępne saldo",
"available_balance_description": "„Dostępne saldo” lub „Potwierdzone saldo” to środki, które można wydać od razu. Jeśli środki pojawiają się w dolnym saldzie, ale nie w górnym, musisz poczekać kilka minut, aż przychodzące środki uzyskają więcej potwierdzeń sieci. Gdy uzyskają więcej potwierdzeń, będzie można je wydać.",
+ "available_balance_short": "Średnia: ${amount}",
"avg_savings": "Śr. oszczędności",
"avl": "Avl",
"awaitDAppProcessing": "Proszę poczekać, aż dApp zakończy przetwarzanie.",
@@ -1149,6 +1150,8 @@
"swap_id": "ID wymiany",
"swap_provider_initial_desc": "Wybierz typ dostawcy swapów, którego wolisz używać podczas swapowania w",
"swap_providers": "Dostawcy wymiany",
+ "swap_unavailable_needs_network_wallet": "Ten typ adresu nie może otrzymywać środków z wymiany. Aby wysłać przesyłkę na ten adres, potrzebujesz portfela ${network}.",
+ "swap_unavailable_switch_to_network": "Ten typ adresu nie może otrzymywać środków z wymiany. Aby kontynuować, przejdź do portfela ${network}.",
"sweeping_wallet": "Opróżnianie portfela",
"sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE OPUSZCZAJ TEGO EKRANU, W PRZECIWNYM RAZIE ZAMIATANE ŚRODKI MOGĄ ZOSTAĆ UTRACONE.",
"swipe_to_bridge": "Przesuń, aby zmostkować",
diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb
index 2f7abc9..2a392da 100644
--- a/res/values/strings_pt.arb
+++ b/res/values/strings_pt.arb
@@ -93,6 +93,7 @@
"available": "Disponível",
"available_balance": "Saldo disponível",
"available_balance_description": "O “Saldo Disponível” ou “Saldo Confirmado” são fundos que podem ser gastos imediatamente. Se os fundos aparecerem no saldo inferior, mas não no saldo superior, você deve aguardar alguns minutos para que os fundos recebidos obtenham mais confirmações na rede. Depois que tiverem mais confirmações, eles poderão ser gastos.",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "Poupança média",
"avl": "Avl",
"awaitDAppProcessing": "Por favor, aguarde o dApp concluir o processamento.",
@@ -1151,6 +1152,8 @@
"swap_id": "ID da troca",
"swap_provider_initial_desc": "Selecione o tipo de provedor de swap que você prefere usar ao fazer swaps em",
"swap_providers": "Provedores de swap",
+ "swap_unavailable_needs_network_wallet": "Este tipo de endereço não pode receber fundos de um swap. Você precisa de uma carteira ${network} para enviar para este endereço.",
+ "swap_unavailable_switch_to_network": "Este tipo de endereço não pode receber fundos de um swap. Mude para sua carteira ${network} para continuar.",
"sweeping_wallet": "Carteira de varredura",
"sweeping_wallet_alert": "Isso não deve demorar muito. NÃO SAIA DESTA TELA OU OS FUNDOS VARRIDOS PODEM SER PERDIDOS.",
"swipe_to_bridge": "Deslize para fazer bridge",
diff --git a/res/values/strings_pt_BR.arb b/res/values/strings_pt_BR.arb
index f43bdf5..51e8169 100644
--- a/res/values/strings_pt_BR.arb
+++ b/res/values/strings_pt_BR.arb
@@ -88,6 +88,7 @@
"automatic": "Automático",
"available_balance": "Saldo disponível",
"available_balance_description": "O “Saldo disponível” ou “Saldo confirmado” são fundos que podem ser gastos imediatamente. Se os fundos aparecerem no saldo inferior, mas não no saldo superior, você deve aguardar alguns minutos para que os fundos recebidos obtenham mais confirmações na rede. Depois que tiverem mais confirmações, eles poderão ser gastos.",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "Economia média",
"awaitDAppProcessing": "Aguarde até que o dApp termine de processar.",
"awaiting_payment_confirmation": "Aguardando confirmação do pagamento",
@@ -1099,6 +1100,8 @@
"swap_id": "ID do Swap",
"swap_provider_initial_desc": "Selecione o tipo de provedor de swap que você prefere usar ao realizar swaps",
"swap_providers": "Provedores de swap",
+ "swap_unavailable_needs_network_wallet": "Este tipo de endereço não pode receber fundos de um swap. Você precisa de uma carteira ${network} para enviar para este endereço.",
+ "swap_unavailable_switch_to_network": "Este tipo de endereço não pode receber fundos de um swap. Mude para sua carteira ${network} para continuar.",
"sweeping_wallet": "Carteira em varredura",
"sweeping_wallet_alert": "Isso não deve demorar muito. NÃO SAIA DESTA TELA OU OS FUNDOS VARRIDOS PODERÃO SER PERDIDOS.",
"swipe_to_send": "Deslize para enviar",
diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb
index 89e569e..1bdeee2 100644
--- a/res/values/strings_ru.arb
+++ b/res/values/strings_ru.arb
@@ -93,6 +93,7 @@
"available": "Доступно",
"available_balance": "Доступный баланс",
"available_balance_description": "«Доступный баланс» или «Подтверждённый баланс» — это средства, которые можно потратить сразу. Если средства отображаются в нижнем балансе, но не в верхнем, значит, нужно подождать несколько минут, пока входящие средства получат больше подтверждений сети. После получения дополнительных подтверждений их можно будет потратить.",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "Средняя экономия",
"avl": "Авл",
"awaitDAppProcessing": "Пожалуйста, подождите, пока dApp завершит обработку.",
@@ -1150,6 +1151,8 @@
"swap_id": "ID обмена",
"swap_provider_initial_desc": "Выберите тип провайдера свопа, который вы предпочитаете использовать при свопе в",
"swap_providers": "Провайдеры обмена",
+ "swap_unavailable_needs_network_wallet": "Этот тип адреса не может получать средства от свопа. Для отправки на этот адрес вам понадобится кошелек ${network}.",
+ "swap_unavailable_switch_to_network": "Этот тип адреса не может получать средства от свопа. Чтобы продолжить, переключитесь на свой кошелек ${network}.",
"sweeping_wallet": "Сканирование кошелька",
"sweeping_wallet_alert": "Это не должно занять много времени. НЕ ПОКИДАЙТЕ ЭТОТ ЭКРАН, ИНАЧЕ ПЕРЕНЕСЁННЫЕ СРЕДСТВА МОГУТ БЫТЬ ПОТЕРЯНЫ.",
"swipe_to_bridge": "Проведите, чтобы выполнить перенос через мост",
diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb
index d4071a3..b8e98d1 100644
--- a/res/values/strings_th.arb
+++ b/res/values/strings_th.arb
@@ -93,6 +93,7 @@
"available": "พร้อมใช้งาน",
"available_balance": "ยอดคงเหลือที่ใช้ได้",
"available_balance_description": "“ยอดคงเหลือที่ใช้ได้” หรือ “ยอดคงเหลือที่ยืนยันแล้ว” คือเงินที่สามารถใช้จ่ายได้ทันที หากเงินปรากฏในยอดคงเหลือด้านล่างแต่ไม่ปรากฏในยอดคงเหลือด้านบน คุณต้องรอสักครู่เพื่อให้เงินที่เข้ามาได้รับการยืนยันจากเครือข่ายเพิ่มขึ้น เมื่อได้รับการยืนยันมากขึ้นแล้ว เงินนั้นจะสามารถใช้จ่ายได้",
+ "available_balance_short": "จำนวน: ${amount}",
"avg_savings": "การประหยัดเฉลี่ย",
"avl": "เฉลี่ย",
"awaitDAppProcessing": "โปรดรอให้ dApp ประมวลผลให้เสร็จสิ้น",
@@ -1149,6 +1150,8 @@
"swap_id": "รหัส Swap",
"swap_provider_initial_desc": "เลือกประเภทผู้ให้บริการ Swap ที่คุณต้องการใช้เมื่อทำการ Swap",
"swap_providers": "ผู้ให้บริการสวอป",
+ "swap_unavailable_needs_network_wallet": "ที่อยู่ประเภทนี้ไม่สามารถรับเงินจากการแลกเปลี่ยนได้ คุณต้องมีกระเป๋าเงิน ${network} เพื่อส่งไปยังที่อยู่นี้",
+ "swap_unavailable_switch_to_network": "ที่อยู่ประเภทนี้ไม่สามารถรับเงินจากการแลกเปลี่ยนได้ เปลี่ยนไปใช้กระเป๋าเงิน ${network} ของคุณเพื่อดำเนินการต่อ",
"sweeping_wallet": "กระเป๋าสตางค์สำหรับกวาดเหรียญ",
"sweeping_wallet_alert": "การดำเนินการนี้ใช้เวลาไม่นาน ห้ามออกจากหน้าจอนี้ มิฉะนั้นเงินที่กวาดอาจสูญหาย",
"swipe_to_bridge": "ปัดเพื่อบริดจ์",
diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb
index b9bfb13..86a0d45 100644
--- a/res/values/strings_tl.arb
+++ b/res/values/strings_tl.arb
@@ -93,6 +93,7 @@
"available": "Available",
"available_balance": "Magagamit na Balanse",
"available_balance_description": "Ang “Magagamit na Balanse” o “Nakumpirmang Balanse” ay mga pondong maaari nang gastusin kaagad. Kung lumalabas ang mga pondo sa mas mababang balanse ngunit wala sa itaas na balanse, kailangan mong maghintay ng ilang minuto para makakuha ng mas maraming kumpirmasyon sa network ang mga papasok na pondo. Kapag nakakuha na sila ng mas maraming kumpirmasyon, maaari na silang gastusin.",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "Avg. Matitipid",
"avl": "Avl",
"awaitDAppProcessing": "Mangyaring maghintay hanggang matapos ang pagproseso ng dApp.",
@@ -1149,6 +1150,8 @@
"swap_id": "Swap ID",
"swap_provider_initial_desc": "Piliin ang uri ng swap provider na mas gusto mong gamitin kapag nagsa-swap",
"swap_providers": "Mga provider ng swap",
+ "swap_unavailable_needs_network_wallet": "Ang uri ng address na ito ay hindi makakatanggap ng mga pondo mula sa isang swap. Kailangan mo ng ${network} wallet upang maipadala sa address na ito.",
+ "swap_unavailable_switch_to_network": "Ang uri ng address na ito ay hindi makakatanggap ng mga pondo mula sa isang swap. Lumipat sa iyong ${network} wallet upang magpatuloy.",
"sweeping_wallet": "Sweeping wallet",
"sweeping_wallet_alert": "Hindi ito dapat magtagal. HUWAG UMALIS SA SCREEN NA ITO O MAARING MAWALA ANG MGA NA-SWEEP NA PONDO.",
"swipe_to_bridge": "Mag-swipe para mag-bridge",
diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb
index e937247..a2f6a18 100644
--- a/res/values/strings_tr.arb
+++ b/res/values/strings_tr.arb
@@ -93,6 +93,7 @@
"available": "Kullanılabilir",
"available_balance": "Kullanılabilir Bakiye",
"available_balance_description": "“Kullanılabilir Bakiye” veya “Onaylanmış Bakiye”, hemen harcanabilen fonlardır. Fonlar alt bakiyede görünüp üst bakiyede görünmüyorsa, gelen fonların ağda daha fazla onay alması için birkaç dakika beklemeniz gerekir. Daha fazla onay aldıktan sonra harcanabilir olacaktır.",
+ "available_balance_short": "Ortalama: ${amount}",
"avg_savings": "Ort. Tasarruf",
"avl": "Avl",
"awaitDAppProcessing": "Lütfen dApp'in işlemi tamamlamasını bekleyin.",
@@ -1149,6 +1150,8 @@
"swap_id": "Swap Kimliği",
"swap_provider_initial_desc": "Swap yaparken kullanmayı tercih ettiğiniz swap sağlayıcı türünü seçin",
"swap_providers": "Takas sağlayıcıları",
+ "swap_unavailable_needs_network_wallet": "Bu adres türü takastan para alamaz. Bu adrese göndermek için bir ${network} cüzdanına ihtiyacınız var.",
+ "swap_unavailable_switch_to_network": "Bu adres türü takastan para alamaz. Devam etmek için ${network} cüzdanınıza geçin.",
"sweeping_wallet": "Cüzdan süpürme",
"sweeping_wallet_alert": "Bu uzun sürmemeli. BU EKRANDAN AYRILMAYIN, AKSİ TAKDİRDE SÜPÜRÜLEN FONLAR KAYBOLABİLİR.",
"swipe_to_bridge": "Köprülemek için kaydırın",
diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb
index 2ebd27f..93ae72c 100644
--- a/res/values/strings_uk.arb
+++ b/res/values/strings_uk.arb
@@ -93,6 +93,7 @@
"available": "Доступно",
"available_balance": "Доступний баланс",
"available_balance_description": "«Доступний баланс» або «Підтверджений баланс» — це кошти, які можна витратити негайно. Якщо кошти відображаються в нижньому балансі, але не у верхньому, потрібно зачекати кілька хвилин, доки вхідні кошти отримають більше підтверджень мережі. Після отримання додаткових підтверджень їх можна буде витратити.",
+ "available_balance_short": "Середня сума: ${amount}",
"avg_savings": "Сер. заощадження",
"avl": "Avl",
"awaitDAppProcessing": "Будь ласка, зачекайте, доки dApp завершить обробку.",
@@ -1150,6 +1151,8 @@
"swap_id": "ID обміну",
"swap_provider_initial_desc": "Виберіть тип провайдера свопу, який ви бажаєте використовувати під час обміну",
"swap_providers": "Провайдери обміну",
+ "swap_unavailable_needs_network_wallet": "Цей тип адреси не може отримати кошти від обміну. Щоб надіслати на цю адресу, вам потрібен гаманець ${network}.",
+ "swap_unavailable_switch_to_network": "Цей тип адреси не може отримати кошти від обміну. Щоб продовжити, перейдіть на гаманець ${network}.",
"sweeping_wallet": "Змітання гаманця",
"sweeping_wallet_alert": "Це не повинно зайняти багато часу. НЕ ЗАЛИШАЙТЕ ЦЕЙ ЕКРАН, ІНАКШЕ ЗМЕТЕНІ КОШТИ МОЖУТЬ БУТИ ВТРАЧЕНІ.",
"swipe_to_bridge": "Проведіть, щоб перенести між мережами",
diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb
index 6f63b73..80e93d2 100644
--- a/res/values/strings_ur.arb
+++ b/res/values/strings_ur.arb
@@ -93,6 +93,7 @@
"available": "دستیاب",
"available_balance": "دستیاب بیلنس",
"available_balance_description": "”دستیاب بیلنس” یا ”تصدیق شدہ بیلنس” وہ فنڈز ہیں جنہیں فوراً خرچ کیا جا سکتا ہے۔ اگر فنڈز نیچے والے بیلنس میں نظر آئیں لیکن اوپر والے بیلنس میں نہیں، تو آپ کو آنے والے فنڈز کے مزید نیٹ ورک کنفرمیشنز حاصل ہونے کے لیے چند منٹ انتظار کرنا ہوگا۔ جب انہیں مزید کنفرمیشنز مل جائیں گی تو وہ خرچ کے قابل ہو جائیں گے۔",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "اوسط بچت",
"avl": "Avl",
"awaitDAppProcessing": "براہِ کرم dApp کی پروسیسنگ مکمل ہونے تک انتظار کریں۔",
@@ -1151,6 +1152,8 @@
"swap_id": "سواپ آئی ڈی",
"swap_provider_initial_desc": "سواپ کرتے وقت استعمال کے لیے اپنی پسند کے سواپ فراہم کنندہ کی قسم منتخب کریں",
"swap_providers": "سویپ فراہم کنندگان",
+ "swap_unavailable_needs_network_wallet": "ایڈریس کی یہ قسم سویپ سے فنڈز وصول نہیں کر سکتی۔ اس پتے پر بھیجنے کے لیے آپ کو ایک ${network} والیٹ کی ضرورت ہے۔",
+ "swap_unavailable_switch_to_network": "ایڈریس کی یہ قسم سویپ سے فنڈز وصول نہیں کر سکتی۔ جاری رکھنے کے لیے اپنے ${network} والیٹ پر جائیں۔",
"sweeping_wallet": "والٹ سویپ کرنا",
"sweeping_wallet_alert": "اس میں زیادہ وقت نہیں لگنا چاہیے۔ اس اسکرین کو مت چھوڑیں، ورنہ سویپ کیے گئے فنڈز ضائع ہو سکتے ہیں۔",
"swipe_to_bridge": "برج کرنے کے لیے سوائپ کریں",
diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb
index 1903946..68fd9af 100644
--- a/res/values/strings_vi.arb
+++ b/res/values/strings_vi.arb
@@ -93,6 +93,7 @@
"available": "Khả dụng",
"available_balance": "Số dư khả dụng",
"available_balance_description": "“Số dư khả dụng” hoặc “Số dư đã xác nhận” là số tiền có thể chi tiêu ngay lập tức. Nếu tiền xuất hiện ở số dư phía dưới nhưng không xuất hiện ở số dư phía trên, thì bạn phải đợi vài phút để khoản tiền đến có thêm xác nhận từ mạng. Sau khi có thêm xác nhận, chúng sẽ có thể chi tiêu.",
+ "available_balance_short": "Trung bình: ${amount}",
"avg_savings": "Tiết kiệm TB",
"avl": "trung bình",
"awaitDAppProcessing": "Vui lòng đợi dApp xử lý xong.",
@@ -1150,6 +1151,8 @@
"swap_id": "ID Swap",
"swap_provider_initial_desc": "Chọn loại nhà cung cấp hoán đổi mà bạn muốn sử dụng khi hoán đổi trong ứng dụng",
"swap_providers": "Nhà cung cấp hoán đổi",
+ "swap_unavailable_needs_network_wallet": "Loại địa chỉ này không thể nhận tiền từ trao đổi. Bạn cần có ví ${network} để gửi tới địa chỉ này.",
+ "swap_unavailable_switch_to_network": "Loại địa chỉ này không thể nhận tiền từ trao đổi. Chuyển sang ví ${network} của bạn để tiếp tục.",
"sweeping_wallet": "Quét ví",
"sweeping_wallet_alert": "Việc này sẽ không mất nhiều thời gian. KHÔNG RỜI KHỎI MÀN HÌNH NÀY, NẾU KHÔNG SỐ TIỀN ĐÃ QUÉT CÓ THỂ BỊ MẤT.",
"swipe_to_bridge": "Vuốt để bắc cầu",
diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb
index d08ab70..6ca70f8 100644
--- a/res/values/strings_yo.arb
+++ b/res/values/strings_yo.arb
@@ -93,6 +93,7 @@
"available": "Ó wà",
"available_balance": "Ìwọ̀ntúnwọ̀nsì tó wà",
"available_balance_description": "“Iwọntunwọnsi Wa” tabi “Iwọntunwọnsi Ti a Jẹ́rìí” jẹ́ àwọn owó tí o lè na lẹ́sẹ̀kẹsẹ. Bí àwọn owó bá hàn ní iwọntunwọnsi ìsàlẹ̀ ṣùgbọ́n kì í hàn ní iwọntunwọnsi òkè, nígbà náà o gbọdọ̀ dúró fún ìṣẹ́jú díẹ̀ kí àwọn owó tí ń wọlé lè gba ìjẹ́rìí nẹ́tíwọ́ọ̀kì púpọ̀ síi. Lẹ́yìn tí wọ́n bá ti gba ìjẹ́rìí púpọ̀ síi, o lè na wọ́n.",
+ "available_balance_short": "Avl: ${amount}",
"avg_savings": "Ìpamọ́ àpapọ̀",
"avl": "Avl",
"awaitDAppProcessing": "Jọ̀wọ́, dúró kí dApp parí ìṣiṣẹ́.",
@@ -1150,6 +1151,8 @@
"swap_id": "ID Swap",
"swap_provider_initial_desc": "Yan iru olupese swap ti o fẹ́ lò nígbà tí o bá ń swap sínú",
"swap_providers": "Awọn olupese swap",
+ "swap_unavailable_needs_network_wallet": "Iru adirẹsi yii ko le gba owo lati swap kan. O nilo apamọwọ ${network} lati fi ranṣẹ si adirẹsi yii.",
+ "swap_unavailable_switch_to_network": "Iru adirẹsi yii ko le gba owo lati swap kan. Yipada si apamọwọ ${network} rẹ lati tẹsiwaju.",
"sweeping_wallet": "Wallet ìkó",
"sweeping_wallet_alert": "Eyi kò yẹ kí ó pẹ́. MÁ ṢE KÚRÒ LÓRÍ IBÓJÚ YÌÍ TÁBÍ OWÓ TÍ A TI SWEEP LÈ SỌ́NÙ.",
"swipe_to_bridge": "Fà láti so ìjápọ̀",
diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb
index c12e808..8e3256b 100644
--- a/res/values/strings_zh.arb
+++ b/res/values/strings_zh.arb
@@ -93,6 +93,7 @@
"available": "可用",
"available_balance": "可用余额",
"available_balance_description": "“可用余额”或“确认余额”是指可立即花费的资金。如果资金显示在下方余额中但未显示在上方余额中,则需要等待几分钟,让转入资金获得更多网络确认。获得更多确认后,这些资金即可花费。",
+ "available_balance_short": "平均价值:${amount}",
"avg_savings": "平均节省",
"avl": "平均",
"awaitDAppProcessing": "请稍候,等待 dApp 完成处理。",
@@ -1150,6 +1151,8 @@
"swap_id": "兑换 ID",
"swap_provider_initial_desc": "选择您在换入时偏好使用的兑换服务提供商类型",
"swap_providers": "兑换服务提供商",
+ "swap_unavailable_needs_network_wallet": "此地址类型无法从交换中接收资金。您需要一个 ${network} 钱包才能发送到此地址。",
+ "swap_unavailable_switch_to_network": "此地址类型无法从交换中接收资金。切换到您的 ${network} 钱包以继续。",
"sweeping_wallet": "正在归集钱包",
"sweeping_wallet_alert": "这应该不会花很长时间。请勿离开此屏幕,否则被归集的资金可能会丢失。",
"swipe_to_bridge": "滑动以跨链桥接",
diff --git a/test/core/anypay/anypay_parser_test.dart b/test/core/anypay/anypay_parser_test.dart
new file mode 100644
index 0000000..76651eb
--- /dev/null
+++ b/test/core/anypay/anypay_parser_test.dart
@@ -0,0 +1,103 @@
+import "package:cake_wallet/core/anypay/anypay_models.dart";
+import "package:cake_wallet/core/anypay/anypay_parser.dart";
+import "package:cake_wallet/utils/payment_request.dart";
+import "package:flutter_test/flutter_test.dart";
+
+void main() {
+ const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
+ const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
+
+ group("chain binding", () {
+ test("explicit ethereum chain binds explicitly", () {
+ final request = AnyPayParser.fromRaw(
+ "ethereum:$recipient@8453?value=1e18",
+ );
+
+ expect(request.chainBinding, isA<ExplicitEvmChain>());
+ expect((request.chainBinding as ExplicitEvmChain).chainId, 8453);
+ });
+
+ test("ethereum without a chain id stays chainless", () {
+ final request = AnyPayParser.fromRaw(
+ "ethereum:$usdtEthContract/transfer?address=$recipient&uint256=1000000",
+ );
+
+ expect(request.chainBinding, isA<ChainlessEvm>());
+ expect(request.hasContract, true);
+ });
+
+ test("non evm schemes bind no evm chain", () {
+ final request = AnyPayParser.fromRaw(
+ "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
+ );
+
+ expect(request.chainBinding, isA<NoEvmBinding>());
+ });
+
+ test("raw evm addresses bind no evm chain", () {
+ final request = AnyPayParser.fromRaw(recipient);
+
+ expect(request.chainBinding, isA<NoEvmBinding>());
+ expect(request.detection.isValid, true);
+ });
+
+ test("an evm chain scheme binds its chain explicitly", () {
+ final request = AnyPayParser.fromRaw("polygon:$recipient");
+
+ expect((request.chainBinding as ExplicitEvmChain).chainId, 137);
+ });
+ });
+
+ group("payment request reconstruction", () {
+ test("an explicit chain request keeps its chain and raw amount", () {
+ final original = PaymentRequest.fromUri(
+ Uri.parse("ethereum:$usdtEthContract@8453/transfer?address=$recipient&uint256=3000000"),
+ );
+
+ final request = AnyPayParser.fromPaymentRequest(original);
+
+ expect(request.rawInput.contains("@8453"), true);
+ expect(request.paymentRequest.rawTokenAmount, "3000000");
+ expect((request.chainBinding as ExplicitEvmChain).chainId, 8453);
+ });
+
+ test("a chainless request rebuilds with a placeholder but keeps the chainless binding", () {
+ final original = PaymentRequest.fromUri(
+ Uri.parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=1000000"),
+ );
+
+ final request = AnyPayParser.fromPaymentRequest(original);
+
+ expect(request.rawInput.contains("@1/"), true);
+ expect(request.chainBinding, isA<ChainlessEvm>());
+ expect(request.detection.isValid, true);
+ });
+
+ test("a tron token request keeps its contract on the payment request", () {
+ final original = PaymentRequest.fromUri(
+ Uri.parse("tron:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t?token=TTokenContract111111111111111111"),
+ );
+
+ final request = AnyPayParser.fromPaymentRequest(original);
+
+ expect(request.contractAddress, "TTokenContract111111111111111111");
+ expect(request.chainBinding, isA<NoEvmBinding>());
+ });
+
+ test("a non evm request rebuilds scheme address and amount", () {
+ final original = PaymentRequest("someaddress", "1.25", "", "monero", null);
+
+ final request = AnyPayParser.fromPaymentRequest(original);
+
+ expect(request.rawInput, "monero:someaddress?amount=1.25");
+ });
+
+ test("a lightning request passes the invoice through untouched", () {
+ final original = PaymentRequest("lnbc1invoice", "", "", "lightning", null);
+
+ final request = AnyPayParser.fromPaymentRequest(original);
+
+ expect(request.rawInput, "lnbc1invoice");
+ });
+ });
+}
diff --git a/test/core/anypay/anypay_resolver_test.dart b/test/core/anypay/anypay_resolver_test.dart
new file mode 100644
index 0000000..0132e81
--- /dev/null
+++ b/test/core/anypay/anypay_resolver_test.dart
@@ -0,0 +1,201 @@
+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:cw_core/crypto_currency.dart";
+import "package:cw_core/erc20_token.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+
+class _FakeTokenLookup implements AnyPayTokenLookup {
+ _FakeTokenLookup({this.onFindToken, this.onFindChainId});
+
+ final Future<CryptoCurrency?> Function({required WalletType walletType, required String address})?
+ onFindToken;
+ final Future<int?> Function(String contractAddress, {int? excludingChainId})? onFindChainId;
+
+ @override
+ Future<CryptoCurrency?> findTokenByAddress({
+ required WalletType walletType,
+ required String address,
+ }) {
+ if (onFindToken == null) {
+ fail("no token lookup expected");
+ }
+ return onFindToken!(walletType: walletType, address: address);
+ }
+
+ @override
+ Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId}) {
+ if (onFindChainId == null) {
+ fail("no chain sweep expected");
+ }
+ return onFindChainId!(contractAddress, excludingChainId: excludingChainId);
+ }
+}
+
+void main() {
+ const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
+ const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
+ const solAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
+ const usdcSolMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
+
+ final usdt = Erc20Token(
+ name: "Tether",
+ symbol: "USDT",
+ contractAddress: usdtEthContract,
+ decimal: 6,
+ );
+
+ WalletSnapshot snapshot({
+ WalletType type = WalletType.ethereum,
+ int? chainId,
+ bool hasEvmProxy = true,
+ }) =>
+ WalletSnapshot(
+ type: type,
+ currentWalletIsEvm:
+ type == WalletType.ethereum || type == WalletType.polygon || type == WalletType.base,
+ currentChainId: chainId,
+ wallets: const [],
+ // The service builds an empty chain map when the evm proxy is absent.
+ supportedEvmChains: hasEvmProxy
+ ? const {
+ 1: WalletType.ethereum,
+ 137: WalletType.polygon,
+ 8453: WalletType.base,
+ }
+ : const {},
+ hasEvmProxy: hasEvmProxy,
+ hasSolanaProxy: true,
+ hasTronProxy: true,
+ );
+
+ AnyPayRequest parse(String input) => AnyPayParser.fromRaw(input);
+
+ test("a request without a contract needs no resolution", () async {
+ final resolver = AnyPayResolver(tokenLookup: _FakeTokenLookup());
+
+ final resolution = await resolver.resolve(
+ parse("ethereum:$recipient@1?value=1e18"),
+ snapshot(chainId: 1),
+ );
+
+ expect(resolution, isA<NoTokenRequested>());
+ });
+
+ test("a token on the explicit target chain resolves with its amount", () async {
+ final lookedUp = <WalletType>[];
+ final resolver = AnyPayResolver(
+ tokenLookup: _FakeTokenLookup(
+ onFindToken: ({required walletType, required address}) async {
+ lookedUp.add(walletType);
+ return walletType == WalletType.ethereum ? usdt : null;
+ },
+ ),
+ );
+
+ final resolution = await resolver.resolve(
+ parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
+ snapshot(type: WalletType.base, chainId: 8453),
+ );
+
+ final resolved = resolution as TokenResolved;
+ expect(lookedUp, [WalletType.ethereum]);
+ expect(resolved.chainId, 1);
+ expect(resolved.token, usdt);
+ expect(resolved.amountOverride, "1");
+ });
+
+ test("an unknown token on an explicit chain is not swept to other chains", () async {
+ bool sweepCalled = false;
+ final resolver = AnyPayResolver(
+ tokenLookup: _FakeTokenLookup(
+ onFindToken: ({required walletType, required address}) async => null,
+ onFindChainId: (contract, {excludingChainId}) async {
+ sweepCalled = true;
+ return 137;
+ },
+ ),
+ );
+
+ final resolution = await resolver.resolve(
+ parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
+ snapshot(chainId: 1),
+ );
+
+ expect(resolution, isA<TokenUnknown>());
+ expect(sweepCalled, false);
+ });
+
+ test("a chainless token unknown on the current chain reroutes through the sweep", () async {
+ final resolver = AnyPayResolver(
+ tokenLookup: _FakeTokenLookup(
+ onFindToken: ({required walletType, required address}) async =>
+ walletType == WalletType.ethereum ? usdt : null,
+ onFindChainId: (contract, {excludingChainId}) async {
+ expect(excludingChainId, 8453);
+ return 1;
+ },
+ ),
+ );
+
+ final resolution = await resolver.resolve(
+ parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
+ snapshot(type: WalletType.base, chainId: 8453),
+ );
+
+ final resolved = resolution as TokenResolved;
+ expect(resolved.chainId, 1);
+ expect(resolved.amountOverride, "2");
+ });
+
+ test("a chainless token unknown everywhere stays unknown", () async {
+ final resolver = AnyPayResolver(
+ tokenLookup: _FakeTokenLookup(
+ onFindToken: ({required walletType, required address}) async => null,
+ onFindChainId: (contract, {excludingChainId}) async => null,
+ ),
+ );
+
+ final resolution = await resolver.resolve(
+ parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
+ snapshot(chainId: 1),
+ );
+
+ expect((resolution as TokenUnknown).contract, usdtEthContract);
+ });
+
+ test("a solana token resolves against the detected wallet type", () async {
+ const usdcSol = CryptoCurrency.usdtSol;
+ final resolver = AnyPayResolver(
+ tokenLookup: _FakeTokenLookup(
+ onFindToken: ({required walletType, required address}) async {
+ expect(walletType, WalletType.solana);
+ expect(address, usdcSolMint);
+ return usdcSol;
+ },
+ ),
+ );
+
+ final resolution = await resolver.resolve(
+ parse("solana:$solAddress?amount=3&spl-token=$usdcSolMint"),
+ snapshot(type: WalletType.monero),
+ );
+
+ final resolved = resolution as TokenResolved;
+ expect(resolved.chainId, null);
+ expect(resolved.token, usdcSol);
+ expect(resolved.amountOverride, "3");
+ });
+
+ test("an evm contract without the evm proxy stays unknown without any lookups", () async {
+ final resolver = AnyPayResolver(tokenLookup: _FakeTokenLookup());
+
+ final resolution = await resolver.resolve(
+ parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
+ snapshot(hasEvmProxy: false),
+ );
+
+ expect(resolution, isA<TokenUnknown>());
+ });
+}
diff --git a/test/core/anypay/anypay_router_test.dart b/test/core/anypay/anypay_router_test.dart
new file mode 100644
index 0000000..681e9d7
--- /dev/null
+++ b/test/core/anypay/anypay_router_test.dart
@@ -0,0 +1,414 @@
+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_router.dart";
+import "package:cake_wallet/reactions/wallet_connect.dart";
+import "package:cw_core/crypto_currency.dart";
+import "package:cw_core/erc20_token.dart";
+import "package:cw_core/wallet_info.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter_test/flutter_test.dart";
+
+void main() {
+ const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
+ const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
+ const btcAddress = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq";
+ const xmrAddress =
+ "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A";
+ const tronAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
+ const solAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
+
+ final usdt = Erc20Token(
+ name: "Tether",
+ symbol: "USDT",
+ contractAddress: usdtEthContract,
+ decimal: 6,
+ );
+
+ WalletInfo walletOf(WalletType type) => WalletInfo.external(
+ id: "id-${type.name}",
+ name: "wallet-${type.name}",
+ type: type,
+ isRecovery: false,
+ restoreHeight: 0,
+ date: DateTime(2026),
+ dirPath: "",
+ path: "",
+ address: "",
+ );
+
+ WalletSnapshot snapshot({
+ WalletType type = WalletType.ethereum,
+ int? chainId,
+ List<WalletInfo> wallets = const [],
+ bool hasEvmProxy = true,
+ bool hasSolanaProxy = true,
+ bool hasTronProxy = true,
+ }) =>
+ WalletSnapshot(
+ type: type,
+ currentWalletIsEvm: isEVMCompatibleChain(type),
+ currentChainId: chainId,
+ wallets: wallets,
+ supportedEvmChains: hasEvmProxy
+ ? const {
+ 1: WalletType.ethereum,
+ 137: WalletType.polygon,
+ 8453: WalletType.base,
+ }
+ : const {},
+ hasEvmProxy: hasEvmProxy,
+ hasSolanaProxy: hasSolanaProxy,
+ hasTronProxy: hasTronProxy,
+ );
+
+ AnyPayRequest parse(String input) => AnyPayParser.fromRaw(input);
+
+ group("evm routing", () {
+ test("explicit chain matching the current wallet applies directly", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$recipient@1?value=1e18"),
+ snapshot(chainId: 1),
+ const NoTokenRequested(),
+ );
+
+ expect(decision, isA<AnyPayApplyToCurrentWallet>());
+ });
+
+ test("explicit chain differing from the current wallet goes cross chain", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$recipient@8453?value=1e18"),
+ snapshot(chainId: 1, wallets: [walletOf(WalletType.base)]),
+ const NoTokenRequested(),
+ );
+
+ expect(decision, isA<AnyPayCrossChainPayment>());
+ final crossChain = decision as AnyPayCrossChainPayment;
+ expect(crossChain.targetChainId, 8453);
+ expect(crossChain.targetWalletType, WalletType.base);
+ expect(crossChain.hasCompatibleWallet, true);
+ });
+
+ test("cross chain with no wallet for the target offers no compatible wallet", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$recipient@8453?value=1e18"),
+ snapshot(chainId: 1),
+ const NoTokenRequested(),
+ );
+
+ expect((decision as AnyPayCrossChainPayment).hasCompatibleWallet, false);
+ });
+
+ test("unsupported explicit chain is rejected with its id", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$recipient@10?value=1e18"),
+ snapshot(chainId: 1),
+ const NoTokenRequested(),
+ );
+
+ expect((decision as AnyPayUnsupportedNetwork).chainId, 10);
+ });
+
+ test("chainless request binds to the current evm chain", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$recipient?value=1e18"),
+ snapshot(type: WalletType.base, chainId: 8453),
+ const NoTokenRequested(),
+ );
+
+ expect(decision, isA<AnyPayApplyToCurrentWallet>());
+ });
+
+ test("chainless request at a non evm wallet targets mainnet cross chain", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$recipient?value=1e18"),
+ snapshot(type: WalletType.monero, wallets: [walletOf(WalletType.ethereum)]),
+ const NoTokenRequested(),
+ );
+
+ expect((decision as AnyPayCrossChainPayment).targetChainId, 1);
+ });
+
+ test("raw evm address at an evm wallet applies directly", () {
+ final decision = AnyPayRouter.route(
+ parse(recipient),
+ snapshot(chainId: 1),
+ const NoTokenRequested(),
+ );
+
+ expect(decision, isA<AnyPayApplyToCurrentWallet>());
+ });
+
+ test("raw evm address at a non evm wallet asks for a network choice", () {
+ final decision = AnyPayRouter.route(
+ parse(recipient),
+ snapshot(type: WalletType.solana),
+ const NoTokenRequested(),
+ );
+
+ expect(decision, isA<AnyPayEvmNetworkChoice>());
+ });
+
+ test("resolved token on the current chain applies with the token", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
+ snapshot(type: WalletType.base, chainId: 8453),
+ TokenResolved(token: usdt, chainId: 8453, amountOverride: "2"),
+ );
+
+ final apply = decision as AnyPayApplyToCurrentWallet;
+ expect(apply.token, usdt);
+ expect(apply.amountOverride, "2");
+ });
+
+ test("chainless token resolved on another chain reroutes there", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
+ snapshot(type: WalletType.base, chainId: 8453, wallets: [walletOf(WalletType.ethereum)]),
+ TokenResolved(token: usdt, chainId: 1, amountOverride: "2"),
+ );
+
+ final crossChain = decision as AnyPayCrossChainPayment;
+ expect(crossChain.targetChainId, 1);
+ expect(crossChain.token, usdt);
+ });
+
+ test("an unknown token never falls back to the native asset", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=2000000"),
+ snapshot(chainId: 1),
+ const TokenUnknown(usdtEthContract),
+ );
+
+ expect(decision, isA<AnyPayUnsupportedToken>());
+ expect((decision as AnyPayUnsupportedToken).contract, usdtEthContract);
+ });
+
+ test("an unsupported network wins over an unknown token", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$usdtEthContract@10/transfer?address=$recipient&uint256=2000000"),
+ snapshot(chainId: 1),
+ const TokenUnknown(usdtEthContract),
+ );
+
+ expect((decision as AnyPayUnsupportedNetwork).chainId, 10);
+ });
+
+ test("evm address without the evm proxy applies as plain input", () {
+ final decision = AnyPayRouter.route(
+ parse(recipient),
+ snapshot(type: WalletType.monero, hasEvmProxy: false),
+ const NoTokenRequested(),
+ );
+
+ expect(decision, isA<AnyPayApplyToCurrentWallet>());
+ expect((decision as AnyPayApplyToCurrentWallet).token, null);
+ });
+ });
+
+ group("non evm routing", () {
+ test("address matching the current wallet applies directly", () {
+ final decision = AnyPayRouter.route(
+ parse(xmrAddress),
+ snapshot(type: WalletType.monero),
+ const NoTokenRequested(),
+ );
+
+ expect(decision, isA<AnyPayApplyToCurrentWallet>());
+ });
+
+ test("btc address at another wallet goes cross chain with its wallets", () {
+ final decision = AnyPayRouter.route(
+ parse(btcAddress),
+ snapshot(type: WalletType.monero, wallets: [walletOf(WalletType.bitcoin)]),
+ const NoTokenRequested(),
+ );
+
+ final crossChain = decision as AnyPayCrossChainPayment;
+ expect(crossChain.targetWalletType, WalletType.bitcoin);
+ expect(crossChain.hasCompatibleWallet, true);
+ });
+
+ test("btc address with no bitcoin wallet leaves only the swap offer", () {
+ final decision = AnyPayRouter.route(
+ parse(btcAddress),
+ snapshot(type: WalletType.monero),
+ const NoTokenRequested(),
+ );
+
+ expect((decision as AnyPayCrossChainPayment).hasCompatibleWallet, false);
+ });
+
+ test("tron address at another wallet goes cross chain to tron", () {
+ final decision = AnyPayRouter.route(
+ parse(tronAddress),
+ snapshot(chainId: 1, wallets: [walletOf(WalletType.tron)]),
+ const NoTokenRequested(),
+ );
+
+ final crossChain = decision as AnyPayCrossChainPayment;
+ expect(crossChain.targetWalletType, WalletType.tron);
+ expect(crossChain.targetChainId, null);
+ expect(crossChain.wallets.length, 1);
+ });
+
+ test("solana address without the solana proxy applies as plain input", () {
+ final decision = AnyPayRouter.route(
+ parse(solAddress),
+ snapshot(chainId: 1, hasSolanaProxy: false),
+ const NoTokenRequested(),
+ );
+
+ expect(decision, isA<AnyPayApplyToCurrentWallet>());
+ });
+
+ test("an unknown non evm token never falls back to the native asset", () {
+ final decision = AnyPayRouter.route(
+ parse("solana:$solAddress?amount=3&spl-token=$solAddress"),
+ snapshot(type: WalletType.solana),
+ const TokenUnknown(solAddress),
+ );
+
+ expect(decision, isA<AnyPayUnsupportedToken>());
+ });
+
+ test("solana token resolved on the current wallet applies with the token", () {
+ const splToken = CryptoCurrency.usdtSol;
+ final decision = AnyPayRouter.route(
+ parse("solana:$solAddress?amount=3&spl-token=$solAddress"),
+ snapshot(type: WalletType.solana),
+ const TokenResolved(token: splToken, amountOverride: "3"),
+ );
+
+ final apply = decision as AnyPayApplyToCurrentWallet;
+ expect(apply.token, splToken);
+ expect(apply.amountOverride, "3");
+ });
+ });
+
+ group("native request currency", () {
+ test("an explicit native request with an amount carries its currency", () {
+ final decision = AnyPayRouter.route(
+ parse("ethereum:$recipient@1?value=2e18"),
+ snapshot(chainId: 1),
+ const NoTokenRequested(),
+ );
+
+ expect((decision as AnyPayApplyToCurrentWallet).fallbackCurrency, CryptoCurrency.eth);
+ });
+
+ test("a raw address carries no fallback currency", () {
+ final decision = AnyPayRouter.route(
+ parse(recipient),
+ snapshot(chainId: 1),
+ const NoTokenRequested(),
+ );
+
+ expect((decision as AnyPayApplyToCurrentWallet).fallbackCurrency, null);
+ });
+ });
+
+ group("degenerate input", () {
+ test("empty input is invalid", () {
+ final decision = AnyPayRouter.route(
+ parse(""),
+ snapshot(chainId: 1),
+ const NoTokenRequested(),
+ );
+
+ expect(decision, isA<AnyPayEmptyInput>());
+ });
+
+ test("undetectable text applies as plain input for the validator to reject", () {
+ final decision = AnyPayRouter.route(
+ parse("definitelynotanaddress"),
+ snapshot(chainId: 1),
+ const NoTokenRequested(),
+ );
+
+ expect(decision, isA<AnyPayApplyToCurrentWallet>());
+ });
+ });
+
+ group("requested evm chain", () {
+ test("explicit binding wins over the wallet chain", () {
+ final request = parse("ethereum:$recipient@137?value=1e18");
+
+ expect(AnyPayRouter.requestedEvmChainId(request, snapshot(chainId: 1)), 137);
+ });
+
+ test("chainless binding takes the wallet chain", () {
+ final request = parse("ethereum:$recipient?value=1e18");
+
+ expect(
+ AnyPayRouter.requestedEvmChainId(
+ request,
+ snapshot(type: WalletType.base, chainId: 8453),
+ ),
+ 8453,
+ );
+ });
+
+ test("chainless binding defaults to mainnet without an evm wallet", () {
+ final request = parse("ethereum:$recipient?value=1e18");
+
+ expect(
+ AnyPayRouter.requestedEvmChainId(request, snapshot(type: WalletType.monero)),
+ 1,
+ );
+ });
+
+ test("raw addresses bind no target", () {
+ expect(AnyPayRouter.requestedEvmChainId(parse(recipient), snapshot(chainId: 1)), null);
+ });
+ });
+
+ group("swap incompatible recipients", () {
+ final spAddress = "sp1${"q" * 113}";
+ final mwebAddress = "ltcmweb1q${"a" * 90}";
+
+ test("a silent payment address with a bitcoin wallet switches without a swap offer", () {
+ final decision = AnyPayRouter.route(
+ parse(spAddress),
+ snapshot(type: WalletType.monero, wallets: [walletOf(WalletType.bitcoin)]),
+ const NoTokenRequested(),
+ );
+
+ final crossChain = decision as AnyPayCrossChainPayment;
+ expect(crossChain.targetWalletType, WalletType.bitcoin);
+ expect(crossChain.hasCompatibleWallet, true);
+ expect(crossChain.canSwap, false);
+ });
+
+ test("a silent payment address with no bitcoin wallet cannot swap either", () {
+ final decision = AnyPayRouter.route(
+ parse(spAddress),
+ snapshot(type: WalletType.monero),
+ const NoTokenRequested(),
+ );
+
+ final crossChain = decision as AnyPayCrossChainPayment;
+ expect(crossChain.hasCompatibleWallet, false);
+ expect(crossChain.canSwap, false);
+ });
+
+ test("an mweb address never offers a swap", () {
+ final decision = AnyPayRouter.route(
+ parse(mwebAddress),
+ snapshot(),
+ const NoTokenRequested(),
+ );
+
+ expect((decision as AnyPayCrossChainPayment).canSwap, false);
+ });
+
+ test("a plain btc address keeps the swap offer", () {
+ final decision = AnyPayRouter.route(
+ parse(btcAddress),
+ snapshot(type: WalletType.monero),
+ const NoTokenRequested(),
+ );
+
+ expect((decision as AnyPayCrossChainPayment).canSwap, true);
+ });
+ });
+}
diff --git a/test/core/universal_address_detector_test.dart b/test/core/universal_address_detector_test.dart
index 9525aed..c74afe5 100644
--- a/test/core/universal_address_detector_test.dart
+++ b/test/core/universal_address_detector_test.dart
@@ -196,6 +196,38 @@ void main() {
});
});
+ group("Zcash Address Detection", () {
+ test("detects Zcash transparent address", () {
+ const address = "t1V9mnyk5Z5cTNMCkLbaDwSskgJZucTLdgW";
+ final result = UniversalAddressDetector.detectAddress(address);
+
+ expect(result.isValid, true);
+ expect(result.detectedCurrency, CryptoCurrency.zec);
+ expect(result.detectedWalletType, WalletType.zcash);
+ expect(result.address, address);
+ });
+
+ test("detects Zcash shielded address", () {
+ const address =
+ "zsab12cd34efab12cd34efab12cd34efab12cd34efab12cd34efab12cd34efab12cd34efgh56kl";
+ final result = UniversalAddressDetector.detectAddress(address);
+
+ expect(result.isValid, true);
+ expect(result.detectedCurrency, CryptoCurrency.zec);
+ expect(result.detectedWalletType, WalletType.zcash);
+ });
+
+ test("detects Zcash unified address", () {
+ const address =
+ "u1z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5";
+ final result = UniversalAddressDetector.detectAddress(address);
+
+ expect(result.isValid, true);
+ expect(result.detectedCurrency, CryptoCurrency.zec);
+ expect(result.detectedWalletType, WalletType.zcash);
+ });
+ });
+
group('Wownero Address Detection', () {
test('detects Wownero address', () {
const address =
Why this scored 38/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.