CW1612: AnyPay Service Layer (#3516)

* fix android CI * feat: add anypay core models, parser and routing engine * feat: add anypay resolver, anypay service and wallet switch service * refactor: route the send page payment flow through anypay service * fix: show message when there's no swap providers for a pair and reset the trade guard on dismissal * refactor: merge the swap from network and send to network decision pages together and clean up anypay widgets * feat: reapply deep links to a currently open send flow and register the solana scheme * feat: add support for zcash address detection to anypay and other minor fixes * refactor: remove trailing icon * feat: hide swap flow for sp and mweb addresses * feat: enhance zcash address detection and deeplink handling * fix: pin bitcoin_base * refactor: Move the anypay decision navigation out of the send page and address minor review comments * refactor: Move the anypay decision navigation out of the send page and address minor review comments * feat: handle exception in switch service and mvoe balance compute to call site * refactor: combine anypay entry points to one and extract selected mode widget --------- Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>

David Adegoke committed Aug 20, 2026 at 08:35 UTC 3b100eae33b8e4e6dc79746e590bb29c924eee7e
67 files changed +2652 -1190
android/app/src/main/AndroidManifestBase.xml
+2
@@ -90,7 +90,9 @@
90 <data android:scheme="polygon" />
91 <data android:scheme="polygon-wallet" />
92 <data android:scheme="polygon_wallet" />
93 + <data android:scheme="solana" />
94 <data android:scheme="solana-wallet" />
95 + <data android:scheme="solana_wallet" />
96 <data android:scheme="tron" />
97 <data android:scheme="tron-wallet" />
98 <data android:scheme="tron_wallet" />
cw_bitcoin/pubspec.yaml
+1 -1
@@ -88,7 +88,7 @@ dependency_overrides:
88 bitcoin_base:
89 git:
90 url: https://github.com/cake-tech/bitcoin_base
91 - ref: cake-update-v11
91 + ref: bda0e2904effa799ec835b4578cc3fc5d31caa78
92 pointycastle: 3.7.4
93 ffi: 2.1.0
94 intl: any
cw_bitcoin_cash/pubspec.yaml
+1 -1
@@ -43,7 +43,7 @@ dependency_overrides:
43 bitcoin_base:
44 git:
45 url: https://github.com/cake-tech/bitcoin_base
46 - ref: cake-update-v11
46 + ref: bda0e2904effa799ec835b4578cc3fc5d31caa78
47
48 # For information on the generic Dart part of this file, see the
49 # following page: https://dart.dev/tools/pub/pubspec
cw_core/lib/crypto_currency.dart
+1
@@ -1007,6 +1007,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
1007 'bitcoin': btc,
1008 'bitcoincash': bch,
1009 'polygon': maticpoly,
1010 + 'solana': sol,
1011 'nano-gpt': nano,
1012 'secret': scrt,
1013 'stellar': xlm,
cw_core/lib/exceptions.dart
+9
@@ -70,3 +70,12 @@ class WalletDeprecationException implements Exception {
70
71 WalletDeprecationException({required this.seed, required this.curr});
72 }
73 +
74 +class WalletSwitchException implements Exception {
75 + WalletSwitchException(this.message);
76 +
77 + final String message;
78 +
79 + @override
80 + String toString() => message;
81 +}
cw_dogecoin/pubspec.yaml
+1 -1
@@ -40,7 +40,7 @@ dependency_overrides:
40 bitcoin_base:
41 git:
42 url: https://github.com/cake-tech/bitcoin_base
43 - ref: cake-update-v11
43 + ref: bda0e2904effa799ec835b4578cc3fc5d31caa78
44
45
46 # For information on the generic Dart part of this file, see the
ios/Runner/InfoBase.plist
+2
@@ -199,7 +199,9 @@
199 <string>solana-wallet</string>
200 <key>CFBundleURLSchemes</key>
201 <array>
202 + <string>solana</string>
203 <string>solana-wallet</string>
204 + <string>solana_wallet</string>
205 </array>
206 </dict>
207 <dict>
lib/core/address_validator.dart
+4 -14
@@ -177,13 +177,7 @@ class AddressValidator extends TextValidator {
177 if (ZcashNetworkType.isDevNetwork(network)) {
178 return null;
179 }
180 - pattern = '(?:'
181 - 't1[0-9A-Za-z]{33}'
182 - '|t3[0-9A-Za-z]{33}'
183 - '|zs[a-z0-9]{76}'
184 - '|zxviews[a-z0-9]{278}'
185 - '|u1[a-z0-9]{1,300}'
186 - ')';
180 + pattern = "(?:$zcashAddressPattern|zxviews[a-z0-9]{278})";
181 case CryptoCurrency.dcr:
182 pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+';
183 case CryptoCurrency.rvn:
@@ -355,6 +349,8 @@ class AddressValidator extends TextValidator {
349 static String get silentPaymentAddressPatternMainnet => 'sp1[0-9a-zA-Z]{113}';
350 static String get silentPaymentAddressPatternTestnet => '(tsp|sprt)1[0-9a-zA-Z]{113}';
351 static String get mWebAddressPattern => MwebAddress.regex.pattern;
352 + static String get zcashAddressPattern =>
353 + "t1[0-9A-Za-z]{33}|t3[0-9A-Za-z]{33}|zs[a-z0-9]{76}|u1[a-z0-9]{1,300}";
354
355 static const String bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$';
356 static const String bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$';
@@ -401,13 +397,7 @@ class AddressValidator extends TextValidator {
397 case CryptoCurrency.zano:
398 pattern = '([1-9A-HJ-NP-Za-km-z]{90,200})|(@[\w\d.-]+)';
399 case CryptoCurrency.zec:
404 - pattern = '(?:'
405 - 't1[0-9A-Za-z]{33}'
406 - '|t3[0-9A-Za-z]{33}'
407 - '|zs[a-z0-9]{76}'
408 - '|zxviews[a-z0-9]{278}'
409 - '|u1[a-z0-9]{1,300}'
410 - ')';
400 + pattern = "(?:$zcashAddressPattern|zxviews[a-z0-9]{278})";
401 default:
402 if (type.tag == CryptoCurrency.eth.title) {
403 pattern = '0x[0-9a-zA-Z]+';
lib/core/anypay/anypay_models.dart new
+174
@@ -0,0 +1,174 @@
1 +import "package:cake_wallet/core/universal_address_detector.dart";
2 +import "package:cake_wallet/utils/payment_request.dart";
3 +import "package:cw_core/amount/money.dart";
4 +import "package:cw_core/crypto_currency.dart";
5 +import "package:cw_core/wallet_info.dart";
6 +import "package:cw_core/wallet_type.dart";
7 +
8 +sealed class ChainBinding {
9 + const ChainBinding();
10 +}
11 +
12 +class ExplicitEvmChain extends ChainBinding {
13 + const ExplicitEvmChain(this.chainId);
14 +
15 + final int chainId;
16 +}
17 +
18 +class ChainlessEvm extends ChainBinding {
19 + const ChainlessEvm();
20 +}
21 +
22 +class NoEvmBinding extends ChainBinding {
23 + const NoEvmBinding();
24 +}
25 +
26 +class AnyPayRequest {
27 + AnyPayRequest({
28 + required this.rawInput,
29 + required this.paymentRequest,
30 + required this.detection,
31 + required this.chainBinding,
32 + });
33 +
34 + final String rawInput;
35 + final PaymentRequest paymentRequest;
36 + final AddressDetectionResult detection;
37 + final ChainBinding chainBinding;
38 +
39 + String get address => paymentRequest.address;
40 +
41 + String get amount => paymentRequest.amount;
42 +
43 + String? get contractAddress => paymentRequest.contractAddress;
44 +
45 + bool get hasContract => contractAddress != null && contractAddress!.isNotEmpty;
46 +
47 + bool get isLightning => detection.detectedCurrency == CryptoCurrency.btcln;
48 +}
49 +
50 +class WalletSnapshot {
51 + WalletSnapshot({
52 + required this.type,
53 + required this.currentWalletIsEvm,
54 + required this.currentChainId,
55 + required this.wallets,
56 + required this.supportedEvmChains,
57 + required this.hasEvmProxy,
58 + required this.hasSolanaProxy,
59 + required this.hasTronProxy,
60 + });
61 +
62 + final WalletType type;
63 + final bool currentWalletIsEvm;
64 + final int? currentChainId;
65 + final List<WalletInfo> wallets;
66 + final Map<int, WalletType> supportedEvmChains;
67 + final bool hasEvmProxy;
68 + final bool hasSolanaProxy;
69 + final bool hasTronProxy;
70 +
71 + List<WalletInfo> walletsOfType(WalletType walletType) =>
72 + wallets.where((wallet) => wallet.type == walletType).toList();
73 +}
74 +
75 +sealed class AnyPayResolution {
76 + const AnyPayResolution();
77 +}
78 +
79 +class NoTokenRequested extends AnyPayResolution {
80 + const NoTokenRequested();
81 +}
82 +
83 +class TokenResolved extends AnyPayResolution {
84 + const TokenResolved({required this.token, this.chainId, this.amountOverride});
85 +
86 + final CryptoCurrency token;
87 + final int? chainId;
88 + final String? amountOverride;
89 +}
90 +
91 +class TokenUnknown extends AnyPayResolution {
92 + const TokenUnknown(this.contract);
93 +
94 + final String contract;
95 +}
96 +
97 +sealed class AnyPayDecision {
98 + const AnyPayDecision();
99 +}
100 +
101 +class AnyPayApplyToCurrentWallet extends AnyPayDecision {
102 + const AnyPayApplyToCurrentWallet({
103 + this.token,
104 + this.amountOverride,
105 + this.fallbackCurrency,
106 + });
107 +
108 + final CryptoCurrency? token;
109 + final String? amountOverride;
110 + final CryptoCurrency? fallbackCurrency;
111 +}
112 +
113 +class AnyPayEvmNetworkChoice extends AnyPayDecision {
114 + const AnyPayEvmNetworkChoice();
115 +}
116 +
117 +class AnyPayCrossChainPayment extends AnyPayDecision {
118 + const AnyPayCrossChainPayment({
119 + required this.targetWalletType,
120 + required this.wallets,
121 + this.targetChainId,
122 + this.token,
123 + this.amountOverride,
124 + this.canSwap = true,
125 + });
126 +
127 + final WalletType targetWalletType;
128 + final int? targetChainId;
129 + final List<WalletInfo> wallets;
130 + final CryptoCurrency? token;
131 + final String? amountOverride;
132 + final bool canSwap;
133 +
134 + bool get hasCompatibleWallet => wallets.isNotEmpty;
135 +}
136 +
137 +class AnyPayUnsupportedNetwork extends AnyPayDecision {
138 + const AnyPayUnsupportedNetwork(this.chainId);
139 +
140 + final int chainId;
141 +}
142 +
143 +class AnyPayUnsupportedToken extends AnyPayDecision {
144 + const AnyPayUnsupportedToken(this.contract);
145 +
146 + final String contract;
147 +}
148 +
149 +class AnyPayEmptyInput extends AnyPayDecision {
150 + const AnyPayEmptyInput();
151 +}
152 +
153 +class AnyPayEvaluation {
154 + AnyPayEvaluation({required this.request, required this.decision});
155 +
156 + final AnyPayRequest request;
157 + final AnyPayDecision decision;
158 +}
159 +
160 +class AnyPaySwapIntent {
161 + AnyPaySwapIntent({
162 + required this.request,
163 + required this.receiveCurrency,
164 + required this.targetWalletType,
165 + this.receiveAmount,
166 + });
167 +
168 + final AnyPayRequest request;
169 + final CryptoCurrency receiveCurrency;
170 + final WalletType targetWalletType;
171 + final Money? receiveAmount;
172 +
173 + String get recipientAddress => request.address;
174 +}
lib/core/anypay/anypay_parser.dart new
+71
@@ -0,0 +1,71 @@
1 +import "package:cake_wallet/core/anypay/anypay_models.dart";
2 +import "package:cake_wallet/core/universal_address_detector.dart";
3 +import "package:cake_wallet/utils/payment_request.dart";
4 +import "package:cw_core/crypto_currency.dart";
5 +import "package:cw_core/currency_for_wallet_type.dart";
6 +import "package:cw_core/payment_uris.dart";
7 +
8 +class AnyPayParser {
9 + static AnyPayRequest fromRaw(String input) {
10 + final paymentRequest = PaymentRequest.fromString(input);
11 +
12 + return AnyPayRequest(
13 + rawInput: input,
14 + paymentRequest: paymentRequest,
15 + detection: UniversalAddressDetector.detectAddress(input),
16 + chainBinding: _bindingFor(paymentRequest),
17 + );
18 + }
19 +
20 + static AnyPayRequest fromPaymentRequest(PaymentRequest request) {
21 + final raw = _reconstructRawInput(request);
22 +
23 + return AnyPayRequest(
24 + rawInput: raw,
25 + paymentRequest: request,
26 + detection: UniversalAddressDetector.detectAddress(raw),
27 + chainBinding: _bindingFor(request),
28 + );
29 + }
30 +
31 + static String _reconstructRawInput(PaymentRequest request) {
32 + final scheme = request.scheme.toLowerCase();
33 +
34 + if (scheme.isEmpty || scheme == "lightning") {
35 + return request.address;
36 + }
37 +
38 + if (scheme == "ethereum") {
39 + return ERC681URI(
40 + address: request.address,
41 + amount: request.amount,
42 + contractAddress: request.contractAddress,
43 + chainId: request.chainId ?? 1,
44 + rawTokenAmount: request.rawTokenAmount,
45 + ).toString();
46 + }
47 +
48 + final amount = request.amount.isNotEmpty ? "?amount=${request.amount}" : "";
49 + return "${request.scheme}:${request.address}$amount";
50 + }
51 +
52 + static ChainBinding _bindingFor(PaymentRequest request) {
53 + final scheme = request.scheme.toLowerCase();
54 +
55 + if (scheme == "ethereum") {
56 + final chainId = request.chainId;
57 + return chainId != null ? ExplicitEvmChain(chainId) : const ChainlessEvm();
58 + }
59 +
60 + if (scheme.isNotEmpty) {
61 + try {
62 + final chainId = getChainIdByCryptoCurrency(CryptoCurrency.fromString(scheme));
63 + if (chainId != null) {
64 + return ExplicitEvmChain(chainId);
65 + }
66 + } catch (_) {}
67 + }
68 +
69 + return const NoEvmBinding();
70 + }
71 +}
lib/core/anypay/anypay_resolver.dart new
+109
@@ -0,0 +1,109 @@
1 +import "package:cake_wallet/core/anypay/anypay_models.dart";
2 +import "package:cake_wallet/core/anypay/anypay_router.dart";
3 +import "package:cake_wallet/reactions/wallet_connect.dart";
4 +import "package:cake_wallet/utils/token_utilities.dart";
5 +import "package:cw_core/crypto_currency.dart";
6 +import "package:cw_core/wallet_type.dart";
7 +
8 +class AnyPayResolver {
9 + AnyPayResolver({AnyPayTokenLookup? tokenLookup})
10 + : _tokenLookup = tokenLookup ?? const TokenUtilitiesLookup();
11 +
12 + final AnyPayTokenLookup _tokenLookup;
13 +
14 + Future<AnyPayResolution> resolve(AnyPayRequest request, WalletSnapshot snapshot) async {
15 + if (!request.hasContract) {
16 + return const NoTokenRequested();
17 + }
18 +
19 + final contract = request.contractAddress!;
20 + final detectedType = request.detection.detectedWalletType;
21 +
22 + if (detectedType != null && isEVMCompatibleChain(detectedType)) {
23 + return _resolveEvm(request, snapshot, contract);
24 + }
25 +
26 + final token = await _tokenLookup.findTokenByAddress(
27 + walletType: detectedType ?? snapshot.type,
28 + address: contract,
29 + );
30 +
31 + if (token == null) {
32 + return TokenUnknown(contract);
33 + }
34 +
35 + return TokenResolved(
36 + token: token,
37 + amountOverride: request.paymentRequest.resolveTokenAmount(token),
38 + );
39 + }
40 +
41 + Future<AnyPayResolution> _resolveEvm(
42 + AnyPayRequest request,
43 + WalletSnapshot snapshot,
44 + String contract,
45 + ) async {
46 + final requestedChainId = AnyPayRouter.requestedEvmChainId(request, snapshot) ?? 1;
47 +
48 + final lookupType = snapshot.supportedEvmChains[requestedChainId];
49 + if (lookupType != null) {
50 + final token =
51 + await _tokenLookup.findTokenByAddress(walletType: lookupType, address: contract);
52 + if (token != null) {
53 + return TokenResolved(
54 + token: token,
55 + chainId: requestedChainId,
56 + amountOverride: request.paymentRequest.resolveTokenAmount(token),
57 + );
58 + }
59 + }
60 +
61 + // The QRs from our old app versions omit the chainId on mainnet, so a contract the target
62 + // network does not know may still belong to another EVM network.
63 + if (request.chainBinding is ChainlessEvm) {
64 + final reboundChainId = await _tokenLookup.findEvmChainIdForContract(
65 + contract,
66 + excludingChainId: requestedChainId,
67 + );
68 + final reboundType =
69 + reboundChainId != null ? snapshot.supportedEvmChains[reboundChainId] : null;
70 + if (reboundType != null) {
71 + final token =
72 + await _tokenLookup.findTokenByAddress(walletType: reboundType, address: contract);
73 + if (token != null) {
74 + return TokenResolved(
75 + token: token,
76 + chainId: reboundChainId,
77 + amountOverride: request.paymentRequest.resolveTokenAmount(token),
78 + );
79 + }
80 + }
81 + }
82 +
83 + return TokenUnknown(contract);
84 + }
85 +}
86 +
87 +abstract class AnyPayTokenLookup {
88 + Future<CryptoCurrency?> findTokenByAddress({
89 + required WalletType walletType,
90 + required String address,
91 + });
92 +
93 + Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId});
94 +}
95 +
96 +class TokenUtilitiesLookup implements AnyPayTokenLookup {
97 + const TokenUtilitiesLookup();
98 +
99 + @override
100 + Future<CryptoCurrency?> findTokenByAddress({
101 + required WalletType walletType,
102 + required String address,
103 + }) =>
104 + TokenUtilities.findTokenByAddress(walletType: walletType, address: address);
105 +
106 + @override
107 + Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId}) =>
108 + TokenUtilities.findEvmChainIdForContract(contractAddress, excludingChainId: excludingChainId);
109 +}
lib/core/anypay/anypay_router.dart new
+141
@@ -0,0 +1,141 @@
1 +import "package:cake_wallet/core/address_validator.dart";
2 +import "package:cake_wallet/core/anypay/anypay_models.dart";
3 +import "package:cake_wallet/reactions/wallet_connect.dart";
4 +import "package:cw_core/crypto_currency.dart";
5 +import "package:cw_core/wallet_type.dart";
6 +
7 +class AnyPayRouter {
8 + static final List<RegExp> _swapIncompatibleAddressPatterns = [
9 + RegExp("^(?:${AddressValidator.silentPaymentAddressPatternMainnet})\$"),
10 + RegExp("^(?:${AddressValidator.silentPaymentAddressPatternTestnet})\$"),
11 + RegExp("^(?:${AddressValidator.mWebAddressPattern})\$"),
12 + ];
13 +
14 + static bool _isSwapIncompatibleAddress(String address) =>
15 + _swapIncompatibleAddressPatterns.any((pattern) => pattern.hasMatch(address));
16 +
17 + static AnyPayDecision route(
18 + AnyPayRequest request,
19 + WalletSnapshot snapshot,
20 + AnyPayResolution resolution,
21 + ) {
22 + if (request.rawInput.trim().isEmpty) {
23 + return const AnyPayEmptyInput();
24 + }
25 +
26 + final detection = request.detection;
27 + final detectedType = detection.detectedWalletType;
28 +
29 + if (!detection.isValid || detectedType == null) {
30 + return const AnyPayApplyToCurrentWallet();
31 + }
32 +
33 + if (detectedType == WalletType.solana && !snapshot.hasSolanaProxy) {
34 + return const AnyPayApplyToCurrentWallet();
35 + }
36 +
37 + if (detectedType == WalletType.tron && !snapshot.hasTronProxy) {
38 + return const AnyPayApplyToCurrentWallet();
39 + }
40 +
41 + if (isEVMCompatibleChain(detectedType)) {
42 + if (!snapshot.hasEvmProxy) {
43 + return const AnyPayApplyToCurrentWallet();
44 + }
45 +
46 + return _routeEvm(request, snapshot, resolution);
47 + }
48 +
49 + if (resolution is TokenUnknown) {
50 + return AnyPayUnsupportedToken(resolution.contract);
51 + }
52 +
53 + if (detectedType == snapshot.type) {
54 + return AnyPayApplyToCurrentWallet(
55 + token: resolution is TokenResolved ? resolution.token : null,
56 + amountOverride: resolution is TokenResolved ? resolution.amountOverride : null,
57 + fallbackCurrency: _nativeRequestCurrency(request),
58 + );
59 + }
60 +
61 + return AnyPayCrossChainPayment(
62 + targetWalletType: detectedType,
63 + wallets: snapshot.walletsOfType(detectedType),
64 + token: resolution is TokenResolved ? resolution.token : null,
65 + amountOverride: resolution is TokenResolved ? resolution.amountOverride : null,
66 + canSwap: !_isSwapIncompatibleAddress(request.address),
67 + );
68 + }
69 +
70 + static AnyPayDecision _routeEvm(
71 + AnyPayRequest request,
72 + WalletSnapshot snapshot,
73 + AnyPayResolution resolution,
74 + ) {
75 + final requestedChainId = requestedEvmChainId(request, snapshot);
76 +
77 + if (requestedChainId == null) {
78 + if (snapshot.currentWalletIsEvm) {
79 + return const AnyPayApplyToCurrentWallet();
80 + }
81 +
82 + return const AnyPayEvmNetworkChoice();
83 + }
84 +
85 + if (!snapshot.supportedEvmChains.containsKey(requestedChainId)) {
86 + return AnyPayUnsupportedNetwork(requestedChainId);
87 + }
88 +
89 + if (resolution is TokenUnknown) {
90 + return AnyPayUnsupportedToken(resolution.contract);
91 + }
92 +
93 + int targetChainId = requestedChainId;
94 + if (resolution is TokenResolved && resolution.chainId != null) {
95 + targetChainId = resolution.chainId!;
96 + }
97 +
98 + final token = resolution is TokenResolved ? resolution.token : null;
99 + final amountOverride = resolution is TokenResolved ? resolution.amountOverride : null;
100 +
101 + if (snapshot.currentWalletIsEvm && snapshot.currentChainId == targetChainId) {
102 + return AnyPayApplyToCurrentWallet(
103 + token: token,
104 + amountOverride: amountOverride,
105 + fallbackCurrency: _nativeRequestCurrency(request),
106 + );
107 + }
108 +
109 + final targetWalletType = snapshot.supportedEvmChains[targetChainId]!;
110 +
111 + return AnyPayCrossChainPayment(
112 + targetWalletType: targetWalletType,
113 + targetChainId: targetChainId,
114 + wallets: snapshot.walletsOfType(targetWalletType),
115 + token: token,
116 + amountOverride: amountOverride,
117 + );
118 + }
119 +
120 + static CryptoCurrency? _nativeRequestCurrency(AnyPayRequest request) {
121 + if (request.paymentRequest.scheme.isEmpty || request.hasContract || request.amount.isEmpty) {
122 + return null;
123 + }
124 + return request.detection.detectedCurrency;
125 + }
126 +
127 + static int? requestedEvmChainId(AnyPayRequest request, WalletSnapshot snapshot) {
128 + final binding = request.chainBinding;
129 +
130 + if (binding is ExplicitEvmChain) {
131 + return binding.chainId;
132 + }
133 +
134 + if (binding is ChainlessEvm) {
135 + // the eip 681 spec says we should use the current chain id if there is no chainId in the incoming request
136 + return snapshot.currentWalletIsEvm ? snapshot.currentChainId ?? 1 : 1;
137 + }
138 +
139 + return null;
140 + }
141 +}
lib/core/anypay/anypay_service.dart new
+140
@@ -0,0 +1,140 @@
1 +import "package:cake_wallet/core/anypay/anypay_models.dart";
2 +import "package:cake_wallet/core/anypay/anypay_parser.dart";
3 +import "package:cake_wallet/core/anypay/anypay_resolver.dart";
4 +import "package:cake_wallet/core/anypay/anypay_router.dart";
5 +import "package:cake_wallet/evm/evm.dart";
6 +import "package:cake_wallet/new-ui/services/wallet_switch_service.dart";
7 +import "package:cake_wallet/reactions/wallet_connect.dart";
8 +import "package:cake_wallet/solana/solana.dart";
9 +import "package:cake_wallet/store/app_store.dart";
10 +import "package:cake_wallet/tron/tron.dart";
11 +import "package:cake_wallet/utils/payment_request.dart";
12 +import "package:cw_core/amount/money.dart";
13 +import "package:cw_core/crypto_currency.dart";
14 +import "package:cw_core/currency_for_wallet_type.dart";
15 +import "package:cw_core/utils/print_verbose.dart";
16 +import "package:cw_core/wallet_info.dart";
17 +import "package:cw_core/wallet_type.dart";
18 +
19 +class AnyPayService {
20 + AnyPayService({
21 + required this.appStore,
22 + required this.walletSwitchService,
23 + AnyPayResolver? resolver,
24 + }) : resolver = resolver ?? AnyPayResolver();
25 +
26 + final AppStore appStore;
27 + final WalletSwitchService walletSwitchService;
28 + final AnyPayResolver resolver;
29 +
30 + Future<AnyPayEvaluation> evaluateRawInput(String input) => _evaluate(AnyPayParser.fromRaw(input));
31 +
32 + Future<AnyPayEvaluation> evaluatePaymentRequest(PaymentRequest request) =>
33 + _evaluate(AnyPayParser.fromPaymentRequest(request));
34 +
35 + Future<AnyPayEvaluation> evaluateForEvmChain(AnyPayRequest request, int chainId) => _evaluate(
36 + AnyPayRequest(
37 + rawInput: request.rawInput,
38 + paymentRequest: request.paymentRequest,
39 + detection: request.detection,
40 + chainBinding: ExplicitEvmChain(chainId),
41 + ),
42 + );
43 +
44 + Future<AnyPayEvaluation> _evaluate(AnyPayRequest request) async {
45 + try {
46 + final snapshot = await _buildWalletSnapshot();
47 + final resolution = await resolver.resolve(request, snapshot);
48 +
49 + return AnyPayEvaluation(
50 + request: request,
51 + decision: AnyPayRouter.route(request, snapshot, resolution),
52 + );
53 + } catch (e) {
54 + printV("anypay evaluation failed: $e");
55 + return AnyPayEvaluation(
56 + request: request,
57 + decision: const AnyPayApplyToCurrentWallet(),
58 + );
59 + }
60 + }
61 +
62 + Future<WalletSnapshot> _buildWalletSnapshot() async {
63 + final wallet = appStore.wallet!;
64 + final currentWalletIsEvm = isEVMCompatibleChain(wallet.type);
65 +
66 + final supportedEvmChains = <int, WalletType>{};
67 + if (evm != null) {
68 + for (final chain in evm!.getAllChains()) {
69 + final walletType = evm!.getWalletTypeByChainId(chain.chainId);
70 + if (walletType != null) {
71 + supportedEvmChains[chain.chainId] = walletType;
72 + }
73 + }
74 + }
75 +
76 + return WalletSnapshot(
77 + type: wallet.type,
78 + currentWalletIsEvm: currentWalletIsEvm,
79 + currentChainId: currentWalletIsEvm && evm != null
80 + ? evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type)
81 + : null,
82 + wallets: await WalletInfo.getAll(),
83 + supportedEvmChains: supportedEvmChains,
84 + hasEvmProxy: evm != null,
85 + hasSolanaProxy: solana != null,
86 + hasTronProxy: tron != null,
87 + );
88 + }
89 +
90 + AnyPaySwapIntent buildSwapIntent(
91 + AnyPayRequest request, {
92 + required WalletType targetWalletType,
93 + int? targetChainId,
94 + CryptoCurrency? token,
95 + }) {
96 + final receiveCurrency = token ??
97 + (targetChainId != null
98 + ? walletTypeToCryptoCurrency(targetWalletType, chainId: targetChainId)
99 + : request.detection.detectedCurrency ?? walletTypeToCryptoCurrency(targetWalletType));
100 +
101 + final amountValue = token != null
102 + ? request.paymentRequest.resolveTokenAmount(token)
103 + : (request.amount.isNotEmpty ? request.amount : null);
104 +
105 + return AnyPaySwapIntent(
106 + request: request,
107 + receiveCurrency: receiveCurrency,
108 + targetWalletType: targetWalletType,
109 + receiveAmount: amountValue != null ? Money.tryParse(amountValue, receiveCurrency) : null,
110 + );
111 + }
112 +
113 + Future<bool> switchWalletForPayment(WalletInfo walletInfo, {int? chainId}) async {
114 + try {
115 + await walletSwitchService.switchToWallet(walletInfo);
116 + } catch (e) {
117 + printV("wallet switch failed: $e");
118 + return false;
119 + }
120 +
121 + final wallet = appStore.wallet!;
122 + if (chainId != null && evm != null && isEVMCompatibleChain(wallet.type)) {
123 + try {
124 + final node = appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId);
125 + await evm!.selectChain(wallet, chainId, node: node);
126 + } catch (e, s) {
127 + printV("switchWalletForPayment chain select failed: $e\n$s");
128 + return false;
129 + }
130 + }
131 +
132 + try {
133 + await wallet.updateBalance();
134 + } catch (e) {
135 + printV("balance refresh after wallet switch failed: $e");
136 + }
137 +
138 + return true;
139 + }
140 +}
lib/core/universal_address_detector.dart
+6
@@ -238,6 +238,12 @@ class UniversalAddressDetector {
238 currency: CryptoCurrency.doge,
239 ),
240
241 + // Zcash (transparent, Sapling shielded, unified)
242 + _DetectionPattern(
243 + pattern: RegExp("^(?:${AddressValidator.zcashAddressPattern})\$"),
244 + currency: CryptoCurrency.zec,
245 + ),
246 +
247 // Solana (Base58 format)
248 _DetectionPattern(
249 pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{43,44}$'),
lib/di.dart
+14 -6
@@ -7,6 +7,7 @@ import 'package:cake_wallet/anypay/anypay_api.dart';
7 import 'package:cake_wallet/bitcoin/bitcoin.dart';
8 import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
9 import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
10 +import 'package:cake_wallet/core/anypay/anypay_service.dart';
11 import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart';
12 import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
13 import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
@@ -55,12 +56,11 @@ import 'package:cake_wallet/new-ui/new_dashboard.dart';
56 import 'package:cake_wallet/new-ui/pages/about_page.dart';
57 import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
58 import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
58 -import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
59 -import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
59 import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
60 import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
61 import 'package:cake_wallet/new-ui/pages/home_page.dart';
62 import 'package:cake_wallet/new-ui/pages/send_page.dart';
63 +import "package:cake_wallet/new-ui/services/wallet_switch_service.dart";
64 import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
65 import 'package:cake_wallet/new-ui/pages/receive_page.dart';
66 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
169 import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
170 import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
171 import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
172 -import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
172 import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
173 import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
174 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';
192 import 'package:cake_wallet/store/bridge_transfers_store.dart';
193 import 'package:cake_wallet/store/dashboard/trades_store.dart';
194 import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
196 -import 'package:cake_wallet/store/node_list_store.dart';
195 import 'package:cake_wallet/store/seed_settings_store.dart';
196 import 'package:cake_wallet/store/settings_store.dart';
197 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
248 import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
249 import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
250 import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
253 -import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
251 import 'package:cake_wallet/view_model/order_details_view_model.dart';
252 import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
253 import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
@@ -926,7 +923,8 @@ Future<void> setup({
923 authService: getIt.get<AuthService>(),
924 params: params,
925 contactListViewModel: getIt.get<ContactListViewModel>(),
929 - paymentViewModel: getIt.get<PaymentViewModel>(),
926 + anyPayService: getIt.get<AnyPayService>(),
927 + linkViewModel: getIt.get<LinkViewModel>(),
928 walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
929 );
930 });
@@ -1291,6 +1289,16 @@ Future<void> setup({
1289 appStore: getIt.get<AppStore>(),
1290 ));
1291
1292 + getIt.registerFactory(() => WalletSwitchService(
1293 + walletLoadingService: getIt.get<WalletLoadingService>(),
1294 + appStore: getIt.get<AppStore>(),
1295 + ));
1296 +
1297 + getIt.registerFactory(() => AnyPayService(
1298 + appStore: getIt.get<AppStore>(),
1299 + walletSwitchService: getIt.get<WalletSwitchService>(),
1300 + ));
1301 +
1302 getIt.registerFactory(() => WalletSwitcherViewModel(
1303 appStore: getIt.get<AppStore>(),
1304 walletLoadingService: getIt.get<WalletLoadingService>(),
lib/new-ui/pages/send_page.dart
+176 -666
@@ -1,11 +1,13 @@
1 -import "package:cake_wallet/core/address_resolver/address_resolver_service.dart";
1 +import "dart:async";
2 +
3 import "package:cake_wallet/core/address_resolver/parsed_address.dart";
4 import "package:cake_wallet/core/address_validator.dart";
5 +import "package:cake_wallet/core/anypay/anypay_models.dart";
6 +import "package:cake_wallet/core/anypay/anypay_parser.dart";
7 +import "package:cake_wallet/core/anypay/anypay_service.dart";
8 import "package:cake_wallet/core/auth_service.dart";
9 import "package:cake_wallet/core/execution_state.dart";
10 import "package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart";
7 -import "package:cake_wallet/core/universal_address_detector.dart";
8 -import "package:cake_wallet/di.dart";
11 import "package:cake_wallet/entities/contact_record.dart";
12 import "package:cake_wallet/entities/priority_for_wallet_type.dart";
13 import "package:cake_wallet/evm/evm.dart";
@@ -14,14 +16,10 @@ import "package:cake_wallet/main.dart";
16 import "package:cake_wallet/monero/monero.dart";
17 import "package:cake_wallet/new-ui/modal_navigator.dart";
18 import "package:cake_wallet/new-ui/pages/coin_control_page.dart";
17 -import "package:cake_wallet/new-ui/pages/swap_page.dart";
19 import "package:cake_wallet/new-ui/widgets/animated_dropdown.dart";
19 -import "package:cake_wallet/new-ui/widgets/anypay/evm_address_detected_sheet.dart";
20 +import "package:cake_wallet/new-ui/widgets/anypay/anypay_flow.dart";
21 import "package:cake_wallet/new-ui/widgets/anypay/recipient_network_row.dart";
22 import "package:cake_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
22 -import "package:cake_wallet/new-ui/widgets/anypay/send_to_network_page.dart";
23 -import "package:cake_wallet/new-ui/widgets/anypay/swap_from_network_page.dart";
24 -import "package:cake_wallet/new-ui/widgets/anypay/switch_network_wallet_page.dart";
23 import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
24 import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart";
25 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";
38 import "package:cake_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart";
39 import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart";
40 import "package:cake_wallet/new-ui/widgets/send_page/send_syncing_indicator.dart";
43 -import "package:cake_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
41 import "package:cake_wallet/reactions/wallet_connect.dart";
42 import "package:cake_wallet/routes.dart" show Routes;
43 import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart";
44 import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
48 -import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart";
45 import "package:cake_wallet/src/widgets/cake_image_widget.dart";
46 import "package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart";
47 import "package:cake_wallet/src/widgets/standard_checkbox.dart";
52 -import "package:cake_wallet/store/app_store.dart";
48 import "package:cake_wallet/utils/payment_request.dart";
49 import "package:cake_wallet/utils/show_pop_up.dart";
55 -import "package:cake_wallet/utils/token_utilities.dart";
50 import "package:cake_wallet/view_model/contact_list/contact_list_view_model.dart";
57 -import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
58 -import "package:cake_wallet/view_model/payment/payment_view_model.dart";
51 +import "package:cake_wallet/view_model/link_view_model.dart";
52 import "package:cake_wallet/view_model/send/output.dart";
53 import "package:cake_wallet/view_model/send/send_view_model.dart";
54 import "package:cake_wallet/view_model/send/send_view_model_state.dart";
@@ -66,11 +59,9 @@ import "package:cw_core/crypto_currency.dart";
59 import "package:cw_core/currency_for_wallet_type.dart";
60 import "package:cw_core/erc20_token.dart";
61 import "package:cw_core/lnurl.dart";
69 -import "package:cw_core/payment_uris.dart";
62 import "package:cw_core/transaction_priority.dart";
63 import "package:cw_core/unspent_coin_type.dart";
64 import "package:cw_core/utils/print_verbose.dart";
73 -import "package:cw_core/wallet_info.dart";
65 import "package:cw_core/wallet_type.dart";
66 import "package:flutter/cupertino.dart";
67 import "package:flutter/material.dart";
@@ -169,12 +160,14 @@ class SendPageModes {
160 class SendPageParams {
161 SendPageParams({
162 this.initialPaymentRequest,
163 + this.initialRawInput,
164 SendPageModes? mode,
165 this.unspentCoinType = UnspentCoinType.any,
166 this.initialCurrency,
167 }) : mode = mode ?? SendPageModes.normal;
168
169 final PaymentRequest? initialPaymentRequest;
170 + final String? initialRawInput;
171 final SendPageModes mode;
172 final CryptoCurrency? initialCurrency;
173 final UnspentCoinType unspentCoinType;
@@ -183,13 +176,15 @@ class SendPageParams {
176 class NewSendPage extends StatefulWidget {
177 NewSendPage({
178 required this.sendViewModel,
186 - required this.paymentViewModel,
179 + required this.anyPayService,
180 + required this.linkViewModel,
181 required this.walletSwitcherViewModel,
182 required this.contactListViewModel,
183 required this.authService,
184 required SendPageParams params,
185 super.key,
186 }) : initialPaymentRequest = params.initialPaymentRequest,
187 + initialRawInput = params.initialRawInput,
188 mode = params.mode {
189 if (params.initialCurrency != null) {
190 sendViewModel.selectedCryptoCurrency = params.initialCurrency!;
@@ -197,11 +192,13 @@ class NewSendPage extends StatefulWidget {
192 }
193
194 final SendViewModel sendViewModel;
200 - final PaymentViewModel paymentViewModel;
195 + final AnyPayService anyPayService;
196 + final LinkViewModel linkViewModel;
197 final WalletSwitcherViewModel walletSwitcherViewModel;
198 final ContactListViewModel contactListViewModel;
199 final AuthService authService;
200 final PaymentRequest? initialPaymentRequest;
201 + final String? initialRawInput;
202 final SendPageModes mode;
203
204 @override
@@ -216,8 +213,14 @@ class _NewSendPageState extends State<NewSendPage> {
213 final _memoControllers = <TextEditingController>[];
214 final _formKey = GlobalKey<FormState>();
215 final _addressFocusNode = FocusNode();
219 - BuildContext? loadingBottomSheetContext;
220 - BuildContext? dialogContext;
216 + final _disposers = <ReactionDisposer>[];
217 + StreamSubscription<Uri>? _deepLinkSubscription;
218 + late final AnyPayFlow _anyPayFlow = AnyPayFlow(
219 + anyPayService: widget.anyPayService,
220 + sendViewModel: widget.sendViewModel,
221 + authService: widget.authService,
222 + walletSwitcherViewModel: widget.walletSwitcherViewModel,
223 + );
224 ContactRecord? newContactAddress;
225
226 bool _justHandledPasteButton = false;
@@ -227,67 +230,64 @@ class _NewSendPageState extends State<NewSendPage> {
230 super.initState();
231 _addInputControllers();
232
230 - reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, (all) {
231 - if (all) {
232 - widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = false;
233 - _amountControllers[_selectedOutput].text = S.current.all;
234 - }
235 - });
233 + _disposers.add(
234 + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].sendAll, (all) {
235 + if (all) {
236 + widget.sendViewModel.outputs[_selectedOutput].isFiatEntry = false;
237 + _amountControllers[_selectedOutput].text = S.current.all;
238 + }
239 + }),
240 + );
241
237 - reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, (address) {
238 - if (_addressControllers[_selectedOutput].text != address) {
239 - _addressControllers[_selectedOutput].text = address;
240 - }
241 - });
242 + _disposers.add(
243 + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].address, (address) {
244 + if (_addressControllers[_selectedOutput].text != address) {
245 + _addressControllers[_selectedOutput].text = address;
246 + }
247 + }),
248 + );
249
243 - reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (memo) {
244 - if (memo != _memoControllers[_selectedOutput].text) {
245 - _memoControllers[_selectedOutput].text = memo;
246 - }
247 - });
250 + _disposers.add(
251 + reaction((_) => widget.sendViewModel.outputs[_selectedOutput].memo, (memo) {
252 + if (memo != _memoControllers[_selectedOutput].text) {
253 + _memoControllers[_selectedOutput].text = memo;
254 + }
255 + }),
256 + );
257
249 - if (widget.initialPaymentRequest != null) {
250 - if (_isInitialRequestTypeSameAsCurrentWallet()) {
251 - final contractAddress = widget.initialPaymentRequest!.contractAddress;
252 - if (contractAddress != null && contractAddress.isNotEmpty) {
253 - WidgetsBinding.instance.addPostFrameCallback((_) async {
254 - if (!mounted) {
255 - return;
256 - }
257 - await _applyPaymentSelectingCurrency(widget.initialPaymentRequest!, null);
258 - });
259 - } else {
260 - _addressControllers[0].text = widget.initialPaymentRequest!.address;
261 - _applyNote(widget.initialPaymentRequest!.note, 0);
262 - _amountControllers[0].text = widget.initialPaymentRequest!.amount;
258 + if (widget.initialRawInput != null || widget.initialPaymentRequest != null) {
259 + WidgetsBinding.instance.addPostFrameCallback((_) async {
260 + if (!mounted) {
261 + return;
262 }
264 - } else {
265 - WidgetsBinding.instance.addPostFrameCallback(
266 - (timeStamp) {
267 - if (mounted) {
268 - final paymentRequest = widget.initialPaymentRequest!;
269 - final scheme = paymentRequest.scheme.toLowerCase();
270 - final String uri;
271 - if (scheme.isEmpty || scheme == "lightning") {
272 - uri = paymentRequest.address;
273 - } else if (scheme == "ethereum") {
274 - uri = ERC681URI(
275 - address: paymentRequest.address,
276 - amount: paymentRequest.amount,
277 - contractAddress: paymentRequest.contractAddress,
278 - chainId: paymentRequest.chainId ?? _currentEvmChainIdOrMainnet(),
279 - rawTokenAmount: paymentRequest.rawTokenAmount,
280 - ).toString();
281 - } else {
282 - final amount =
283 - paymentRequest.amount.isNotEmpty ? "?amount=${paymentRequest.amount}" : "";
284 - uri = "${paymentRequest.scheme}:${paymentRequest.address}$amount";
285 - }
286 - _handlePaymentFlow(uri, paymentRequest);
287 - }
288 - },
263 + await _runAnyPayFlow(
264 + rawInput: widget.initialRawInput,
265 + paymentRequest: widget.initialPaymentRequest,
266 );
290 - }
267 + });
268 + }
269 +
270 + if (widget.mode == SendPageModes.normal) {
271 + _deepLinkSubscription = widget.linkViewModel.incomingPaymentLinks.listen((link) async {
272 + if (!mounted) {
273 + return;
274 + }
275 +
276 + final state = widget.sendViewModel.state;
277 + if (_anyPayFlow.isSwitchingWallet ||
278 + state is IsExecutingState ||
279 + state is TransactionCommitting) {
280 + printV("dropping incoming payment link while the send flow is busy");
281 + return;
282 + }
283 +
284 + final route = ModalRoute.of(context);
285 + if (route != null && !route.isCurrent) {
286 + Navigator.of(context).popUntil((r) => r == route);
287 + }
288 +
289 + await _runAnyPayFlow(rawInput: link.toString());
290 + });
291 }
292
293 _addressFocusNode.addListener(() async {
@@ -298,6 +298,25 @@ class _NewSendPageState extends State<NewSendPage> {
298 });
299 }
300
301 + @override
302 + void dispose() {
303 + _deepLinkSubscription?.cancel();
304 + for (final disposer in _disposers) {
305 + disposer();
306 + }
307 + for (final controller in _amountControllers) {
308 + controller.dispose();
309 + }
310 + for (final controller in _addressControllers) {
311 + controller.dispose();
312 + }
313 + for (final controller in _memoControllers) {
314 + controller.dispose();
315 + }
316 + _addressFocusNode.dispose();
317 + super.dispose();
318 + }
319 +
320 Future<void> _resolveAddressForOutput(Output output) async {
321 final result = await widget.sendViewModel.resolveAddressForOutput(output);
322 if (result == null || !mounted) {
@@ -326,6 +345,9 @@ class _NewSendPageState extends State<NewSendPage> {
345 Widget build(BuildContext context) => Observer(
346 builder: (_) {
347 final output = widget.sendViewModel.outputs[_selectedOutput];
348 + final recipientChain = widget.sendViewModel.isEVMWallet
349 + ? evm!.getChainInfoByChainId(_currentEvmChainIdOrMainnet())
350 + : null;
351 return SafeArea(
352 bottom: false,
353 child: KeyboardHideOverlay(
@@ -451,11 +473,7 @@ class _NewSendPageState extends State<NewSendPage> {
473 output.resetParsedAddress();
474 await _resolveAddressForOutput(output);
475
454 - // Process the payment through the new flow
455 - await _handlePaymentFlow(
456 - uri.toString(),
457 - PaymentRequest.fromString(uri.toString()),
458 - );
476 + await _runAnyPayFlow(rawInput: uri.toString());
477 },
478 onEditingComplete: _addressFocusNode.unfocus,
479 onPushAddressBookButton: (_) {
@@ -477,16 +495,7 @@ class _NewSendPageState extends State<NewSendPage> {
495 ? output.extractedAddress
496 : output.address;
497
480 - await _handlePaymentFlow(
481 - address,
482 - PaymentRequest(
483 - address,
484 - _amountControllers[_selectedOutput].text,
485 - _memoControllers[_selectedOutput].text,
486 - "",
487 - null,
488 - ),
489 - );
498 + await _runAnyPayPaste(address);
499 } finally {
500 _justHandledPasteButton = false;
501 }
@@ -497,10 +506,17 @@ class _NewSendPageState extends State<NewSendPage> {
506 widget.sendViewModel.selectedCryptoCurrency,
507 ),
508 if (widget.sendViewModel.isEVMWallet &&
500 - _hasEvmRecipient(output))
509 + _hasEvmRecipient(output) &&
510 + recipientChain != null)
511 RecipientNetworkSelector(
502 - wallet: widget.sendViewModel.wallet,
503 - onTap: _presentRecipientNetworkPicker,
512 + networkName: recipientChain.name,
513 + networkIconPath: symbolIconPathForWalletType(
514 + widget.sendViewModel.wallet.type,
515 + ) ??
516 + "",
517 + onTap: () => _presentRecipientNetworkPicker(
518 + recipientChain,
519 + ),
520 ),
521 ],
522 ),
@@ -1031,555 +1047,124 @@ class _NewSendPageState extends State<NewSendPage> {
1047 }
1048
1049 Future<void> _handleManualNetworkSelection(ChainInfo target) async {
1034 - final address = _addressControllers[_selectedOutput].text.trim();
1050 + final output = widget.sendViewModel.outputs[_selectedOutput];
1051 + final address = output.isParsedAddress
1052 + ? output.extractedAddress
1053 + : _addressControllers[_selectedOutput].text.trim();
1054 final note = _memoControllers[_selectedOutput].text;
1055 final isTokenSelected = widget.sendViewModel.selectedCryptoCurrency is Erc20Token;
1056 final amount = isTokenSelected ? "" : _amountControllers[_selectedOutput].text;
1038 - final paymentRequest = PaymentRequest(address, amount, note, "", null);
1039 - await _handleEvmNetworkFlow(target, paymentRequest);
1040 - }
1041 -
1042 - bool _isInitialRequestTypeSameAsCurrentWallet() {
1043 - final req = widget.initialPaymentRequest;
1044 - if (req == null) {
1045 - return false;
1046 - }
1047 -
1048 - final currentType = widget.sendViewModel.wallet.type;
1049 - if (evm != null && isEVMCompatibleChain(currentType)) {
1050 - final targetChainId = _evmTargetChainId(req);
1051 - if (targetChainId != null) {
1052 - return targetChainId == _currentEvmChainIdOrMainnet();
1053 - }
1054 - }
1055 - return widget.sendViewModel.walletCurrencyName == req.scheme.toLowerCase();
1056 - }
1057 + final request =
1058 + AnyPayParser.fromPaymentRequest(PaymentRequest(address, amount, note, "", null));
1059
1058 - int? _evmTargetChainId(PaymentRequest req) {
1059 - final scheme = req.scheme.toLowerCase();
1060 - if (scheme == "ethereum") {
1061 - if (req.chainId != null) {
1062 - return req.chainId;
1063 - }
1064 -
1065 - return _currentEvmChainIdOrMainnet();
1066 - }
1067 - try {
1068 - return getChainIdByCryptoCurrency(CryptoCurrency.fromString(scheme));
1069 - } catch (_) {
1070 - return null;
1060 + final evaluation = await widget.anyPayService.evaluateForEvmChain(request, target.chainId);
1061 + if (!mounted) {
1062 + return;
1063 }
1064 + await _handleEvaluation(evaluation, fallbackCurrency: target.currency);
1065 }
1066
1067 int _currentEvmChainIdOrMainnet() {
1068 final wallet = widget.sendViewModel.wallet;
1076 - if (evm != null && isEVMCompatibleChain(wallet.type)) {
1069 + if (isEVMCompatibleChain(wallet.type)) {
1070 return evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
1071 }
1072 return 1;
1073 }
1074
1082 - Future<void> _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async {
1083 - final isEip681 = uri.toLowerCase().startsWith("ethereum:");
1084 - if (!isEip681 && (uri.contains("@") || paymentRequest.address.contains("@"))) {
1085 - return;
1086 - }
1087 -
1088 - if (OpenCryptoPayService.isOpenCryptoPayQR(uri) &&
1089 - widget.sendViewModel.selectedCryptoCurrency != CryptoCurrency.btcln) {
1090 - final request = await widget.sendViewModel.getOpenCryptoPayRequest(uri);
1091 - if (request == null) {
1092 - return;
1093 - }
1094 - _applyPaymentRequest(request);
1095 - return;
1096 - }
1097 -
1098 - try {
1099 - final result = await widget.paymentViewModel.processAddress(uri);
1100 -
1101 - if (paymentRequest.contractAddress != null) {
1102 - await widget.sendViewModel.fetchTokenForContractAddress(paymentRequest.contractAddress!);
1103 - }
1104 -
1105 - // This automatically switches to lightning mode if you are in a bitcoin wallet
1106 - if (result.addressDetectionResult?.detectedCurrency == CryptoCurrency.btcln) {
1107 - widget.sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
1108 - widget.sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
1109 - }
1110 -
1111 - switch (result.type) {
1112 - case PaymentFlowType.singleWallet:
1113 - case PaymentFlowType.multipleWallets:
1114 - case PaymentFlowType.noWallets:
1115 - await _showPaymentConfirmation(
1116 - widget.paymentViewModel,
1117 - widget.walletSwitcherViewModel,
1118 - paymentRequest,
1119 - result,
1120 - );
1121 - break;
1122 - case PaymentFlowType.evmNetworkSelection:
1123 - if (result.chainId != null && evm!.getChainInfoByChainId(result.chainId!) == null) {
1124 - _showUnsupportedNetworkAlert(result.chainId!);
1125 - return;
1126 - }
1127 -
1128 - final targetChainId =
1129 - paymentRequest.scheme.isNotEmpty ? _evmTargetChainId(paymentRequest) : null;
1130 - final targetChain =
1131 - targetChainId != null ? evm!.getChainInfoByChainId(targetChainId) : null;
1132 - final currentChainId = isEVMCompatibleChain(widget.sendViewModel.wallet.type)
1133 - ? _currentEvmChainIdOrMainnet()
1134 - : null;
1135 - final isCrossChain = targetChain != null && targetChain.chainId != currentChainId;
1136 -
1137 - if (isCrossChain) {
1138 - await _handleEvmNetworkFlow(targetChain, paymentRequest);
1139 - } else if (widget.sendViewModel.isEVMWallet) {
1140 - await _applyPaymentSelectingCurrency(paymentRequest, null);
1141 - } else {
1142 - await _showEvmNetworkPicker(paymentRequest, result.walletType);
1143 - }
1144 - break;
1145 - case PaymentFlowType.solanaTokenSelection:
1146 - case PaymentFlowType.tronTokenSelection:
1147 - await _showPaymentConfirmation(
1148 - widget.paymentViewModel,
1149 - widget.walletSwitcherViewModel,
1150 - paymentRequest,
1151 - result,
1152 - );
1153 - break;
1154 - case PaymentFlowType.currentWalletCompatible:
1155 - case PaymentFlowType.error:
1156 - case PaymentFlowType.incompatible:
1157 - _applyPaymentRequest(paymentRequest);
1158 - break;
1159 - }
1160 - } catch (e) {
1161 - printV("Payment flow error: $e");
1162 - _applyPaymentRequest(paymentRequest);
1163 - }
1164 - }
1165 -
1166 - void _showUnsupportedNetworkAlert(int chainId) {
1167 - if (!mounted) {
1168 - return;
1169 - }
1170 -
1171 - showPopUp<void>(
1172 - context: context,
1173 - builder: (context) => AlertWithOneAction(
1174 - alertTitle: S.of(context).error,
1175 - alertContent: S.of(context).unsupported_network_requested(chainId.toString()),
1176 - buttonText: S.of(context).ok,
1177 - buttonAction: () => Navigator.of(context).pop(),
1178 - ),
1179 - );
1180 - }
1181 -
1182 - Future<void> _showPaymentConfirmation(
1183 - PaymentViewModel paymentViewModel,
1184 - WalletSwitcherViewModel walletSwitcherViewModel,
1185 - PaymentRequest paymentRequest,
1186 - PaymentFlowResult result,
1187 - ) async {
1188 - if (!mounted || result.walletType == null) {
1189 - return;
1190 - }
1191 -
1192 - final destinationType = result.walletType!;
1193 - final isEvmTarget = isEVMCompatibleChain(destinationType);
1194 - final destinationChainId = result.chainId;
1195 - final destinationNetworkName = networkDisplayName(destinationType, destinationChainId);
1196 - final destinationNetworkIcon = symbolIconPathForWalletType(destinationType) ?? "";
1197 -
1198 - final currentType = widget.sendViewModel.wallet.type;
1199 - final currentChainId =
1200 - isEVMCompatibleChain(currentType) && evm != null ? _currentEvmChainIdOrMainnet() : null;
1201 - final currentNetworkName = networkDisplayName(currentType, currentChainId);
1202 - final currentNetworkIcon = symbolIconPathForWalletType(currentType) ?? "";
1203 -
1204 - final hasSingleWallet =
1205 - result.type == PaymentFlowType.singleWallet || result.wallets.length == 1;
1206 - final hasMultipleWallets =
1207 - result.type == PaymentFlowType.multipleWallets || result.wallets.length > 1;
1208 - final hasWallet = hasSingleWallet || hasMultipleWallets || result.wallet != null;
1209 -
1210 - final decisionTitle = isEvmTarget
1211 - ? S.of(context).send_to_network(destinationNetworkName)
1212 - : S.of(context).network_address_detected(destinationNetworkName);
1213 - final swapConfirmTitle = isEvmTarget
1214 - ? S.of(context).swap_from_network(currentNetworkName)
1215 - : S.of(context).network_address_detected(destinationNetworkName);
1216 - final swapConfirmPrimary = isEvmTarget
1217 - ? S.of(context).continue_to_swap
1218 - : S.of(context).swap_from_network(currentNetworkName);
1219 -
1220 - if (hasWallet) {
1221 - await Navigator.of(context).push<void>(
1222 - CupertinoPageRoute(
1223 - builder: (pageContext) => SendToNetworkPage(
1224 - title: decisionTitle,
1225 - destinationNetworkName: destinationNetworkName,
1226 - destinationIconPath: destinationNetworkIcon,
1227 - currentNetworkName: currentNetworkName,
1228 - onSwitchWallet: () => _onSwitchWalletSelected(
1229 - pageContext,
1230 - result,
1231 - paymentRequest,
1232 - destinationNetworkName,
1233 - destinationNetworkIcon,
1234 - hasMultipleWallets,
1235 - ),
1236 - onSwap: () => _onSwapSelected(pageContext, result, paymentRequest),
1237 - ),
1238 - ),
1239 - );
1240 - } else {
1241 - await Navigator.of(context).push<void>(
1242 - CupertinoPageRoute(
1243 - builder: (pageContext) => SwapFromNetworkPage(
1244 - title: swapConfirmTitle,
1245 - primaryButtonText: swapConfirmPrimary,
1246 - primaryHasSwapIcon: !isEvmTarget,
1247 - destinationNetworkName: destinationNetworkName,
1248 - destinationNetworkIconPath: destinationNetworkIcon,
1249 - currentNetworkName: currentNetworkName,
1250 - currentNetworkIconPath: currentNetworkIcon,
1251 - onProceed: () => _onSwapSelected(pageContext, result, paymentRequest),
1252 - ),
1253 - ),
1254 - );
1255 - }
1256 - }
1257 -
1258 - Future<void> _onSwapSelected(
1259 - BuildContext pageContext,
1260 - PaymentFlowResult result,
1261 - PaymentRequest paymentRequest,
1262 - ) async {
1263 - await _handleSwapFlow(result, pageContext, paymentRequest);
1264 - }
1265 -
1266 - Future<void> _onSwitchWalletSelected(
1267 - BuildContext pageContext,
1268 - PaymentFlowResult result,
1269 - PaymentRequest paymentRequest,
1270 - String destName,
1271 - String destIcon,
1272 - bool hasMultipleWallets,
1273 - ) async {
1274 - WalletInfo? destinationWalletInfo;
1275 - if (hasMultipleWallets) {
1276 - destinationWalletInfo = await SwitchNetworkWalletPage.push(
1277 - context: pageContext,
1278 - networkName: destName,
1279 - targetIconPath: destIcon,
1280 - wallets: result.wallets,
1281 - );
1282 - if (destinationWalletInfo == null) {
1283 - return;
1284 - }
1285 - } else {
1286 - destinationWalletInfo =
1287 - result.wallet ?? (result.wallets.isNotEmpty ? result.wallets.first : null);
1288 - }
1289 -
1290 - if (destinationWalletInfo == null) {
1291 - return;
1292 - }
1293 -
1294 - // We may have awaited the wallet picker above, so make sure the decision page
1295 - // is still around before we reach into its navigator.
1296 - if (!pageContext.mounted) {
1297 - return;
1298 - }
1299 - if (Navigator.of(pageContext).canPop()) {
1300 - Navigator.of(pageContext).pop();
1301 - }
1302 - if (!mounted) {
1303 - return;
1304 - }
1305 - await _completeWalletSwitch(destinationWalletInfo, result, paymentRequest);
1306 - }
1307 -
1308 - Future<void> _applyPaymentSelectingCurrency(
1309 - PaymentRequest paymentRequest,
1310 - CryptoCurrency? fallbackCurrency,
1311 - ) async {
1312 - String? amountOverride;
1313 - final contract = paymentRequest.contractAddress;
1314 - if (contract != null && contract.isNotEmpty) {
1315 - final walletType = widget.sendViewModel.wallet.type;
1316 - final lookupType = evm != null && isEVMCompatibleChain(walletType)
1317 - ? (evm!.getWalletTypeByChainId(_currentEvmChainIdOrMainnet()) ?? walletType)
1318 - : walletType;
1319 - final token = await TokenUtilities.findTokenByAddress(
1320 - walletType: lookupType,
1321 - address: contract,
1322 - );
1323 - if (!mounted) {
1324 - return;
1325 - }
1326 -
1327 - if (token == null) {
1328 - final rerouted = await _rerouteChainlessContractPayment(paymentRequest);
1329 - if (rerouted || !mounted) {
1330 - return;
1331 - }
1332 - _showUnsupportedTokenAlert();
1333 - return;
1334 - }
1335 - await widget.sendViewModel.fetchTokenForContractAddress(contract, walletType: lookupType);
1336 - amountOverride = paymentRequest.resolveTokenAmount(token);
1337 - } else if (fallbackCurrency != null) {
1338 - widget.sendViewModel.setSelectedCryptoCurrency(fallbackCurrency.title);
1339 - if (paymentRequest.amount.isEmpty) {
1340 - widget.sendViewModel.outputs[_selectedOutput].setCryptoAmount("");
1341 - _amountControllers[_selectedOutput].clear();
1342 - }
1343 - }
1344 - if (!mounted) {
1345 - return;
1346 - }
1347 - _applyPaymentRequest(paymentRequest, amountOverride: amountOverride);
1348 - }
1349 -
1350 - Future<bool> _rerouteChainlessContractPayment(PaymentRequest paymentRequest) async {
1351 - if (evm == null || paymentRequest.chainId != null) {
1075 + Future<bool> _handleIfOpenCryptoPay(String input) async {
1076 + if (!OpenCryptoPayService.isOpenCryptoPayQR(input) ||
1077 + widget.sendViewModel.selectedCryptoCurrency == CryptoCurrency.btcln) {
1078 return false;
1079 }
1080
1355 - if (paymentRequest.scheme.toLowerCase() != "ethereum") {
1356 - return false;
1357 - }
1358 -
1359 - final contract = paymentRequest.contractAddress;
1360 - if (contract == null || contract.isEmpty) {
1361 - return false;
1081 + if (_selectedOutput != 0) {
1082 + setState(() => _selectedOutput = 0);
1083 }
1084
1364 - final currentChainId = isEVMCompatibleChain(widget.sendViewModel.wallet.type)
1365 - ? _currentEvmChainIdOrMainnet()
1366 - : null;
1367 -
1368 - // QRs from old app versions omit the chainId on mainnet, so a contract the current
1369 - // network does not know may still belong to another EVM network
1370 - final chainId = await TokenUtilities.findEvmChainIdForContract(
1371 - contract,
1372 - excludingChainId: currentChainId,
1373 - );
1374 - if (chainId == null || !mounted) {
1375 - return false;
1376 - }
1377 -
1378 - final targetChain = evm!.getChainInfoByChainId(chainId);
1379 - if (targetChain == null) {
1380 - return false;
1085 + final request = await widget.sendViewModel.getOpenCryptoPayRequest(input);
1086 + if (request != null && mounted) {
1087 + _applyPaymentRequest(request);
1088 }
1382 -
1383 - printV("chainless contract payment rerouted to chainId $chainId");
1384 - await _handleEvmNetworkFlow(targetChain, paymentRequest);
1089 return true;
1090 }
1091
1388 - Future<void> _completeWalletSwitch(
1389 - WalletInfo wallet,
1390 - PaymentFlowResult result,
1391 - PaymentRequest paymentRequest,
1392 - ) async {
1393 - widget.walletSwitcherViewModel.selectWallet(wallet);
1394 - final success = await widget.walletSwitcherViewModel.switchToSelectedWallet();
1395 - if (!success) {
1396 - _showNetworkSwitchFailedAlert();
1397 - return;
1398 - }
1399 - if (!mounted) {
1400 - return;
1401 - }
1092 + Future<void> _runAnyPayFlow({String? rawInput, PaymentRequest? paymentRequest}) async {
1093 + assert(rawInput != null || paymentRequest != null);
1094
1403 - bool completedFlow = false;
1404 - WidgetsBinding.instance.addPostFrameCallback((_) {
1405 - if (!mounted || completedFlow) {
1406 - return;
1407 - }
1408 - showModalBottomSheet<void>(
1409 - context: context,
1410 - isDismissible: false,
1411 - builder: (BuildContext sheetContext) {
1412 - if (completedFlow) {
1413 - WidgetsBinding.instance.addPostFrameCallback((_) {
1414 - if (Navigator.canPop(sheetContext)) {
1415 - Navigator.of(sheetContext).pop();
1416 - }
1417 - });
1418 - return const SizedBox.shrink();
1419 - }
1420 - loadingBottomSheetContext = sheetContext;
1421 - return LoadingBottomSheet(titleText: S.of(sheetContext).loading_your_wallet);
1422 - },
1423 - );
1424 - });
1425 -
1426 - bool chainSwitchFailed = false;
1427 - try {
1428 - if (evm != null &&
1429 - isEVMCompatibleChain(widget.sendViewModel.wallet.type) &&
1430 - result.chainId != null) {
1431 - final appStore = getIt.get<AppStore>();
1432 - final node = appStore.settingsStore
1433 - .getCurrentNode(widget.sendViewModel.wallet.type, chainId: result.chainId);
1434 - await evm!.selectChain(widget.sendViewModel.wallet, result.chainId!, node: node);
1435 - }
1436 - } catch (e, s) {
1437 - chainSwitchFailed = true;
1438 - printV("completeWalletSwitch failed: $e\n$s");
1439 - }
1440 -
1441 - if (!chainSwitchFailed) {
1442 - try {
1443 - await widget.sendViewModel.wallet.updateBalance();
1444 - } catch (e) {
1445 - printV("balance refresh after network switch failed: $e");
1446 - }
1447 - }
1448 -
1449 - completedFlow = true;
1450 - if (loadingBottomSheetContext != null &&
1451 - loadingBottomSheetContext!.mounted &&
1452 - Navigator.canPop(loadingBottomSheetContext!)) {
1453 - Navigator.of(loadingBottomSheetContext!).pop();
1454 - }
1455 - loadingBottomSheetContext = null;
1095 + final evaluation = rawInput != null
1096 + ? await widget.anyPayService.evaluateRawInput(rawInput)
1097 + : await widget.anyPayService.evaluatePaymentRequest(paymentRequest!);
1098
1099 if (!mounted) {
1100 return;
1101 }
1102
1461 - if (chainSwitchFailed) {
1462 - _showNetworkSwitchFailedAlert();
1103 + if (await _handleIfOpenCryptoPay(evaluation.request.rawInput)) {
1104 return;
1105 }
1106
1466 - await _applyPaymentSelectingCurrency(
1467 - paymentRequest,
1468 - result.addressDetectionResult?.detectedCurrency,
1469 - );
1107 + await _handleEvaluation(evaluation);
1108 }
1109
1472 - void _showNetworkSwitchFailedAlert() {
1473 - if (!mounted) {
1110 + Future<void> _runAnyPayPaste(String pasted) async {
1111 + if (await _handleIfOpenCryptoPay(pasted)) {
1112 return;
1113 }
1114
1477 - showPopUp<void>(
1478 - context: context,
1479 - builder: (context) => AlertWithOneAction(
1480 - alertTitle: S.of(context).error,
1481 - alertContent: S.of(context).network_switch_failed,
1482 - buttonText: S.of(context).ok,
1483 - buttonAction: () => Navigator.of(context).pop(),
1484 - ),
1115 + final parsed = PaymentRequest.fromString(pasted);
1116 + final parsedAmount = parsed.amount == "0" ? "" : parsed.amount;
1117 + final merged = parsed.copyWith(
1118 + amount: parsedAmount.isNotEmpty ? parsedAmount : _amountControllers[_selectedOutput].text,
1119 + note: parsed.note.isNotEmpty ? parsed.note : _memoControllers[_selectedOutput].text,
1120 );
1486 - }
1121
1488 - void _showUnsupportedTokenAlert() {
1489 - if (!mounted) {
1490 - return;
1491 - }
1492 -
1493 - showPopUp<void>(
1494 - context: context,
1495 - builder: (context) => AlertWithOneAction(
1496 - alertTitle: S.of(context).error,
1497 - alertContent: S.of(context).unsupported_token_requested,
1498 - buttonText: S.of(context).ok,
1499 - buttonAction: () => Navigator.of(context).pop(),
1500 - ),
1501 - );
1122 + await _runAnyPayFlow(paymentRequest: merged);
1123 }
1124
1504 - Future<void> _showEvmNetworkPicker(
1505 - PaymentRequest paymentRequest,
1506 - WalletType? fixedNetwork,
1507 - ) async {
1508 - if (!mounted || evm == null || fixedNetwork == null || !isEVMCompatibleChain(fixedNetwork)) {
1125 + Future<void> _handleEvaluation(
1126 + AnyPayEvaluation evaluation, {
1127 + CryptoCurrency? fallbackCurrency,
1128 + }) async {
1129 + final evaluationResult = await _anyPayFlow.handleEvaluation(
1130 + context,
1131 + evaluation,
1132 + fallbackCurrency: fallbackCurrency,
1133 + );
1134 + if (evaluationResult == null || !mounted) {
1135 return;
1136 }
1137
1512 - final chains = evm!.getAllChains();
1513 - final networks = chains.map((chain) {
1514 - final walletType = evm!.getWalletTypeByChainId(chain.chainId) ?? fixedNetwork;
1515 - return RecipientNetworkItem(
1516 - chainId: chain.chainId,
1517 - name: chain.name,
1518 - iconPath: getCryptoCurrencyIconForWalletListItem(walletType, chainId: chain.chainId),
1519 - chainBadgeIconPath: symbolIconPathForWalletType(walletType),
1138 + try {
1139 + await _applyIntent(
1140 + evaluationResult.request,
1141 + token: evaluationResult.token,
1142 + fallbackCurrency: evaluationResult.fallbackCurrency,
1143 + amountOverride: evaluationResult.amountOverride,
1144 );
1521 - }).toList();
1522 -
1523 - final selectedChainId =
1524 - await EvmAddressDetectedSheet.show(context: context, networks: networks);
1525 - if (!mounted || selectedChainId == null) {
1526 - return;
1145 + } catch (e) {
1146 + printV("Payment flow error: $e");
1147 + _applyPaymentRequest(evaluationResult.request.paymentRequest);
1148 }
1528 -
1529 - final target = chains.firstWhere(
1530 - (chain) => chain.chainId == selectedChainId,
1531 - orElse: () => chains.first,
1532 - );
1533 - await _handleEvmNetworkFlow(target, paymentRequest);
1149 }
1150
1536 - Future<void> _handleEvmNetworkFlow(ChainInfo target, PaymentRequest paymentRequest) async {
1537 - if (evm == null || !mounted) {
1538 - return;
1539 - }
1540 -
1541 - final targetType = evm!.getWalletTypeByChainId(target.chainId);
1542 - if (targetType == null) {
1543 - _showUnsupportedNetworkAlert(target.chainId);
1544 - return;
1545 - }
1546 -
1547 - final detection = AddressDetectionResult(
1548 - address: paymentRequest.address,
1549 - detectedWalletType: targetType,
1550 - detectedCurrency: target.currency,
1551 - chainId: target.chainId,
1552 - amount: paymentRequest.amount,
1553 - note: paymentRequest.note,
1554 - scheme: "",
1555 - isValid: true,
1556 - );
1557 - widget.paymentViewModel.applyManualEvmSelection(detection);
1558 -
1559 - final currentType = widget.sendViewModel.wallet.type;
1560 - final currentChainId = isEVMCompatibleChain(currentType) ? _currentEvmChainIdOrMainnet() : null;
1561 - if (currentChainId != null && target.chainId == currentChainId) {
1562 - await _applyPaymentSelectingCurrency(paymentRequest, target.currency);
1563 - return;
1151 + Future<void> _applyIntent(
1152 + AnyPayRequest request, {
1153 + CryptoCurrency? token,
1154 + CryptoCurrency? fallbackCurrency,
1155 + String? amountOverride,
1156 + }) async {
1157 + final currency = token ?? fallbackCurrency;
1158 + if (currency != null) {
1159 + widget.sendViewModel.applyAnyPayCurrency(currency);
1160 }
1161
1566 - final compatibleWallets = await widget.paymentViewModel.getWalletsByType(targetType);
1567 - if (!mounted) {
1568 - return;
1162 + if (token == null && fallbackCurrency != null && request.amount.isEmpty) {
1163 + widget.sendViewModel.outputs[_selectedOutput].setCryptoAmount("");
1164 + _amountControllers[_selectedOutput].clear();
1165 }
1166
1571 - final result = PaymentFlowResult.evmNetworkSelection(
1572 - detection,
1573 - compatibleWallets: compatibleWallets,
1574 - wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null,
1575 - );
1576 -
1577 - await _showPaymentConfirmation(
1578 - widget.paymentViewModel,
1579 - widget.walletSwitcherViewModel,
1580 - paymentRequest,
1581 - result,
1582 - );
1167 + _applyPaymentRequest(request.paymentRequest, amountOverride: amountOverride);
1168 }
1169
1170 void _applyPaymentRequest(PaymentRequest paymentRequest, {String? amountOverride}) {
@@ -1604,81 +1189,6 @@ class _NewSendPageState extends State<NewSendPage> {
1189 _applyNote(paymentRequest.note, _selectedOutput);
1190 }
1191
1607 - Future<void> _handleSwapFlow(
1608 - PaymentFlowResult result,
1609 - BuildContext presentContext,
1610 - PaymentRequest paymentRequest,
1611 - ) async {
1612 - if (!mounted) {
1613 - return;
1614 - }
1615 -
1616 - final destWalletType = result.walletType;
1617 - if (destWalletType == null) {
1618 - return;
1619 - }
1620 -
1621 - CryptoCurrency? resolvedToken;
1622 - final contract = paymentRequest.contractAddress;
1623 - if (contract != null && contract.isNotEmpty) {
1624 - resolvedToken = await TokenUtilities.findTokenByAddress(
1625 - walletType: destWalletType,
1626 - address: contract,
1627 - );
1628 - }
1629 - if (!mounted || !presentContext.mounted) {
1630 - return;
1631 - }
1632 -
1633 - if (contract != null && contract.isNotEmpty && resolvedToken == null) {
1634 - _showUnsupportedTokenAlert();
1635 - return;
1636 - }
1637 -
1638 - final receiveCurrency = resolvedToken ??
1639 - result.detectedCurrency ??
1640 - walletTypeToCryptoCurrency(destWalletType, chainId: result.chainId);
1641 -
1642 - final receiveAmountValue = resolvedToken != null
1643 - ? paymentRequest.resolveTokenAmount(resolvedToken)
1644 - : (paymentRequest.amount.isNotEmpty ? paymentRequest.amount : null);
1645 - final receiveAmount =
1646 - receiveAmountValue != null ? Money.tryParse(receiveAmountValue, receiveCurrency) : null;
1647 -
1648 - final isFiatDisabled = widget.sendViewModel.isFiatDisabled;
1649 - final depositBalanceByAsset = <CryptoCurrency, CurrencyPickerBalance>{
1650 - for (final r in widget.sendViewModel.balanceViewModel.formattedBalances)
1651 - r.asset: CurrencyPickerBalance(
1652 - amount: "${r.availableBalance} ${r.asset.title}",
1653 - fiat: isFiatDisabled ? null : "${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}",
1654 - fiatValue: isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw),
1655 - ),
1656 - };
1657 -
1658 - final page = NewSwapPage(
1659 - getIt.get<ExchangeViewModel>(),
1660 - widget.authService,
1661 - getIt.get<AddressResolverService>(),
1662 - null,
1663 - walletSwitcherViewModel: widget.walletSwitcherViewModel,
1664 - fromSend: SwapFromSendArgs(
1665 - recipientAddress: result.addressDetectionResult?.address ?? "",
1666 - receiveCurrency: receiveCurrency,
1667 - targetWalletType: destWalletType,
1668 - depositBalanceByAsset: depositBalanceByAsset,
1669 - receiveAmount: receiveAmount,
1670 - ),
1671 - );
1672 - await Navigator.of(presentContext).push<void>(
1673 - CupertinoPageRoute(
1674 - builder: (context) => Material(
1675 - color: Theme.of(context).colorScheme.surface,
1676 - child: SafeArea(bottom: false, child: page),
1677 - ),
1678 - ),
1679 - );
1680 - }
1681 -
1192 void showErrorValidationAlert(BuildContext context) {
1193 int emptyAddressIndex = -1;
1194 for (int i = 0; i < widget.sendViewModel.outputs.length; i++) {
lib/new-ui/pages/swap_page.dart
+36 -8
@@ -7,6 +7,7 @@ import "package:cake_wallet/exchange/exchange_trade_state.dart";
7 import "package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart";
8 import "package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart";
9 import "package:cake_wallet/generated/i18n.dart";
10 +import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
11 import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart";
12 import "package:cake_wallet/new-ui/widgets/modern_button.dart";
13 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";
28 import "package:cake_wallet/utils/debounce.dart";
29 import "package:cake_wallet/utils/payment_request.dart";
30 import "package:cake_wallet/utils/show_pop_up.dart";
31 +import "package:cake_wallet/view_model/dashboard/balance_view_model.dart";
32 import "package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart";
33 import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
34 import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
@@ -51,6 +53,7 @@ class NewSwapPage extends StatefulWidget {
53 required this.walletSwitcherViewModel,
54 CryptoCurrency? initialCurrency,
55 this.fromSend,
56 + this.balanceViewModel,
57 }) {
58 depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
59 ? exchangeViewModel.wallet.name
@@ -69,6 +72,7 @@ class NewSwapPage extends StatefulWidget {
72 final AddressResolverService adrResService;
73 final PaymentRequest? initialPaymentRequest;
74 final SwapFromSendArgs? fromSend;
75 + final BalanceViewModel? balanceViewModel;
76 late final String? depositWalletName;
77 late final String? receiveWalletName;
78
@@ -93,6 +97,25 @@ class _NewSwapPageState extends State<NewSwapPage> {
97 .contains(widget.exchangeViewModel.depositCurrency) &&
98 !(widget.exchangeViewModel.status is SyncedSyncStatus);
99
100 + Map<CryptoCurrency, CurrencyPickerBalance>? _depositBalanceByAsset() {
101 + final balanceViewModel = widget.balanceViewModel;
102 + if (balanceViewModel == null) {
103 + return null;
104 + }
105 +
106 + return {
107 + for (final r in balanceViewModel.formattedBalances)
108 + r.asset: CurrencyPickerBalance(
109 + amount: "${r.availableBalance} ${r.asset.title}",
110 + fiat: balanceViewModel.isFiatDisabled
111 + ? null
112 + : "${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}",
113 + fiatValue:
114 + balanceViewModel.isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw),
115 + ),
116 + };
117 + }
118 +
119 @override
120 void initState() {
121 super.initState();
@@ -258,8 +281,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
281 (bool isReceiveAmountEditable) {},
282 ));
283
261 - _disposers
262 - .add(reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) {
284 + _disposers.add(
285 + reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) async {
286 if (state is TradeIsCreatedFailure) {
287 WidgetsBinding.instance.addPostFrameCallback((_) {
288 if (mounted) {
@@ -298,11 +321,15 @@ class _NewSwapPageState extends State<NewSwapPage> {
321 exchangeTradeViewModel: vm,
322 receiveAmount: receiveAmount,
323 );
301 - showMaterialModalBottomSheet(
302 - context: context,
303 - builder: (context) => page,
304 - backgroundColor: Colors.transparent,
305 - );
324 + try {
325 + await showMaterialModalBottomSheet<void>(
326 + context: context,
327 + builder: (context) => page,
328 + backgroundColor: Colors.transparent,
329 + );
330 + } finally {
331 + widget.exchangeViewModel.tradeStarted = false;
332 + }
333 }
334 }));
335
@@ -629,7 +656,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
656 walletName: fromSend != null
657 ? widget.exchangeViewModel.wallet.name
658 : null,
632 - balanceByAsset: fromSend?.depositBalanceByAsset,
659 + balanceByAsset:
660 + fromSend != null ? _depositBalanceByAsset() : null,
661 useSingleNetworkLayout: fromSend != null,
662 filteredNetwork: fromSend != null
663 ? widget.exchangeViewModel.wallet.type
lib/new-ui/services/wallet_switch_service.dart new
+29
@@ -0,0 +1,29 @@
1 +import "package:cake_wallet/core/wallet_loading_service.dart";
2 +import "package:cake_wallet/store/app_store.dart";
3 +import "package:cw_core/exceptions.dart";
4 +import "package:cw_core/wallet_info.dart";
5 +
6 +class WalletSwitchService {
7 + WalletSwitchService({
8 + required WalletLoadingService walletLoadingService,
9 + required AppStore appStore,
10 + }) : _walletLoadingService = walletLoadingService,
11 + _appStore = appStore;
12 +
13 + final AppStore _appStore;
14 + final WalletLoadingService _walletLoadingService;
15 +
16 + Future<void> switchToWallet(WalletInfo walletInfo) async {
17 + final wallet = await _walletLoadingService.load(walletInfo.type, walletInfo.name);
18 +
19 + // load() recovers from a corrupted wallet by opening any other wallet it can,
20 + // so the one it returns may not be the one that was requested.
21 + if (wallet.name != walletInfo.name || wallet.type != walletInfo.type) {
22 + throw WalletSwitchException(
23 + "wallet switch loaded ${wallet.name} instead of ${walletInfo.name}",
24 + );
25 + }
26 +
27 + await _appStore.changeCurrentWallet(wallet);
28 + }
29 +}
lib/new-ui/widgets/anypay/anypay_flow.dart new
+391
@@ -0,0 +1,391 @@
1 +import "package:cake_wallet/core/address_resolver/address_resolver_service.dart";
2 +import "package:cake_wallet/core/anypay/anypay_models.dart";
3 +import "package:cake_wallet/core/anypay/anypay_service.dart";
4 +import "package:cake_wallet/core/auth_service.dart";
5 +import "package:cake_wallet/di.dart";
6 +import "package:cake_wallet/evm/evm.dart";
7 +import "package:cake_wallet/generated/i18n.dart";
8 +import "package:cake_wallet/new-ui/pages/swap_page.dart";
9 +import "package:cake_wallet/new-ui/widgets/anypay/evm_address_detected_sheet.dart";
10 +import "package:cake_wallet/new-ui/widgets/anypay/network_decision_page.dart";
11 +import "package:cake_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
12 +import "package:cake_wallet/new-ui/widgets/anypay/switch_network_wallet_page.dart";
13 +import "package:cake_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
14 +import "package:cake_wallet/reactions/wallet_connect.dart";
15 +import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
16 +import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart";
17 +import "package:cake_wallet/utils/show_pop_up.dart";
18 +import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
19 +import "package:cake_wallet/view_model/send/send_view_model.dart";
20 +import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
21 +import "package:cw_core/crypto_currency.dart";
22 +import "package:cw_core/currency_for_wallet_type.dart";
23 +import "package:cw_core/unspent_coin_type.dart";
24 +import "package:cw_core/utils/print_verbose.dart";
25 +import "package:cw_core/wallet_info.dart";
26 +import "package:flutter/cupertino.dart";
27 +import "package:flutter/material.dart";
28 +
29 +class AnyPayFormFill {
30 + const AnyPayFormFill({
31 + required this.request,
32 + this.token,
33 + this.fallbackCurrency,
34 + this.amountOverride,
35 + });
36 +
37 + final AnyPayRequest request;
38 + final CryptoCurrency? token;
39 + final CryptoCurrency? fallbackCurrency;
40 + final String? amountOverride;
41 +}
42 +
43 +class AnyPayFlow {
44 + AnyPayFlow({
45 + required this.anyPayService,
46 + required this.sendViewModel,
47 + required this.authService,
48 + required this.walletSwitcherViewModel,
49 + });
50 +
51 + final AnyPayService anyPayService;
52 + final SendViewModel sendViewModel;
53 + final AuthService authService;
54 + final WalletSwitcherViewModel walletSwitcherViewModel;
55 +
56 + bool _isSwitchingWallet = false;
57 +
58 + bool get isSwitchingWallet => _isSwitchingWallet;
59 +
60 + Future<AnyPayFormFill?> handleEvaluation(
61 + BuildContext context,
62 + AnyPayEvaluation evaluation, {
63 + CryptoCurrency? fallbackCurrency,
64 + }) async {
65 + final request = evaluation.request;
66 +
67 + if (!request.rawInput.toLowerCase().startsWith("ethereum:") &&
68 + (request.rawInput.contains("@") || request.address.contains("@"))) {
69 + return null;
70 + }
71 +
72 + try {
73 + return await _handleDecision(context, evaluation, fallbackCurrency);
74 + } catch (e) {
75 + printV("Payment flow error: $e");
76 + return AnyPayFormFill(request: request);
77 + }
78 + }
79 +
80 + Future<AnyPayFormFill?> _handleDecision(
81 + BuildContext context,
82 + AnyPayEvaluation evaluation,
83 + CryptoCurrency? fallbackCurrency,
84 + ) async {
85 + final request = evaluation.request;
86 +
87 + switch (evaluation.decision) {
88 + case final AnyPayApplyToCurrentWallet decision:
89 + if (request.isLightning) {
90 + _enterLightningMode();
91 + }
92 + return AnyPayFormFill(
93 + request: request,
94 + token: decision.token,
95 + fallbackCurrency: fallbackCurrency ?? decision.fallbackCurrency,
96 + amountOverride: decision.amountOverride,
97 + );
98 + case AnyPayEvmNetworkChoice():
99 + return _pickEvmNetwork(context, request);
100 + case final AnyPayCrossChainPayment decision:
101 + return _handleCrossChain(context, request, decision, fallbackCurrency);
102 + case final AnyPayUnsupportedNetwork decision:
103 + _showError(
104 + context,
105 + S.of(context).unsupported_network_requested(decision.chainId.toString()),
106 + );
107 + return null;
108 + case AnyPayUnsupportedToken():
109 + _showError(context, S.of(context).unsupported_token_requested);
110 + return null;
111 + case AnyPayEmptyInput():
112 + return null;
113 + }
114 + }
115 +
116 + Future<AnyPayFormFill?> _pickEvmNetwork(BuildContext context, AnyPayRequest request) async {
117 + final chains = evm!.getAllChains();
118 + final networks = chains.map((chain) {
119 + final walletType = evm!.getWalletTypeByChainId(chain.chainId) ?? sendViewModel.wallet.type;
120 + return RecipientNetworkItem(
121 + chainId: chain.chainId,
122 + name: chain.name,
123 + iconPath: getCryptoCurrencyIconForWalletListItem(walletType, chainId: chain.chainId),
124 + chainBadgeIconPath: symbolIconPathForWalletType(walletType),
125 + );
126 + }).toList();
127 +
128 + final selectedChainId =
129 + await EvmAddressDetectedSheet.show(context: context, networks: networks);
130 + if (!context.mounted || selectedChainId == null) {
131 + return null;
132 + }
133 +
134 + final target = chains.firstWhere(
135 + (chain) => chain.chainId == selectedChainId,
136 + orElse: () => chains.first,
137 + );
138 +
139 + final evaluation = await anyPayService.evaluateForEvmChain(request, target.chainId);
140 + if (!context.mounted) {
141 + return null;
142 + }
143 + return handleEvaluation(context, evaluation, fallbackCurrency: target.currency);
144 + }
145 +
146 + Future<AnyPayFormFill?> _handleCrossChain(
147 + BuildContext context,
148 + AnyPayRequest request,
149 + AnyPayCrossChainPayment decision,
150 + CryptoCurrency? fallbackCurrency,
151 + ) async {
152 + final destinationType = decision.targetWalletType;
153 + final isEvmTarget = isEVMCompatibleChain(destinationType);
154 + final destinationNetworkName = networkDisplayName(destinationType, decision.targetChainId);
155 + final destinationNetworkIcon = symbolIconPathForWalletType(destinationType) ?? "";
156 +
157 + if (!decision.canSwap && !decision.hasCompatibleWallet) {
158 + _showError(
159 + context,
160 + S.of(context).swap_unavailable_needs_network_wallet(destinationNetworkName),
161 + );
162 + return null;
163 + }
164 +
165 + final currentType = sendViewModel.wallet.type;
166 + final currentChainId = isEVMCompatibleChain(currentType) ? _currentEvmChainId() : null;
167 + final currentNetworkName = networkDisplayName(currentType, currentChainId);
168 + final currentNetworkIcon = symbolIconPathForWalletType(currentType) ?? "";
169 +
170 + if (!decision.hasCompatibleWallet) {
171 + final swapConfirmTitle = isEvmTarget
172 + ? S.of(context).swap_from_network(currentNetworkName)
173 + : S.of(context).network_address_detected(destinationNetworkName);
174 + final swapConfirmPrimary = isEvmTarget
175 + ? S.of(context).continue_to_swap
176 + : S.of(context).swap_from_network(currentNetworkName);
177 +
178 + await Navigator.of(context).push<void>(
179 + CupertinoPageRoute(
180 + builder: (pageContext) => NetworkDecisionPage(
181 + title: swapConfirmTitle,
182 + description: S.of(context).swap_from_network_description(
183 + destinationNetworkName,
184 + currentNetworkName,
185 + ),
186 + destinationIconPath: destinationNetworkIcon,
187 + currentIconPath: currentNetworkIcon,
188 + primaryText: swapConfirmPrimary,
189 + primaryIconPath: isEvmTarget ? null : "assets/new-ui/swap_arrows.svg",
190 + onPrimary: () => _openSwap(pageContext, request, decision),
191 + secondaryText: S.of(context).cancel,
192 + ),
193 + ),
194 + );
195 + return null;
196 + }
197 +
198 + final decisionTitle = isEvmTarget
199 + ? S.of(context).send_to_network(destinationNetworkName)
200 + : S.of(context).network_address_detected(destinationNetworkName);
201 +
202 + final selectedWallet = await Navigator.of(context).push<WalletInfo>(
203 + CupertinoPageRoute(
204 + builder: (pageContext) => NetworkDecisionPage(
205 + title: decisionTitle,
206 + description: decision.canSwap
207 + ? S.of(context).send_to_network_description(
208 + destinationNetworkName,
209 + currentNetworkName,
210 + )
211 + : S.of(context).swap_unavailable_switch_to_network(destinationNetworkName),
212 + destinationIconPath: destinationNetworkIcon,
213 + primaryText: S.of(context).switch_to_x_wallet(destinationNetworkName),
214 + primaryIconPath: "assets/new-ui/wallet_filled.svg",
215 + onPrimary: () => _pickDestinationWallet(pageContext, decision, destinationNetworkName),
216 + secondaryText: decision.canSwap
217 + ? S.of(context).swap_from_network(currentNetworkName)
218 + : S.of(context).cancel,
219 + secondaryIconPath: decision.canSwap ? "assets/new-ui/swap_arrows.svg" : null,
220 + onSecondary: decision.canSwap ? () => _openSwap(pageContext, request, decision) : null,
221 + ),
222 + ),
223 + );
224 +
225 + if (selectedWallet == null || !context.mounted) {
226 + return null;
227 + }
228 +
229 + return _switchWallet(context, selectedWallet, request, decision, fallbackCurrency);
230 + }
231 +
232 + Future<void> _pickDestinationWallet(
233 + BuildContext pageContext,
234 + AnyPayCrossChainPayment decision,
235 + String networkName,
236 + ) async {
237 + WalletInfo? destinationWalletInfo;
238 + if (decision.wallets.length > 1) {
239 + destinationWalletInfo = await SwitchNetworkWalletPage.push(
240 + context: pageContext,
241 + networkName: networkName,
242 + targetIconPath: symbolIconPathForWalletType(decision.targetWalletType) ?? "",
243 + wallets: decision.wallets,
244 + );
245 + } else {
246 + destinationWalletInfo = decision.wallets.first;
247 + }
248 +
249 + if (destinationWalletInfo == null || !pageContext.mounted) {
250 + return;
251 + }
252 +
253 + Navigator.of(pageContext).pop(destinationWalletInfo);
254 + }
255 +
256 + Future<AnyPayFormFill?> _switchWallet(
257 + BuildContext context,
258 + WalletInfo walletInfo,
259 + AnyPayRequest request,
260 + AnyPayCrossChainPayment decision,
261 + CryptoCurrency? fallbackCurrency,
262 + ) async {
263 + if (request.isLightning) {
264 + _enterLightningMode();
265 + }
266 +
267 + _isSwitchingWallet = true;
268 + BuildContext? loadingSheetContext;
269 + bool completedFlow = false;
270 + WidgetsBinding.instance.addPostFrameCallback((_) {
271 + if (!context.mounted || completedFlow) {
272 + return;
273 + }
274 + showModalBottomSheet<void>(
275 + context: context,
276 + isDismissible: false,
277 + builder: (sheetContext) {
278 + if (completedFlow) {
279 + WidgetsBinding.instance.addPostFrameCallback((_) {
280 + if (Navigator.canPop(sheetContext)) {
281 + Navigator.of(sheetContext).pop();
282 + }
283 + });
284 + return const SizedBox.shrink();
285 + }
286 + loadingSheetContext = sheetContext;
287 + return LoadingBottomSheet(titleText: S.of(sheetContext).loading_your_wallet);
288 + },
289 + );
290 + });
291 +
292 + bool success = false;
293 + try {
294 + success = await anyPayService.switchWalletForPayment(
295 + walletInfo,
296 + chainId: decision.targetChainId,
297 + );
298 + } finally {
299 + _isSwitchingWallet = false;
300 + completedFlow = true;
301 + if (loadingSheetContext != null &&
302 + loadingSheetContext!.mounted &&
303 + Navigator.canPop(loadingSheetContext!)) {
304 + Navigator.of(loadingSheetContext!).pop();
305 + }
306 + loadingSheetContext = null;
307 + }
308 +
309 + if (!context.mounted) {
310 + return null;
311 + }
312 +
313 + if (!success) {
314 + _showError(context, S.of(context).network_switch_failed);
315 + return null;
316 + }
317 +
318 + return AnyPayFormFill(
319 + request: request,
320 + token: decision.token,
321 + fallbackCurrency: fallbackCurrency ?? request.detection.detectedCurrency,
322 + amountOverride: decision.amountOverride,
323 + );
324 + }
325 +
326 + Future<void> _openSwap(
327 + BuildContext presentContext,
328 + AnyPayRequest request,
329 + AnyPayCrossChainPayment decision,
330 + ) async {
331 + if (!presentContext.mounted) {
332 + return;
333 + }
334 +
335 + if (request.hasContract && decision.token == null) {
336 + _showError(presentContext, S.of(presentContext).unsupported_token_requested);
337 + return;
338 + }
339 +
340 + final intent = anyPayService.buildSwapIntent(
341 + request,
342 + targetWalletType: decision.targetWalletType,
343 + targetChainId: decision.targetChainId,
344 + token: decision.token,
345 + );
346 +
347 + final page = NewSwapPage(
348 + getIt.get<ExchangeViewModel>(),
349 + authService,
350 + getIt.get<AddressResolverService>(),
351 + null,
352 + walletSwitcherViewModel: walletSwitcherViewModel,
353 + fromSend: SwapFromSendArgs.fromIntent(intent),
354 + balanceViewModel: sendViewModel.balanceViewModel,
355 + );
356 + await Navigator.of(presentContext).push<void>(
357 + CupertinoPageRoute(
358 + builder: (context) => Material(
359 + color: Theme.of(context).colorScheme.surface,
360 + child: SafeArea(bottom: false, child: page),
361 + ),
362 + ),
363 + );
364 + }
365 +
366 + void _showError(BuildContext context, String message) {
367 + if (!context.mounted) {
368 + return;
369 + }
370 +
371 + showPopUp<void>(
372 + context: context,
373 + builder: (context) => AlertWithOneAction(
374 + alertTitle: S.of(context).error,
375 + alertContent: message,
376 + buttonText: S.of(context).ok,
377 + buttonAction: () => Navigator.of(context).pop(),
378 + ),
379 + );
380 + }
381 +
382 + void _enterLightningMode() {
383 + sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
384 + sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
385 + }
386 +
387 + int _currentEvmChainId() {
388 + final wallet = sendViewModel.wallet;
389 + return evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
390 + }
391 +}
lib/new-ui/widgets/anypay/network_decision_page.dart renamed
+87 -53
@@ -5,31 +5,37 @@ import "package:cake_wallet/src/widgets/cake_image_widget.dart";
5 import "package:cw_core/currency_for_wallet_type.dart";
6 import "package:flutter/material.dart";
7
8 -class SwapFromNetworkPage extends StatelessWidget {
9 - const SwapFromNetworkPage({
8 +class NetworkDecisionPage extends StatelessWidget {
9 + const NetworkDecisionPage({
10 required this.title,
11 - required this.primaryButtonText,
12 - required this.destinationNetworkName,
13 - required this.destinationNetworkIconPath,
14 - required this.currentNetworkName,
15 - required this.currentNetworkIconPath,
16 - required this.onProceed,
11 + required this.description,
12 + required this.destinationIconPath,
13 + required this.primaryText,
14 + required this.onPrimary,
15 + required this.secondaryText,
16 + this.currentIconPath,
17 + this.primaryIconPath,
18 + this.secondaryIconPath,
19 + this.onSecondary,
20 super.key,
18 - this.primaryHasSwapIcon = false,
21 });
22
23 final String title;
22 - final String primaryButtonText;
23 - final bool primaryHasSwapIcon;
24 - final String destinationNetworkName;
25 - final String destinationNetworkIconPath;
26 - final String currentNetworkName;
27 - final String currentNetworkIconPath;
28 - final VoidCallback onProceed;
24 + final String description;
25 + final String destinationIconPath;
26 + final String? currentIconPath;
27 + final String primaryText;
28 + final VoidCallback onPrimary;
29 + final String? primaryIconPath;
30 + final String secondaryText;
31 + final String? secondaryIconPath;
32 + final VoidCallback? onSecondary;
33
34 @override
35 Widget build(BuildContext context) {
36 final colors = Theme.of(context).colorScheme;
37 + final primaryIcon = primaryIconPath;
38 + final secondaryIcon = secondaryIconPath;
39 return Material(
40 color: colors.surface,
41 child: SafeArea(
@@ -40,9 +46,6 @@ class SwapFromNetworkPage extends StatelessWidget {
46 leadingIcon: const Icon(Icons.arrow_back_ios_new),
47 leadingSemanticLabel: S.of(context).seed_alert_back,
48 onLeadingPressed: () => Navigator.of(context).maybePop(),
43 - trailingIcon: const Icon(Icons.close),
44 - trailingSemanticLabel: S.of(context).close,
45 - onTrailingPressed: () => Navigator.of(context).maybePop(),
49 ),
50 Expanded(
51 child: Center(
@@ -51,30 +54,11 @@ class SwapFromNetworkPage extends StatelessWidget {
54 child: Column(
55 mainAxisSize: MainAxisSize.min,
56 children: [
54 - Row(
55 - mainAxisSize: MainAxisSize.min,
56 - spacing: 12,
57 - children: [
58 - CakeImageWidget(
59 - imageUrl: currentNetworkIconPath,
60 - width: 50,
61 - height: 50,
62 - fit: BoxFit.contain,
63 - color: isMonochromeSymbolIcon(currentNetworkIconPath)
64 - ? colors.primary
65 - : null,
66 - ),
67 - Icon(Icons.arrow_forward, color: colors.primary, size: 28),
68 - CakeImageWidget(
69 - imageUrl: destinationNetworkIconPath,
70 - width: 50,
71 - height: 50,
72 - fit: BoxFit.contain,
73 - color: isMonochromeSymbolIcon(destinationNetworkIconPath)
74 - ? colors.primary
75 - : null,
76 - ),
77 - ],
57 + ExcludeSemantics(
58 + child: _DecisionHeader(
59 + destinationIconPath: destinationIconPath,
60 + currentIconPath: currentIconPath,
61 + ),
62 ),
63 const SizedBox(height: 24),
64 Text(
@@ -88,10 +72,7 @@ class SwapFromNetworkPage extends StatelessWidget {
72 ),
73 const SizedBox(height: 16),
74 Text(
91 - S.of(context).swap_from_network_description(
92 - destinationNetworkName,
93 - currentNetworkName,
94 - ),
75 + description,
76 textAlign: TextAlign.center,
77 style: Theme.of(context)
78 .textTheme
@@ -109,23 +90,31 @@ class SwapFromNetworkPage extends StatelessWidget {
90 mainAxisSize: MainAxisSize.min,
91 children: [
92 NewPrimaryButton(
112 - onPressed: onProceed,
113 - image: primaryHasSwapIcon
93 + onPressed: onPrimary,
94 + image: primaryIcon != null
95 ? CakeImageWidget(
115 - imageUrl: "assets/new-ui/swap_arrows.svg",
96 + imageUrl: primaryIcon,
97 width: 24,
98 height: 24,
99 colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
100 )
101 : null,
121 - text: primaryButtonText,
102 + text: primaryText,
103 color: colors.primary,
104 textColor: colors.onPrimary,
105 ),
106 const SizedBox(height: 12),
107 NewPrimaryButton(
127 - onPressed: () => Navigator.of(context).maybePop(),
128 - text: S.of(context).cancel,
108 + onPressed: onSecondary ?? () => Navigator.of(context).maybePop(),
109 + image: secondaryIcon != null
110 + ? CakeImageWidget(
111 + imageUrl: secondaryIcon,
112 + width: 24,
113 + height: 24,
114 + colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
115 + )
116 + : null,
117 + text: secondaryText,
118 color: colors.surfaceContainer,
119 textColor: colors.primary,
120 ),
@@ -138,3 +127,48 @@ class SwapFromNetworkPage extends StatelessWidget {
127 );
128 }
129 }
130 +
131 +class _DecisionHeader extends StatelessWidget {
132 + const _DecisionHeader({required this.destinationIconPath, this.currentIconPath});
133 +
134 + final String destinationIconPath;
135 + final String? currentIconPath;
136 +
137 + @override
138 + Widget build(BuildContext context) {
139 + final colors = Theme.of(context).colorScheme;
140 + final current = currentIconPath;
141 +
142 + if (current == null) {
143 + return CakeImageWidget(
144 + imageUrl: destinationIconPath,
145 + width: 75,
146 + height: 75,
147 + fit: BoxFit.contain,
148 + color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
149 + );
150 + }
151 +
152 + return Row(
153 + mainAxisSize: MainAxisSize.min,
154 + spacing: 12,
155 + children: [
156 + CakeImageWidget(
157 + imageUrl: current,
158 + width: 50,
159 + height: 50,
160 + fit: BoxFit.contain,
161 + color: isMonochromeSymbolIcon(current) ? colors.primary : null,
162 + ),
163 + Icon(Icons.arrow_forward, color: colors.primary, size: 28),
164 + CakeImageWidget(
165 + imageUrl: destinationIconPath,
166 + width: 50,
167 + height: 50,
168 + fit: BoxFit.contain,
169 + color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
170 + ),
171 + ],
172 + );
173 + }
174 +}
lib/new-ui/widgets/anypay/recipient_network_row.dart
+49 -75
@@ -1,46 +1,14 @@
1 -import "package:cake_wallet/evm/evm.dart";
1 import "package:cake_wallet/generated/i18n.dart";
2 import "package:cake_wallet/src/widgets/cake_image_widget.dart";
3 import "package:cw_core/currency_for_wallet_type.dart";
5 -import "package:cw_core/utils/print_verbose.dart";
6 -import "package:cw_core/wallet_base.dart";
4 import "package:flutter/material.dart";
5
6 class RecipientNetworkSelector extends StatelessWidget {
7 const RecipientNetworkSelector({
11 - super.key,
12 - required this.wallet,
13 - required this.onTap,
14 - });
15 -
16 - final WalletBase wallet;
17 - final void Function(ChainInfo currentChain) onTap;
18 -
19 - @override
20 - Widget build(BuildContext context) {
21 - if (evm == null) return const SizedBox.shrink();
22 -
23 - final currentChainId =
24 - evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
25 - final currentChain = evm!.getChainInfoByChainId(currentChainId);
26 - if (currentChain == null) {
27 - printV("RecipientNetworkSelector: no chain info for chainId $currentChainId");
28 - return const SizedBox.shrink();
29 - }
30 -
31 - return _RecipientNetworkRow(
32 - networkName: currentChain.name,
33 - networkIconPath: symbolIconPathForWalletType(wallet.type) ?? "",
34 - onTap: () => onTap(currentChain),
35 - );
36 - }
37 -}
38 -
39 -class _RecipientNetworkRow extends StatelessWidget {
40 - const _RecipientNetworkRow({
8 required this.networkName,
9 required this.networkIconPath,
10 required this.onTap,
11 + super.key,
12 });
13
14 final String networkName;
@@ -50,50 +18,56 @@ class _RecipientNetworkRow extends StatelessWidget {
18 @override
19 Widget build(BuildContext context) {
20 final colors = Theme.of(context).colorScheme;
53 - return GestureDetector(
54 - onTap: onTap,
55 - child: Row(
56 - mainAxisSize: MainAxisSize.min,
57 - children: [
58 - Text(
59 - S.of(context).network_prefix_on,
60 - style: Theme.of(context)
61 - .textTheme
62 - .bodyMedium
63 - ?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
64 - ),
65 - const SizedBox(width: 12),
66 - CakeImageWidget(
67 - imageUrl: networkIconPath,
68 - width: 16,
69 - height: 16,
70 - fit: BoxFit.cover,
71 - color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
72 - ),
73 - const SizedBox(width: 8),
74 - Text(
75 - networkName,
76 - style: Theme.of(context).textTheme.bodyMedium?.copyWith(
77 - fontWeight: FontWeight.w400,
78 - letterSpacing: -0.07,
79 - color: colors.primary,
21 + return Semantics(
22 + button: true,
23 + label: "${S.of(context).network_prefix_on} $networkName",
24 + child: GestureDetector(
25 + onTap: onTap,
26 + child: ExcludeSemantics(
27 + child: Row(
28 + mainAxisSize: MainAxisSize.min,
29 + children: [
30 + Text(
31 + S.of(context).network_prefix_on,
32 + style: Theme.of(context)
33 + .textTheme
34 + .bodyMedium
35 + ?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
36 + ),
37 + const SizedBox(width: 12),
38 + CakeImageWidget(
39 + imageUrl: networkIconPath,
40 + width: 16,
41 + height: 16,
42 + fit: BoxFit.cover,
43 + color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
44 + ),
45 + const SizedBox(width: 8),
46 + Text(
47 + networkName,
48 + style: Theme.of(context).textTheme.bodyMedium?.copyWith(
49 + fontWeight: FontWeight.w400,
50 + letterSpacing: -0.07,
51 + color: colors.primary,
52 + ),
53 + ),
54 + const SizedBox(width: 8),
55 + Container(
56 + width: 28,
57 + height: 28,
58 + decoration: BoxDecoration(
59 + color: colors.surfaceContainer,
60 + shape: BoxShape.circle,
61 ),
62 + child: Icon(
63 + Icons.keyboard_arrow_down,
64 + size: 18,
65 + color: colors.onSurfaceVariant,
66 + ),
67 + ),
68 + ],
69 ),
82 - const SizedBox(width: 8),
83 - Container(
84 - width: 28,
85 - height: 28,
86 - decoration: BoxDecoration(
87 - color: colors.surfaceContainer,
88 - shape: BoxShape.circle,
89 - ),
90 - child: Icon(
91 - Icons.keyboard_arrow_down,
92 - size: 18,
93 - color: colors.onSurfaceVariant,
94 - ),
95 - ),
96 - ],
70 + ),
71 ),
72 );
73 }
lib/new-ui/widgets/anypay/send_to_network_page.dart deleted
-122
@@ -1,122 +0,0 @@
1 -import "package:cake_wallet/generated/i18n.dart";
2 -import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
3 -import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
4 -import "package:cake_wallet/src/widgets/cake_image_widget.dart";
5 -import "package:cw_core/currency_for_wallet_type.dart";
6 -import "package:flutter/material.dart";
7 -
8 -class SendToNetworkPage extends StatelessWidget {
9 - const SendToNetworkPage({
10 - required this.title,
11 - required this.destinationNetworkName,
12 - required this.destinationIconPath,
13 - required this.currentNetworkName,
14 - required this.onSwitchWallet,
15 - required this.onSwap,
16 - super.key,
17 - });
18 -
19 - final String title;
20 - final String destinationNetworkName;
21 - final String destinationIconPath;
22 - final String currentNetworkName;
23 - final VoidCallback onSwitchWallet;
24 - final VoidCallback onSwap;
25 -
26 - @override
27 - Widget build(BuildContext context) {
28 - final colors = Theme.of(context).colorScheme;
29 - return Material(
30 - color: colors.surface,
31 - child: SafeArea(
32 - child: Column(
33 - children: [
34 - ModalTopBar(
35 - title: "",
36 - leadingIcon: const Icon(Icons.arrow_back_ios_new),
37 - leadingSemanticLabel: S.of(context).seed_alert_back,
38 - onLeadingPressed: () => Navigator.of(context).maybePop(),
39 - trailingIcon: const Icon(Icons.close),
40 - trailingSemanticLabel: S.of(context).close,
41 - onTrailingPressed: () => Navigator.of(context).maybePop(),
42 - ),
43 - Expanded(
44 - child: Center(
45 - child: Padding(
46 - padding: const EdgeInsets.symmetric(horizontal: 24),
47 - child: Column(
48 - mainAxisSize: MainAxisSize.min,
49 - children: [
50 - CakeImageWidget(
51 - imageUrl: destinationIconPath,
52 - width: 75,
53 - height: 75,
54 - fit: BoxFit.contain,
55 - color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
56 - ),
57 - const SizedBox(height: 24),
58 - Text(
59 - title,
60 - textAlign: TextAlign.center,
61 - style: Theme.of(context).textTheme.titleLarge?.copyWith(
62 - fontSize: 20,
63 - fontWeight: FontWeight.w500,
64 - letterSpacing: -0.1,
65 - ),
66 - ),
67 - const SizedBox(height: 16),
68 - Text(
69 - S.of(context).send_to_network_description(
70 - destinationNetworkName,
71 - currentNetworkName,
72 - ),
73 - textAlign: TextAlign.center,
74 - style: Theme.of(context)
75 - .textTheme
76 - .bodyMedium
77 - ?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
78 - ),
79 - ],
80 - ),
81 - ),
82 - ),
83 - ),
84 - Padding(
85 - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
86 - child: Column(
87 - mainAxisSize: MainAxisSize.min,
88 - children: [
89 - NewPrimaryButton(
90 - onPressed: onSwitchWallet,
91 - image: CakeImageWidget(
92 - imageUrl: "assets/new-ui/wallet_filled.svg",
93 - width: 24,
94 - height: 24,
95 - colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
96 - ),
97 - text: S.of(context).switch_to_x_wallet(destinationNetworkName),
98 - color: colors.primary,
99 - textColor: colors.onPrimary,
100 - ),
101 - const SizedBox(height: 12),
102 - NewPrimaryButton(
103 - onPressed: onSwap,
104 - image: CakeImageWidget(
105 - imageUrl: "assets/new-ui/swap_arrows.svg",
106 - width: 24,
107 - height: 24,
108 - colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
109 - ),
110 - text: S.of(context).swap_from_network(currentNetworkName),
111 - color: colors.surfaceContainer,
112 - textColor: colors.primary,
113 - ),
114 - ],
115 - ),
116 - ),
117 - ],
118 - ),
119 - ),
120 - );
121 - }
122 -}
lib/new-ui/widgets/coins_page/action_row/coin_action_row.dart
+6 -7
@@ -162,7 +162,8 @@ class CoinActionRow extends StatelessWidget {
162
163 if (code == null || code.isEmpty) return;
164
165 - late final PaymentRequest req;
165 + PaymentRequest? req;
166 + String? rawInput;
167 var unspentCoinType = UnspentCoinType.any;
168 if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) {
169 unspentCoinType = UnspentCoinType.lightning;
@@ -172,8 +173,6 @@ class CoinActionRow extends StatelessWidget {
173 unspentCoinType = UnspentCoinType.lightning;
174 final amount = (await LNURL.getPayRequestAmount(code))?.toString() ?? "0";
175 req = PaymentRequest(code, amount, "", "", "");
175 - } else if (OpenCryptoPayService.isOpenCryptoPayQR(code)) {
176 - req = PaymentRequest(code, "", "", "", "");
176 } else if (Uri.tryParse(code)?.scheme == "wc") {
177 if (!isWalletConnectCompatibleChain(walletType)) {
178 showPopUp<void>(
@@ -190,19 +189,19 @@ class CoinActionRow extends StatelessWidget {
189 Navigator.of(context)
190 .pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
191 return;
193 - } else if (["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
192 + } else if (!OpenCryptoPayService.isOpenCryptoPayQR(code) &&
193 + ["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
194 Navigator.of(context).pushNamed(Routes.newNode,
195 arguments: {"editingNode": Node.fromUri(Uri.parse(code), walletType)});
196 return;
197 } else {
198 - final uri = Uri.tryParse(code);
199 - if (uri == null) return;
200 - req = PaymentRequest.fromUri(uri);
198 + rawInput = code;
199 }
200
201 final sendPage = getIt.get<NewSendPage>(
202 param1: SendPageParams(
203 initialPaymentRequest: req,
204 + initialRawInput: rawInput,
205 unspentCoinType: unspentCoinType,
206 ),
207 );
lib/new-ui/widgets/swap_page/anypay_swap_footer.dart
+16
@@ -18,6 +18,22 @@ class AnyPaySwapFooter extends StatelessWidget {
18 final textTheme = Theme.of(context).textTheme;
19 return Observer(
20 builder: (_) {
21 + if (exchangeViewModel.noProviderForPair) {
22 + return Container(
23 + alignment: Alignment.center,
24 + height: 48,
25 + padding: const EdgeInsets.symmetric(horizontal: 16),
26 + decoration: BoxDecoration(
27 + borderRadius: BorderRadius.circular(18),
28 + border: Border.all(color: colors.error),
29 + ),
30 + child: Text(
31 + S.of(context).no_providers_available,
32 + style: textTheme.bodyMedium?.copyWith(color: colors.error, letterSpacing: -0.07),
33 + ),
34 + );
35 + }
36 +
37 if (exchangeViewModel.depositAmount.isEmpty) {
38 return const SizedBox.shrink();
39 }
lib/new-ui/widgets/swap_page/swap_amount_box.dart
+157 -205
@@ -8,6 +8,7 @@ import "package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart";
8 import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart";
9 import "package:cake_wallet/new-ui/widgets/swap_page/refund_address_modal.dart";
10 import "package:cake_wallet/new-ui/widgets/swap_page/swap_address_selection_modal.dart";
11 +import "package:cake_wallet/new-ui/widgets/swap_page/swap_source_selector.dart";
12 import "package:cake_wallet/src/widgets/cake_image_widget.dart";
13 import "package:cake_wallet/utils/decimal_input_formatter.dart";
14 import "package:cake_wallet/utils/permission_handler.dart";
@@ -82,10 +83,14 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
83 final fiatAmountController = TextEditingController();
84 final amountFocusNode = FocusNode();
85 final memoController = TextEditingController();
86 + final _amountFieldKey = GlobalKey<FormFieldState<String>>();
87 mobx.ReactionDisposer? _memoReactionDisposer;
88
89 @override
90 void initState() {
91 + amountController
92 + .addListener(() => _amountFieldKey.currentState?.didChange(amountController.text));
93 +
94 if (widget.isReceiverCard) {
95 memoController.text = widget.exchangeViewModel.receiveAddressExtraId;
96
@@ -145,95 +150,13 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
150 final colors = Theme.of(context).colorScheme;
151
152 if (widget.sourceSelectorMode) {
148 - final available = _sourceBalance()?.amount.split(" ").first ?? "—";
149 - return Column(
150 - spacing: 12,
151 - crossAxisAlignment: CrossAxisAlignment.start,
152 - children: [
153 - GestureDetector(
154 - onTap: _presentCurrencyPicker,
155 - child: Container(
156 - height: 52,
157 - padding: const EdgeInsets.symmetric(horizontal: 12),
158 - decoration: BoxDecoration(
159 - color: colors.surfaceContainer,
160 - borderRadius: BorderRadius.circular(18),
161 - ),
162 - child: Row(
163 - children: [
164 - CakeImageWidget(
165 - imageUrl: widget.currency.iconPath ?? "",
166 - width: 24,
167 - height: 24,
168 - ),
169 - const SizedBox(width: 8),
170 - Text(
171 - currencyToShow,
172 - style: Theme.of(context).textTheme.bodyLarge?.copyWith(letterSpacing: -0.08),
173 - ),
174 - if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
175 - const SizedBox(width: 4),
176 - CakeImageWidget(
177 - imageUrl: chainIconPath,
178 - width: 12,
179 - height: 12,
180 - colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
181 - ),
182 - ],
183 - const Spacer(),
184 - CakeImageWidget(
185 - imageUrl: "assets/new-ui/chooser.svg",
186 - width: 12,
187 - height: 12,
188 - colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
189 - ),
190 - ],
191 - ),
192 - ),
193 - ),
194 - Padding(
195 - padding: const EdgeInsets.symmetric(horizontal: 16),
196 - child: Row(
197 - mainAxisAlignment: MainAxisAlignment.spaceBetween,
198 - children: [
199 - Flexible(
200 - child: Row(
201 - mainAxisSize: MainAxisSize.min,
202 - children: [
203 - CakeImageWidget(
204 - imageUrl: "assets/new-ui/wallet_filled.svg",
205 - width: 16,
206 - height: 16,
207 - colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
208 - ),
209 - const SizedBox(width: 4),
210 - Flexible(
211 - child: Text(
212 - widget.walletName ?? "",
213 - overflow: TextOverflow.ellipsis,
214 - style: Theme.of(context).textTheme.bodySmall?.copyWith(
215 - fontWeight: FontWeight.w500,
216 - letterSpacing: -0.06,
217 - color: colors.onSurfaceVariant,
218 - ),
219 - ),
220 - ),
221 - ],
222 - ),
223 - ),
224 - const SizedBox(width: 8),
225 - Text(
226 - "${S.of(context).avl}: $available",
227 - style: Theme.of(context).textTheme.bodySmall?.copyWith(
228 - fontWeight: FontWeight.w500,
229 - letterSpacing: -0.06,
230 - color: colors.primary,
231 - ),
232 - ),
233 - ],
234 - ),
235 - ),
236 - ],
153 + return SwapSourceSelector(
154 + currencyIconPath: widget.currency.iconPath ?? "",
155 + currencyLabel: currencyToShow,
156 + chainIconPath: chainIconPath,
157 + walletName: widget.walletName,
158 + availableBalance: _sourceBalance()?.amount.split(" ").first ?? "—",
159 + onTap: _presentCurrencyPicker,
160 );
161 }
162
@@ -260,139 +183,168 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
183 child: Column(
184 spacing: 12,
185 children: [
263 - GestureDetector(
264 - behavior: HitTestBehavior.opaque,
265 - onTap: amountFocusNode.requestFocus,
266 - child: Row(
267 - mainAxisAlignment: MainAxisAlignment.spaceBetween,
268 - spacing: 4,
186 + FormField<String>(
187 + key: _amountFieldKey,
188 + initialValue: amountController.text,
189 + validator: _fiatInputMode ? null : widget.currencyValueValidator,
190 + builder: (state) => Column(
191 + crossAxisAlignment: CrossAxisAlignment.start,
192 children: [
270 - Expanded(
193 + GestureDetector(
194 + behavior: HitTestBehavior.opaque,
195 + onTap: amountFocusNode.requestFocus,
196 child: Row(
197 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
198 spacing: 4,
273 - crossAxisAlignment: CrossAxisAlignment.center,
199 children: [
275 - Flexible(
276 - child: IntrinsicWidth(
277 - child: Observer(
278 - builder: (_) {
279 - final showFetching = !widget.isReceiverCard &&
280 - widget.exchangeViewModel.isFixedRateMode &&
281 - widget.exchangeViewModel.receiveAmount.isNotEmpty &&
282 - widget.exchangeViewModel.depositAmount.isEmpty;
283 - return TextFormField(
284 - keyboardType: TextInputType.numberWithOptions(
285 - signed: false,
286 - decimal: !widget.useBaseUnit,
287 - ),
288 - validator:
289 - _fiatInputMode ? null : widget.currencyValueValidator,
290 - controller:
291 - _fiatInputMode ? fiatAmountController : amountController,
292 - focusNode: amountFocusNode,
293 - style: TextStyle(
294 - fontSize: 28,
295 - color: Theme.of(context).colorScheme.onSurface,
296 - fontWeight: FontWeight.w400,
297 - ),
298 - decoration: InputDecoration(
299 - contentPadding: EdgeInsets.zero,
300 - isDense: true,
301 - hintText: showFetching ? S.of(context).fetching : "0",
302 - fillColor: Colors.transparent,
303 - hoverColor: Colors.transparent,
304 - focusedBorder: InputBorder.none,
305 - enabledBorder: InputBorder.none,
200 + Expanded(
201 + child: Row(
202 + spacing: 4,
203 + crossAxisAlignment: CrossAxisAlignment.center,
204 + children: [
205 + Flexible(
206 + child: IntrinsicWidth(
207 + child: Observer(
208 + builder: (_) {
209 + final showFetching = !widget.isReceiverCard &&
210 + widget.exchangeViewModel.isFixedRateMode &&
211 + widget.exchangeViewModel.receiveAmount.isNotEmpty &&
212 + widget.exchangeViewModel.depositAmount.isEmpty;
213 + return TextField(
214 + keyboardType: TextInputType.numberWithOptions(
215 + signed: false,
216 + decimal: !widget.useBaseUnit,
217 + ),
218 + onChanged: state.didChange,
219 + controller: _fiatInputMode
220 + ? fiatAmountController
221 + : amountController,
222 + focusNode: amountFocusNode,
223 + style: TextStyle(
224 + fontSize: 28,
225 + color: Theme.of(context).colorScheme.onSurface,
226 + fontWeight: FontWeight.w400,
227 + ),
228 + decoration: InputDecoration(
229 + contentPadding: EdgeInsets.zero,
230 + isDense: true,
231 + hintText: showFetching ? S.of(context).fetching : "0",
232 + fillColor: Colors.transparent,
233 + hoverColor: Colors.transparent,
234 + focusedBorder: InputBorder.none,
235 + enabledBorder: InputBorder.none,
236 + ),
237 + inputFormatters: <TextInputFormatter>[
238 + DecimalInputFormatter(
239 + maxDecimals: widget.useBaseUnit
240 + ? 0
241 + : widget.currency.decimals,
242 + ),
243 + ],
244 + );
245 + },
246 ),
307 - inputFormatters: <TextInputFormatter>[
308 - DecimalInputFormatter(
309 - maxDecimals:
310 - widget.useBaseUnit ? 0 : widget.currency.decimals,
247 + ),
248 + ),
249 + if (_fiatInputMode)
250 + Center(
251 + child: Text(
252 + widget.exchangeViewModel.fiat.title,
253 + style: TextStyle(
254 + fontSize: 16,
255 + color: Theme.of(context).colorScheme.onSurfaceVariant,
256 ),
312 - ],
313 - );
314 - },
315 - ),
257 + ),
258 + ),
259 + ],
260 ),
261 ),
318 - if (_fiatInputMode)
319 - Center(
320 - child: Text(
321 - widget.exchangeViewModel.fiat.title,
322 - style: TextStyle(
323 - fontSize: 16,
324 - color: Theme.of(context).colorScheme.onSurfaceVariant,
262 + GestureDetector(
263 + onTap: _presentCurrencyPicker,
264 + child: Container(
265 + decoration: BoxDecoration(
266 + color: Theme.of(context).colorScheme.surface,
267 + borderRadius: BorderRadius.circular(999999),
268 + ),
269 + child: Padding(
270 + padding:
271 + const EdgeInsets.only(top: 4, bottom: 4, left: 4, right: 4),
272 + child: Row(
273 + mainAxisAlignment: MainAxisAlignment.center,
274 + children: [
275 + CakeImageWidget(
276 + imageUrl: widget.currency.iconPath ?? "",
277 + width: 28,
278 + height: 28,
279 + ),
280 + const SizedBox(width: 10),
281 + Text(
282 + currencyToShow,
283 + textAlign: TextAlign.center,
284 + ),
285 + if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
286 + const SizedBox(width: 4),
287 + CakeImageWidget(
288 + imageUrl: chainIconPath,
289 + width: 12,
290 + height: 12,
291 + colorFilter: ColorFilter.mode(
292 + Theme.of(context).colorScheme.onSurfaceVariant,
293 + BlendMode.srcIn,
294 + ),
295 + ),
296 + const SizedBox(width: 6),
297 + ] else
298 + const SizedBox(width: 10),
299 + Container(
300 + width: 16,
301 + height: 16,
302 + decoration: BoxDecoration(
303 + borderRadius: BorderRadius.circular(9999999999),
304 + color: Theme.of(context).colorScheme.surfaceContainerHigh,
305 + ),
306 + child: Padding(
307 + padding: const EdgeInsets.all(4),
308 + child: RotatedBox(
309 + quarterTurns: 2,
310 + child: CakeImageWidget(
311 + imageUrl: "assets/new-ui/dropdown_arrow.svg",
312 + width: 4,
313 + height: 4,
314 + colorFilter: ColorFilter.mode(
315 + Theme.of(context).colorScheme.primary,
316 + BlendMode.srcIn,
317 + ),
318 + ),
319 + ),
320 + ),
321 + ),
322 + const SizedBox(width: 4),
323 + ],
324 ),
325 ),
326 ),
327 + ),
328 ],
329 ),
330 ),
331 - GestureDetector(
332 - onTap: _presentCurrencyPicker,
333 - child: Container(
334 - decoration: BoxDecoration(
335 - color: Theme.of(context).colorScheme.surface,
336 - borderRadius: BorderRadius.circular(999999),
337 - ),
338 - child: Padding(
339 - padding: const EdgeInsets.only(top: 4, bottom: 4, left: 4, right: 4),
340 - child: Row(
341 - mainAxisAlignment: MainAxisAlignment.center,
342 - children: [
343 - CakeImageWidget(
344 - imageUrl: widget.currency.iconPath ?? "",
345 - width: 28,
346 - height: 28,
347 - ),
348 - const SizedBox(width: 10),
349 - Text(
350 - currencyToShow,
351 - textAlign: TextAlign.center,
352 - ),
353 - if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
354 - const SizedBox(width: 4),
355 - CakeImageWidget(
356 - imageUrl: chainIconPath,
357 - width: 12,
358 - height: 12,
359 - colorFilter: ColorFilter.mode(
360 - Theme.of(context).colorScheme.onSurfaceVariant,
361 - BlendMode.srcIn,
362 - ),
363 - ),
364 - const SizedBox(width: 6),
365 - ] else
366 - const SizedBox(width: 10),
367 - Container(
368 - width: 16,
369 - height: 16,
370 - decoration: BoxDecoration(
371 - borderRadius: BorderRadius.circular(9999999999),
372 - color: Theme.of(context).colorScheme.surfaceContainerHigh,
373 - ),
374 - child: Padding(
375 - padding: const EdgeInsets.all(4),
376 - child: RotatedBox(
377 - quarterTurns: 2,
378 - child: CakeImageWidget(
379 - imageUrl: "assets/new-ui/dropdown_arrow.svg",
380 - width: 4,
381 - height: 4,
382 - colorFilter: ColorFilter.mode(
383 - Theme.of(context).colorScheme.primary,
384 - BlendMode.srcIn,
385 - ),
386 - ),
387 - ),
388 - ),
389 - ),
390 - const SizedBox(width: 4),
391 - ],
331 + if (state.hasError)
332 + Padding(
333 + padding: const EdgeInsets.only(top: 6),
334 + child: Semantics(
335 + container: true,
336 + liveRegion: true,
337 + label: "${S.of(context).amount}${state.errorText!}",
338 + excludeSemantics: true,
339 + child: Text(
340 + state.errorText!,
341 + style: TextStyle(
342 + fontSize: 12,
343 + color: Theme.of(context).colorScheme.error,
344 + ),
345 ),
346 ),
347 ),
395 - ),
348 ],
349 ),
350 ),
lib/new-ui/widgets/swap_page/swap_from_send_args.dart
+8 -3
@@ -1,4 +1,4 @@
1 -import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
1 +import "package:cake_wallet/core/anypay/anypay_models.dart";
2 import "package:cw_core/amount/money.dart";
3 import "package:cw_core/crypto_currency.dart";
4 import "package:cw_core/wallet_type.dart";
@@ -8,13 +8,18 @@ class SwapFromSendArgs {
8 required this.recipientAddress,
9 required this.receiveCurrency,
10 required this.targetWalletType,
11 - this.depositBalanceByAsset,
11 this.receiveAmount,
12 });
13
14 + factory SwapFromSendArgs.fromIntent(AnyPaySwapIntent intent) => SwapFromSendArgs(
15 + recipientAddress: intent.recipientAddress,
16 + receiveCurrency: intent.receiveCurrency,
17 + targetWalletType: intent.targetWalletType,
18 + receiveAmount: intent.receiveAmount,
19 + );
20 +
21 final String recipientAddress;
22 final CryptoCurrency receiveCurrency;
23 final WalletType targetWalletType;
18 - final Map<CryptoCurrency, CurrencyPickerBalance>? depositBalanceByAsset;
24 final Money? receiveAmount;
25 }
lib/new-ui/widgets/swap_page/swap_source_selector.dart new
+118
@@ -0,0 +1,118 @@
1 +import "package:cake_wallet/generated/i18n.dart";
2 +import "package:cake_wallet/src/widgets/cake_image_widget.dart";
3 +import "package:flutter/material.dart";
4 +
5 +class SwapSourceSelector extends StatelessWidget {
6 + const SwapSourceSelector({
7 + required this.currencyIconPath,
8 + required this.currencyLabel,
9 + required this.availableBalance,
10 + required this.onTap,
11 + this.chainIconPath,
12 + this.walletName,
13 + super.key,
14 + });
15 +
16 + final String currencyIconPath;
17 + final String currencyLabel;
18 + final String availableBalance;
19 + final VoidCallback onTap;
20 + final String? chainIconPath;
21 + final String? walletName;
22 +
23 + @override
24 + Widget build(BuildContext context) {
25 + final colors = Theme.of(context).colorScheme;
26 + final chainIcon = chainIconPath;
27 +
28 + return Column(
29 + spacing: 12,
30 + crossAxisAlignment: CrossAxisAlignment.start,
31 + children: [
32 + GestureDetector(
33 + onTap: onTap,
34 + child: Container(
35 + height: 52,
36 + padding: const EdgeInsets.symmetric(horizontal: 12),
37 + decoration: BoxDecoration(
38 + color: colors.surfaceContainer,
39 + borderRadius: BorderRadius.circular(18),
40 + ),
41 + child: Row(
42 + children: [
43 + CakeImageWidget(
44 + imageUrl: currencyIconPath,
45 + width: 24,
46 + height: 24,
47 + ),
48 + const SizedBox(width: 8),
49 + Text(
50 + currencyLabel,
51 + style: Theme.of(context).textTheme.bodyLarge?.copyWith(letterSpacing: -0.08),
52 + ),
53 + if (chainIcon != null && chainIcon.isNotEmpty) ...[
54 + const SizedBox(width: 4),
55 + CakeImageWidget(
56 + imageUrl: chainIcon,
57 + width: 12,
58 + height: 12,
59 + colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
60 + ),
61 + ],
62 + const Spacer(),
63 + CakeImageWidget(
64 + imageUrl: "assets/new-ui/chooser.svg",
65 + width: 12,
66 + height: 12,
67 + colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
68 + ),
69 + ],
70 + ),
71 + ),
72 + ),
73 + Padding(
74 + padding: const EdgeInsets.symmetric(horizontal: 16),
75 + child: Row(
76 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
77 + children: [
78 + Flexible(
79 + child: Row(
80 + mainAxisSize: MainAxisSize.min,
81 + children: [
82 + CakeImageWidget(
83 + imageUrl: "assets/new-ui/wallet_filled.svg",
84 + width: 16,
85 + height: 16,
86 + colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
87 + ),
88 + const SizedBox(width: 4),
89 + Flexible(
90 + child: Text(
91 + walletName ?? "",
92 + overflow: TextOverflow.ellipsis,
93 + style: Theme.of(context).textTheme.bodySmall?.copyWith(
94 + fontWeight: FontWeight.w500,
95 + letterSpacing: -0.06,
96 + color: colors.onSurfaceVariant,
97 + ),
98 + ),
99 + ),
100 + ],
101 + ),
102 + ),
103 + const SizedBox(width: 8),
104 + Text(
105 + S.of(context).available_balance_short(availableBalance),
106 + style: Theme.of(context).textTheme.bodySmall?.copyWith(
107 + fontWeight: FontWeight.w500,
108 + letterSpacing: -0.06,
109 + color: colors.primary,
110 + ),
111 + ),
112 + ],
113 + ),
114 + ),
115 + ],
116 + );
117 + }
118 +}
lib/router.dart
+4 -5
@@ -149,7 +149,6 @@ import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_mode
149 import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
150 import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
151 import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
152 -import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
152 import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart';
153 import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
154 import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
@@ -445,6 +444,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
444 case Routes.send:
445 final args = settings.arguments as Map<String, dynamic>?;
446 final initialPaymentRequest = args?['paymentRequest'] as PaymentRequest?;
447 + final initialRawInput = args?["rawLink"] as String?;
448 final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?;
449
450 return handleRouteWithPlatformAwareness(
@@ -452,6 +452,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
452 child: getIt.get<NewSendPage>(
453 param1: SendPageParams(
454 initialPaymentRequest: initialPaymentRequest,
455 + initialRawInput: initialRawInput,
456 unspentCoinType: coinTypeToSpendFrom ?? UnspentCoinType.any),
457 ),
458 ),
@@ -473,7 +474,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
474 case Routes.newReceivePage:
475 if (FeatureFlag.hasNewUi) {
476 return handleRouteWithPlatformAwareness(
476 - (context) => Material(child: getIt.get<NewReceivePage>(param1: false, param2: null)),
477 + (context) => Material(child: getIt.get<NewReceivePage>(param1: false, param2: null)),
478 settings: settings,
479 );
480 }
@@ -835,9 +836,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
836 final useTestnet = args['useTestnet'] as bool;
837 final toggleTestnet = args['toggleTestnet'] as Function(bool? val);
838 final zcashNetwork = args['zcashNetwork'] as int? ?? ZcashNetworkType.mainnet;
838 - final setZcashNetwork =
839 - args['setZcashNetwork'] as void Function(int network)? ?? (_) {};
840 - final restoredWallet = args['restoredWallet'] as RestoredWallet?;
839 + final setZcashNetwork = args['setZcashNetwork'] as void Function(int network)? ?? (_) {};
840
841 final viewModelParam = {'type': type, 'isPow': false};
842
lib/utils/payment_request.dart
+25
@@ -125,6 +125,31 @@ class PaymentRequest {
125 );
126 }
127
128 + PaymentRequest copyWith({
129 + String? address,
130 + String? amount,
131 + String? note,
132 + String? scheme,
133 + String? pjUri,
134 + String? callbackUrl,
135 + String? callbackMessage,
136 + String? contractAddress,
137 + int? chainId,
138 + String? rawTokenAmount,
139 + }) =>
140 + PaymentRequest(
141 + address ?? this.address,
142 + amount ?? this.amount,
143 + note ?? this.note,
144 + scheme ?? this.scheme,
145 + pjUri ?? this.pjUri,
146 + callbackUrl: callbackUrl ?? this.callbackUrl,
147 + callbackMessage: callbackMessage ?? this.callbackMessage,
148 + contractAddress: contractAddress ?? this.contractAddress,
149 + chainId: chainId ?? this.chainId,
150 + rawTokenAmount: rawTokenAmount ?? this.rawTokenAmount,
151 + );
152 +
153 final String address;
154 final String amount;
155 final String note;
lib/view_model/exchange/exchange_view_model.dart
+4 -20
@@ -2,12 +2,10 @@ import 'dart:async';
2 import 'dart:collection';
3 import 'dart:convert';
4 import 'dart:developer';
5 -import 'dart:math' show min;
5
6 import 'package:bitcoin_base/bitcoin_base.dart';
7 import 'package:cake_wallet/.secrets.g.dart' as secrets;
8 import 'package:cake_wallet/bitcoin/bitcoin.dart';
10 -import 'package:cake_wallet/core/address_validator.dart';
9 import 'package:cake_wallet/core/amount_parsing_proxy.dart';
10 import 'package:cake_wallet/core/create_trade_result.dart';
11 import 'package:cake_wallet/core/fiat_conversion_service.dart';
@@ -415,6 +413,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
413 @observable
414 bool tradeStarted = false;
415
416 + @observable
417 + bool noProviderForPair = false;
418 +
419 @observable
420 bool isSendFromExternal = false;
421
@@ -980,6 +981,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
981 bestRate = _sortedAvailableProviders.keys.first;
982 bestRateProvider = _sortedAvailableProviders.values.first;
983 }
984 + noProviderForPair = _sortedAvailableProviders.isEmpty;
985 }
986
987 @action
@@ -1603,24 +1605,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
1605 }
1606 }
1607
1606 - String? _addressTypeValidation(String refundAddress, String receiveAddress) {
1607 - final isRefundAddressSP =
1608 - RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(refundAddress);
1609 - if (isRefundAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_refund}';
1610 -
1611 - final isReceiveAddressSP =
1612 - RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(receiveAddress);
1613 - if (isReceiveAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_receive}';
1614 -
1615 - final isRefundAddressMWEB = RegExp(AddressValidator.mWebAddressPattern).hasMatch(refundAddress);
1616 - if (isRefundAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_refund}';
1617 -
1618 - final isReceiveAddressMWEB =
1619 - RegExp(AddressValidator.mWebAddressPattern).hasMatch(receiveAddress);
1620 - if (isReceiveAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_receive}';
1621 - return null;
1622 - }
1623 -
1608 void _defineIsReceiveAmountEditable() {
1609 /*if ((provider is ChangeNowExchangeProvider)
1610 &&(depositCurrency == CryptoCurrency.xmr)
lib/view_model/link_view_model.dart
+21 -2
@@ -25,6 +25,10 @@ class LinkViewModel {
25 Uri? currentLink;
26 bool readyToOpenPage = false;
27
28 + final StreamController<Uri> _incomingPaymentLinksController = StreamController<Uri>.broadcast();
29 +
30 + Stream<Uri> get incomingPaymentLinks => _incomingPaymentLinksController.stream;
31 +
32 bool get _isValidPaymentUri => currentLink?.path.isNotEmpty ?? false;
33 bool get isWalletConnectLink => currentLink?.authority == 'wc';
34 bool get isNanoGptLink => currentLink?.scheme == 'nano-gpt';
@@ -96,7 +100,10 @@ class LinkViewModel {
100 }
101
102 if (_isValidPaymentUri) {
99 - return {"paymentRequest": PaymentRequest.fromUri(currentLink)};
103 + return {
104 + "paymentRequest": PaymentRequest.fromUri(currentLink),
105 + "rawLink": currentLink.toString(),
106 + };
107 }
108
109 return null;
@@ -132,8 +139,20 @@ class LinkViewModel {
139 return;
140 }
141
135 - // Prevent navigating to the same route again.
142 + if (route == Routes.send &&
143 + !isQuickActionLink &&
144 + currentLink != null &&
145 + _incomingPaymentLinksController.hasListener) {
146 + final link = currentLink!;
147 + currentLink = null;
148 + _incomingPaymentLinksController.add(link);
149 + return;
150 + }
151 +
152 + // Prevent navigating to the same route again and clear the link so a later auth
153 + // event cannot refire it.
154 if (appStore.currentRouteName == route) {
155 + currentLink = null;
156 return;
157 }
158
lib/view_model/send/send_view_model.dart
+5
@@ -1730,6 +1730,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
1730 }
1731 }
1732
1733 + @action
1734 + void applyAnyPayCurrency(CryptoCurrency currency) {
1735 + selectedCryptoCurrency = currency;
1736 + }
1737 +
1738 String _decodeMethodSelector(String s) =>
1739 (s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
1740 }
res/values/strings_ar.arb
+3
@@ -93,6 +93,7 @@
93 "available": "متاح",
94 "available_balance": "الرصيد المتاح",
95 "available_balance_description": "يشير “Available Balance” أو “Confirmed Balance” إلى الأموال التي يمكن إنفاقها فورًا. إذا ظهرت الأموال في الرصيد السفلي ولكن ليس في الرصيد العلوي، فعليك الانتظار بضع دقائق حتى تحصل الأموال الواردة على المزيد من تأكيدات الشبكة. بعد حصولها على المزيد من التأكيدات، ستصبح قابلة للإنفاق.",
96 + "available_balance_short": "المبلغ الإجمالي: ${amount}",
97 "avg_savings": "متوسط التوفير",
98 "avl": "AVL",
99 "awaitDAppProcessing": "يرجى الانتظار حتى ينتهي الـ dApp من المعالجة.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "معرّف المبادلة",
1151 "swap_provider_initial_desc": "اختر نوع مزوّد المبادلة الذي تفضّل استخدامه عند إجراء المبادلة داخل التطبيق",
1152 "swap_providers": "مزودو المبادلة",
1153 + "swap_unavailable_needs_network_wallet": "لا يمكن لهذا النوع من العنوان تلقي الأموال من المبادلة. أنت بحاجة إلى محفظة ${network} لإرسالها إلى هذا العنوان.",
1154 + "swap_unavailable_switch_to_network": "لا يمكن لهذا النوع من العنوان تلقي الأموال من المبادلة. قم بالتبديل إلى محفظة ${network} الخاصة بك للمتابعة.",
1155 "sweeping_wallet": "محفظة السحب الشامل",
1156 "sweeping_wallet_alert": "لن يستغرق هذا وقتًا طويلًا. لا تغادر هذه الشاشة وإلا فقد تُفقد الأموال المُسحوبة.",
1157 "swipe_to_bridge": "اسحب لإجراء الجسر",
res/values/strings_bg.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Налично",
94 "available_balance": "Наличен баланс",
95 "available_balance_description": "„Наличният баланс“ или „Потвърденият баланс“ са средства, които могат да бъдат изразходвани веднага. Ако средствата се появяват в долния баланс, но не и в горния, трябва да изчакате няколко минути, докато входящите средства получат повече мрежови потвърждения. След като получат повече потвърждения, те ще могат да бъдат изразходвани.",
96 + "available_balance_short": "Ср.: ${amount}",
97 "avg_savings": "Средни спестявания",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Моля, изчакайте dApp да завърши обработката.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "ID на суап",
1151 "swap_provider_initial_desc": "Изберете типа доставчик за суап, който предпочитате да използвате при суапване в",
1152 "swap_providers": "Доставчици за суап",
1153 + "swap_unavailable_needs_network_wallet": "Този тип адрес не може да получава средства от суап. Имате нужда от ${network} портфейл, за да изпратите на този адрес.",
1154 + "swap_unavailable_switch_to_network": "Този тип адрес не може да получава средства от суап. Превключете към вашия ${network} портфейл, за да продължите.",
1155 "sweeping_wallet": "Изчистване на портфейл",
1156 "sweeping_wallet_alert": "Това не би трябвало да отнеме много време. НЕ НАПУСКАЙТЕ ТОЗИ ЕКРАН, ИНАЧЕ ИЗМЕТЕНИТЕ СРЕДСТВА МОГАТ ДА БЪДАТ ЗАГУБЕНИ.",
1157 "swipe_to_bridge": "Плъзнете, за да прехвърлите",
res/values/strings_cs.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Dostupné",
94 "available_balance": "Dostupný zůstatek",
95 "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.",
96 + "available_balance_short": "Avl: ${amount}",
97 "avg_savings": "Prům. úspora",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Počkejte prosím, až dApp dokončí zpracování.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "ID směny",
1151 "swap_provider_initial_desc": "Vyberte typ poskytovatele swapu, kterého chcete používat při swapování v",
1152 "swap_providers": "Poskytovatelé swapu",
1153 + "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}.",
1154 + "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}.",
1155 "sweeping_wallet": "Peněženka pro sweep",
1156 "sweeping_wallet_alert": "To by nemělo trvat dlouho. NEOPUŠTĚJTE TUTO OBRAZOVKU, JINAK MOHOU BÝT PŘESUNUTÉ PROSTŘEDKY ZTRACENY.",
1157 "swipe_to_bridge": "Přemostění přejetím prstem",
res/values/strings_de.arb
+3
@@ -96,6 +96,7 @@
96 "available": "Verfügbar",
97 "available_balance": "Verfügbares Guthaben",
98 "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.",
99 + "available_balance_short": "Durchschnitt: ${amount}",
100 "avg_savings": "Durchschn. Ersparnis",
101 "avl": "Avl",
102 "awaitDAppProcessing": "Bitte warten Sie, bis die dApp die Verarbeitung abgeschlossen hat.",
@@ -1183,6 +1184,8 @@
1184 "swap_provider_initial_desc": "Wählen Sie die Art des Swap-Anbieters aus, die Sie beim Tauschen bevorzugen.",
1185 "swap_providers": "Swap-Anbieter",
1186 "swap_reverse_direction": "Swap-Richtung umkehren",
1187 + "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.",
1188 + "swap_unavailable_switch_to_network": "Dieser Adresstyp kann keine Gelder von einem Swap erhalten. Wechseln Sie zu Ihrem ${network}-Wallet, um fortzufahren.",
1189 "sweeping_wallet": "Wallet leeren",
1190 "sweeping_wallet_alert": "Das sollte nicht lange dauern. Verlassen Sie diesen Bildschirm nicht, sonst können die geleerten Guthaben verloren gehen.",
1191 "swipe_to_bridge": "Zum Bridging wischen",
res/values/strings_en.arb
+4 -1
@@ -96,6 +96,7 @@
96 "available": "Available",
97 "available_balance": "Available Balance",
98 "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.",
99 + "available_balance_short": "Avl: ${amount}",
100 "avg_savings": "Avg. Savings",
101 "avl": "Avl",
102 "awaitDAppProcessing": "Kindly wait for the dApp to finish processing.",
@@ -1180,6 +1181,8 @@
1181 "swap_provider_initial_desc": "Select the type of swap provider you prefer to use when swapping in",
1182 "swap_providers": "Swap providers",
1183 "swap_reverse_direction": "Reverse swap direction",
1184 + "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.",
1185 + "swap_unavailable_switch_to_network": "This address type can't receive funds from a swap. Switch to your ${network} wallet to continue.",
1186 "sweeping_wallet": "Sweeping wallet",
1187 "sweeping_wallet_alert": "This shouldn’t take long. DO NOT LEAVE THIS SCREEN OR THE SWEPT FUNDS MAY BE LOST.",
1188 "swipe_to_bridge": "Swipe to bridge",
@@ -1514,4 +1517,4 @@
1517 "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.",
1518 "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.",
1519 "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."
1517 -}
1520 +}
\ No newline at end of file
res/values/strings_es.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Disponible",
94 "available_balance": "Saldo disponible",
95 "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.",
96 + "available_balance_short": "Valor medio: ${amount}",
97 "avg_savings": "Ahorro prom.",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Espere a que la dApp termine de procesar.",
@@ -1151,6 +1152,8 @@
1152 "swap_id": "ID de intercambio",
1153 "swap_provider_initial_desc": "Seleccione el tipo de proveedor de intercambio que prefiere usar al intercambiar",
1154 "swap_providers": "Proveedores de intercambio",
1155 + "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.",
1156 + "swap_unavailable_switch_to_network": "Este tipo de dirección no puede recibir fondos de un swap. Cambie a su billetera ${network} para continuar.",
1157 "sweeping_wallet": "Cartera de barrido",
1158 "sweeping_wallet_alert": "Esto no debería tardar mucho. NO SALGAS DE ESTA PANTALLA O LOS FONDOS BARRIDOS PODRÍAN PERDERSE.",
1159 "swipe_to_bridge": "Desliza para transferir entre cadenas",
res/values/strings_fa.arb
+3
@@ -93,6 +93,7 @@
93 "available": "در دسترس",
94 "available_balance": "موجودی در دسترس",
95 "available_balance_description": "«موجودی قابل‌دسترس» یا «موجودی تأییدشده» وجوهی هستند که می‌توان بلافاصله خرج کرد.\nاگر وجوه در موجودی پایین‌تر ظاهر شوند اما در موجودی بالا نباشند، باید چند دقیقه صبر کنید تا وجوه ورودی تأییدهای شبکه بیشتری دریافت کنند.\nپس از دریافت تأییدهای بیشتر، قابل خرج کردن خواهند بود.",
96 + "available_balance_short": "Avl: ${amount}",
97 "avg_savings": "میانگین صرفه‌جویی",
98 "avl": "Avl",
99 "awaitDAppProcessing": "لطفاً منتظر بمانید تا پردازش dApp به پایان برسد.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "شناسهٔ سواپ",
1151 "swap_provider_initial_desc": "نوع ارائه‌دهندهٔ سواپ را که ترجیح می‌دهید هنگام سواپ از آن استفاده کنید، انتخاب کنید",
1152 "swap_providers": "ارائه‌دهندگان سواپ",
1153 + "swap_unavailable_needs_network_wallet": "این نوع آدرس نمی‌تواند وجوهی را از مبادله دریافت کند. برای ارسال به این آدرس به یک کیف پول ${network} نیاز دارید.",
1154 + "swap_unavailable_switch_to_network": "این نوع آدرس نمی‌تواند وجوهی را از مبادله دریافت کند. برای ادامه به کیف پول ${network} خود بروید.",
1155 "sweeping_wallet": "در حال انتقال موجودی کیف پول",
1156 "sweeping_wallet_alert": "این کار نباید طول بکشد.\nاین صفحه را ترک نکنید، وگرنه ممکن است وجوه انتقال‌یافته از دست برود.",
1157 "swipe_to_bridge": "برای پل‌زدن، انگشت خود را بکشید",
res/values/strings_fr.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Disponible",
94 "available_balance": "Solde disponible",
95 "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.",
96 + "available_balance_short": "Valeur moyenne : ${amount}",
97 "avg_savings": "Économies moy.",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Veuillez patienter pendant que la dApp termine le traitement.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "ID d’échange",
1151 "swap_provider_initial_desc": "Sélectionnez le type de fournisseur de swap que vous préférez utiliser lors d’un swap",
1152 "swap_providers": "Fournisseurs de swap",
1153 + "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.",
1154 + "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.",
1155 "sweeping_wallet": "Balayage du portefeuille",
1156 "sweeping_wallet_alert": "Cela ne devrait pas prendre longtemps. NE QUITTEZ PAS CET ÉCRAN, SINON LES FONDS BALAYÉS RISQUENT D’ÊTRE PERDUS.",
1157 "swipe_to_bridge": "Glissez pour transférer (bridge)",
res/values/strings_gn.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Oĩva",
94 "available_balance": "Saldo ojepurukuaáva",
95 "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.",
96 + "available_balance_short": "Avl: ${amount}.",
97 "avg_savings": "Ñongatu mbytegua",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Eha'arõmi pe dApp omohu'ã hag̃ua hembiapo.",
@@ -1155,6 +1156,8 @@
1156 "swap_id": "ID ñemoambue rehegua",
1157 "swap_provider_initial_desc": "Eiporavo mba’eichagua swap puruhára reipuruséva rembojopyru hag̃ua swap jave.",
1158 "swap_providers": "Proveedor-kuéra intercambio rehegua",
1159 + "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.",
1160 + "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.",
1161 "sweeping_wallet": "Billetera ñemopotĩ",
1162 "sweeping_wallet_alert": "Kóva ndoguerahámo’ãi heta aravo. ANI REHEJA KO PANTALLA, térã umi fondo oñemopotĩva ikatu okañy.",
1163 "swipe_to_bridge": "Embosyryry embridge hag̃ua",
res/values/strings_ha.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Akwai",
94 "available_balance": "Ma’aunin da ake da shi",
95 "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.",
96 + "available_balance_short": "Avl: ${amount}",
97 "avg_savings": "Matsakaitan Ajiya",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Da fatan za a jira dApp ya kammala sarrafawa.",
@@ -1151,6 +1152,8 @@
1152 "swap_id": "ID na Musanya",
1153 "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",
1154 "swap_providers": "Masu samar da Swap",
1155 + "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.",
1156 + "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.",
1157 "sweeping_wallet": "Ana kwashe kuɗi daga walat",
1158 "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.",
1159 "swipe_to_bridge": "Zame don gada",
res/values/strings_hi.arb
+3
@@ -93,6 +93,7 @@
93 "available": "उपलब्ध",
94 "available_balance": "उपलब्ध शेष राशि",
95 "available_balance_description": "“Available Balance” या “Confirmed Balance” वे धनराशि हैं जिन्हें तुरंत खर्च किया जा सकता है। यदि फंड नीचे वाले बैलेंस में दिखाई देते हैं, लेकिन ऊपर वाले बैलेंस में नहीं, तो आने वाले फंड को नेटवर्क पर और अधिक पुष्टिकरण मिलने के लिए आपको कुछ मिनट इंतज़ार करना होगा। अधिक पुष्टिकरण मिलने के बाद, वे खर्च करने योग्य हो जाएंगे।",
96 + "available_balance_short": "एवीएल: ${amount}",
97 "avg_savings": "औसत बचत",
98 "avl": "अवल",
99 "awaitDAppProcessing": "कृपया dApp की प्रोसेसिंग पूरी होने तक प्रतीक्षा करें।",
@@ -1151,6 +1152,8 @@
1152 "swap_id": "स्वैप आईडी",
1153 "swap_provider_initial_desc": "स्वैप करते समय आप जिस प्रकार के स्वैप प्रदाता का उपयोग करना पसंद करते हैं, उसका चयन करें",
1154 "swap_providers": "स्वैप प्रदाता",
1155 + "swap_unavailable_needs_network_wallet": "यह पता प्रकार स्वैप से धनराशि प्राप्त नहीं कर सकता। इस पते पर भेजने के लिए आपको एक ${network} वॉलेट की आवश्यकता है।",
1156 + "swap_unavailable_switch_to_network": "यह पता प्रकार स्वैप से धनराशि प्राप्त नहीं कर सकता। जारी रखने के लिए अपने ${network} वॉलेट पर स्विच करें।",
1157 "sweeping_wallet": "वॉलेट स्वीप करना",
1158 "sweeping_wallet_alert": "इसमें ज़्यादा समय नहीं लगना चाहिए। इस स्क्रीन को न छोड़ें, वरना स्वीप किए गए फंड खो सकते हैं।",
1159 "swipe_to_bridge": "ब्रिज करने के लिए स्वाइप करें",
res/values/strings_hr.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Dostupno",
94 "available_balance": "Raspoloživo stanje",
95 "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.",
96 + "available_balance_short": "Prosjek: ${amount}",
97 "avg_savings": "Prosj. ušteda",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Molimo pričekajte da dApp završi s obradom.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "ID zamjene",
1151 "swap_provider_initial_desc": "Odaberite vrstu pružatelja zamjene koju želite koristiti pri zamjeni",
1152 "swap_providers": "Pružatelji usluga zamjene",
1153 + "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.",
1154 + "swap_unavailable_switch_to_network": "Ova vrsta adrese ne može primiti sredstva od zamjene. Za nastavak prebacite se na svoj ${network} novčanik.",
1155 "sweeping_wallet": "Prebacivanje sredstava u novčanik",
1156 "sweeping_wallet_alert": "Ovo ne bi trebalo dugo trajati. NE NAPUŠTAJTE OVAJ ZASLON ILI PREBAČENA SREDSTVA MOGU BITI IZGUBLJENA.",
1157 "swipe_to_bridge": "Prijeđite prstom za premošćivanje",
res/values/strings_hy.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Հասանելի",
94 "available_balance": "Հասանելի մնացորդ",
95 "available_balance_description": "«Հասանելի մնացորդը» կամ «Հաստատված մնացորդը» միջոցներ են, որոնք կարելի է անմիջապես ծախսել։ Եթե միջոցները երևում են ստորին մնացորդում, բայց ոչ վերևի մնացորդում, ապա պետք է մի քանի րոպե սպասեք, մինչև մուտքագրվող միջոցները ստանան ավելի շատ ցանցային հաստատումներ։ Ավելի շատ հաստատումներ ստանալուց հետո դրանք հնարավոր կլինի ծախսել։",
96 + "available_balance_short": "Ավլ՝ ${amount}",
97 "avg_savings": "Միջին խնայողություն",
98 "avl": "Ավլ",
99 "awaitDAppProcessing": "Խնդրում ենք սպասել, մինչև dApp-ը ավարտի մշակումը։",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "Փոխանակման ID",
1151 "swap_provider_initial_desc": "Ընտրեք փոխանակման մատակարարի տեսակը, որը նախընտրում եք օգտագործել փոխանակում կատարելիս",
1152 "swap_providers": "Փոխանակման մատակարարներ",
1153 + "swap_unavailable_needs_network_wallet": "Այս հասցեի տեսակը չի կարող գումարներ ստանալ փոխանակումից: Այս հասցեին ուղարկելու համար ձեզ հարկավոր է ${network} դրամապանակ:",
1154 + "swap_unavailable_switch_to_network": "Այս հասցեի տեսակը չի կարող գումարներ ստանալ փոխանակումից: Շարունակելու համար անցեք ձեր ${network} դրամապանակին:",
1155 "sweeping_wallet": "Դրամապանակի սվիպինգ",
1156 "sweeping_wallet_alert": "Սա երկար չի տևի։ ՄԻ ԼՔԵՔ ԱՅՍ ԷԿՐԱՆԸ, ՀԱԿԱՌԱԿ ԴԵՊՔՈՒՄ ՄԱՔՐՎՈՂ ՄԻՋՈՑՆԵՐԸ ԿԱՐՈՂ ԵՆ ԿՈՐՉԵԼ։",
1157 "swipe_to_bridge": "Սահեցրեք՝ կամրջելու համար",
res/values/strings_id.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Tersedia",
94 "available_balance": "Saldo Tersedia",
95 "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.",
96 + "available_balance_short": "Rata-rata: ${amount}",
97 "avg_savings": "Rata-rata Penghematan",
98 "avl": "Rata-rata",
99 "awaitDAppProcessing": "Mohon tunggu hingga dApp selesai memproses.",
@@ -1152,6 +1153,8 @@
1153 "swap_id": "ID Swap",
1154 "swap_provider_initial_desc": "Pilih jenis penyedia swap yang Anda inginkan untuk digunakan saat melakukan swap",
1155 "swap_providers": "Penyedia swap",
1156 + "swap_unavailable_needs_network_wallet": "Jenis alamat ini tidak dapat menerima dana dari swap. Anda memerlukan dompet ${network} untuk mengirim ke alamat ini.",
1157 + "swap_unavailable_switch_to_network": "Jenis alamat ini tidak dapat menerima dana dari swap. Beralih ke dompet ${network} Anda untuk melanjutkan.",
1158 "sweeping_wallet": "Dompet sweep",
1159 "sweeping_wallet_alert": "Ini seharusnya tidak memakan waktu lama. JANGAN TINGGALKAN LAYAR INI ATAU DANA YANG DISAPU DAPAT HILANG.",
1160 "swipe_to_bridge": "Geser untuk menjembatani",
res/values/strings_it.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Disponibile",
94 "available_balance": "Saldo disponibile",
95 "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.",
96 + "available_balance_short": "Avl: ${amount}",
97 "avg_savings": "Risparmio medio",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Attendi che la dApp completi l'elaborazione.",
@@ -1150,6 +1151,8 @@
1151 "swap_id": "ID swap",
1152 "swap_provider_initial_desc": "Seleziona il tipo di fornitore di swap che preferisci utilizzare quando effettui uno swap in",
1153 "swap_providers": "Provider di swap",
1154 + "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.",
1155 + "swap_unavailable_switch_to_network": "Questo tipo di indirizzo non può ricevere fondi da uno swap. Passa al tuo portafoglio ${network} per continuare.",
1156 "sweeping_wallet": "Svuotamento del portafoglio",
1157 "sweeping_wallet_alert": "Questo non dovrebbe richiedere molto tempo. NON LASCIARE QUESTA SCHERMATA O I FONDI SPOSTATI POTREBBERO ANDARE PERSI.",
1158 "swipe_to_bridge": "Scorri per il bridge",
res/values/strings_ja.arb
+3
@@ -93,6 +93,7 @@
93 "available": "利用可能",
94 "available_balance": "利用可能残高",
95 "available_balance_description": "「利用可能な残高」または「承認済み残高」は、すぐに使用できる資金です。下の残高には表示されているのに上の残高に表示されない場合は、受信した資金がネットワークでさらに承認(確認)されるまで数分待つ必要があります。承認数が増えると、使用できるようになります。",
96 + "available_balance_short": "平均: ${amount}",
97 "avg_savings": "平均節約額",
98 "avl": "アヴル",
99 "awaitDAppProcessing": "dAppの処理が完了するまでお待ちください。",
@@ -1150,6 +1151,8 @@
1151 "swap_id": "スワップID",
1152 "swap_provider_initial_desc": "スワップイン時に使用するスワッププロバイダーの種類を選択してください",
1153 "swap_providers": "スワッププロバイダー",
1154 + "swap_unavailable_needs_network_wallet": "このアドレスタイプはスワップから資金を受け取ることができません。このアドレスに送信するには ${network} ウォレットが必要です。",
1155 + "swap_unavailable_switch_to_network": "このアドレスタイプはスワップから資金を受け取ることができません。 ${network} ウォレットに切り替えて続行してください。",
1156 "sweeping_wallet": "スイーピングウォレット",
1157 "sweeping_wallet_alert": "すぐに完了します。この画面から離れないでください。離れると、スイープした資金が失われる可能性があります。",
1158 "swipe_to_bridge": "スワイプしてブリッジ",
res/values/strings_ko.arb
+3
@@ -93,6 +93,7 @@
93 "available": "사용 가능",
94 "available_balance": "사용 가능 잔액",
95 "available_balance_description": "“사용 가능 잔액” 또는 “확정 잔액”은 즉시 사용할 수 있는 자금입니다. 자금이 아래 잔액에는 표시되지만 위 잔액에는 표시되지 않는 경우, 입금된 자금이 네트워크에서 더 많은 컨펌(확인)을 받을 때까지 몇 분 정도 기다려야 합니다. 더 많은 컨펌을 받은 후에는 사용할 수 있습니다.",
96 + "available_balance_short": "평균: ${amount}",
97 "avg_savings": "평균 절감액",
98 "avl": "Avl",
99 "awaitDAppProcessing": "dApp의 처리가 완료될 때까지 잠시 기다려 주세요.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "스왑 ID",
1151 "swap_provider_initial_desc": "스왑 인할 때 사용하려는 스왑 제공자 유형을 선택하세요.",
1152 "swap_providers": "스왑 제공업체",
1153 + "swap_unavailable_needs_network_wallet": "이 주소 유형은 스왑에서 자금을 받을 수 없습니다. 이 주소로 보내려면 ${network} 지갑이 필요합니다.",
1154 + "swap_unavailable_switch_to_network": "이 주소 유형은 스왑에서 자금을 받을 수 없습니다. 계속하려면 ${network} 지갑으로 전환하세요.",
1155 "sweeping_wallet": "지갑 스윕",
1156 "sweeping_wallet_alert": "오래 걸리지 않습니다. 이 화면을 떠나지 마세요. 그렇지 않으면 스윕된 자금이 손실될 수 있습니다.",
1157 "swipe_to_bridge": "스와이프하여 브리지",
res/values/strings_my.arb
+3
@@ -93,6 +93,7 @@
93 "available": "ရရှိနိုင်သည်",
94 "available_balance": "ရရှိနိုင်သော လက်ကျန်",
95 "available_balance_description": "“Available Balance” သို့မဟုတ် “Confirmed Balance” သည် ချက်ချင်းသုံးစွဲနိုင်သော လက်ကျန်ငွေဖြစ်သည်။ လက်ကျန်အောက်ပိုင်းတွင်သာ ဝင်လာသော်လည်း အပေါ်ပိုင်းလက်ကျန်တွင် မပေါ်သေးပါက ကွန်ယက်အတည်ပြုချက် (network confirmations) ပိုမိုရရှိရန် မိနစ်အနည်းငယ် စောင့်ရပါမည်။ အတည်ပြုချက်များ ပိုမိုရရှိပြီးနောက် ထိုငွေများကို သုံးစွဲနိုင်ပါလိမ့်မည်။",
96 + "available_balance_short": "Avl- ${amount}",
97 "avg_savings": "ပျမ်းမျှ ချွေတာမှု",
98 "avl": "Avl",
99 "awaitDAppProcessing": "ကျေးဇူးပြု၍ dApp ၏ စီမံဆောင်ရွက်မှု ပြီးဆုံးသည်အထိ စောင့်ပါ။",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "Swap ID",
1151 "swap_provider_initial_desc": "Swap လုပ်ရာတွင် သင်အသုံးပြုလိုသည့် swap provider အမျိုးအစားကို ရွေးချယ်ပါ။",
1152 "swap_providers": "Swap ဝန်ဆောင်မှုပေးသူများ",
1153 + "swap_unavailable_needs_network_wallet": "ဤလိပ်စာအမျိုးအစားသည် လဲလှယ်မှုတစ်ခုမှ ရံပုံငွေများကို လက်ခံမရနိုင်ပါ။ ဤလိပ်စာသို့ ပေးပို့ရန် ${network} ပိုက်ဆံအိတ်တစ်ခု လိုအပ်ပါသည်။",
1154 + "swap_unavailable_switch_to_network": "ဤလိပ်စာအမျိုးအစားသည် လဲလှယ်မှုတစ်ခုမှ ရံပုံငွေများကို လက်ခံမရနိုင်ပါ။ ရှေ့ဆက်ရန် သင်၏ ${network} ပိုက်ဆံအိတ်သို့ ပြောင်းပါ။",
1155 "sweeping_wallet": "ပိုက်ဆံအိတ် ရှင်းလင်းခြင်း",
1156 "sweeping_wallet_alert": "ဒါက ကြာကြာမခံသင့်ပါဘူး။ ဒီစခရင်ကနေ မထွက်ပါနဲ့၊ မဟုတ်ရင် sweep လုပ်ထားတဲ့ ရန်ပုံငွေတွေ ပျောက်ဆုံးနိုင်ပါတယ်။",
1157 "swipe_to_bridge": "Bridge လုပ်ရန် ပွတ်ဆွဲပါ",
res/values/strings_nl.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Beschikbaar",
94 "available_balance": "Beschikbaar saldo",
95 "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.",
96 + "available_balance_short": "Gem.: ${amount}",
97 "avg_savings": "Gem. besparingen",
98 "avl": "Gem",
99 "awaitDAppProcessing": "Wacht tot de dApp klaar is met verwerken.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "Swap-ID",
1151 "swap_provider_initial_desc": "Selecteer het type swapprovider dat je wilt gebruiken bij het swappen",
1152 "swap_providers": "Swapproviders",
1153 + "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.",
1154 + "swap_unavailable_switch_to_network": "Dit adrestype kan geen geld ontvangen van een swap. Schakel over naar uw ${network}-portemonnee om door te gaan.",
1155 "sweeping_wallet": "Wallet wordt geveegd",
1156 "sweeping_wallet_alert": "Dit duurt niet lang. VERLAAT DIT SCHERM NIET, ANDERS KUNNEN DE FONDSEN VERLOREN GAAN.",
1157 "swipe_to_bridge": "Veeg om te bridgen",
res/values/strings_pl.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Dostępne",
94 "available_balance": "Dostępne saldo",
95 "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ć.",
96 + "available_balance_short": "Średnia: ${amount}",
97 "avg_savings": "Śr. oszczędności",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Proszę poczekać, aż dApp zakończy przetwarzanie.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "ID wymiany",
1151 "swap_provider_initial_desc": "Wybierz typ dostawcy swapów, którego wolisz używać podczas swapowania w",
1152 "swap_providers": "Dostawcy wymiany",
1153 + "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}.",
1154 + "swap_unavailable_switch_to_network": "Ten typ adresu nie może otrzymywać środków z wymiany. Aby kontynuować, przejdź do portfela ${network}.",
1155 "sweeping_wallet": "Opróżnianie portfela",
1156 "sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE OPUSZCZAJ TEGO EKRANU, W PRZECIWNYM RAZIE ZAMIATANE ŚRODKI MOGĄ ZOSTAĆ UTRACONE.",
1157 "swipe_to_bridge": "Przesuń, aby zmostkować",
res/values/strings_pt.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Disponível",
94 "available_balance": "Saldo disponível",
95 "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.",
96 + "available_balance_short": "Avl: ${amount}",
97 "avg_savings": "Poupança média",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Por favor, aguarde o dApp concluir o processamento.",
@@ -1151,6 +1152,8 @@
1152 "swap_id": "ID da troca",
1153 "swap_provider_initial_desc": "Selecione o tipo de provedor de swap que você prefere usar ao fazer swaps em",
1154 "swap_providers": "Provedores de swap",
1155 + "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.",
1156 + "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.",
1157 "sweeping_wallet": "Carteira de varredura",
1158 "sweeping_wallet_alert": "Isso não deve demorar muito. NÃO SAIA DESTA TELA OU OS FUNDOS VARRIDOS PODEM SER PERDIDOS.",
1159 "swipe_to_bridge": "Deslize para fazer bridge",
res/values/strings_pt_BR.arb
+3
@@ -88,6 +88,7 @@
88 "automatic": "Automático",
89 "available_balance": "Saldo disponível",
90 "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.",
91 + "available_balance_short": "Avl: ${amount}",
92 "avg_savings": "Economia média",
93 "awaitDAppProcessing": "Aguarde até que o dApp termine de processar.",
94 "awaiting_payment_confirmation": "Aguardando confirmação do pagamento",
@@ -1099,6 +1100,8 @@
1100 "swap_id": "ID do Swap",
1101 "swap_provider_initial_desc": "Selecione o tipo de provedor de swap que você prefere usar ao realizar swaps",
1102 "swap_providers": "Provedores de swap",
1103 + "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.",
1104 + "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.",
1105 "sweeping_wallet": "Carteira em varredura",
1106 "sweeping_wallet_alert": "Isso não deve demorar muito. NÃO SAIA DESTA TELA OU OS FUNDOS VARRIDOS PODERÃO SER PERDIDOS.",
1107 "swipe_to_send": "Deslize para enviar",
res/values/strings_ru.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Доступно",
94 "available_balance": "Доступный баланс",
95 "available_balance_description": "«Доступный баланс» или «Подтверждённый баланс» — это средства, которые можно потратить сразу. Если средства отображаются в нижнем балансе, но не в верхнем, значит, нужно подождать несколько минут, пока входящие средства получат больше подтверждений сети. После получения дополнительных подтверждений их можно будет потратить.",
96 + "available_balance_short": "Avl: ${amount}",
97 "avg_savings": "Средняя экономия",
98 "avl": "Авл",
99 "awaitDAppProcessing": "Пожалуйста, подождите, пока dApp завершит обработку.",
@@ -1150,6 +1151,8 @@
1151 "swap_id": "ID обмена",
1152 "swap_provider_initial_desc": "Выберите тип провайдера свопа, который вы предпочитаете использовать при свопе в",
1153 "swap_providers": "Провайдеры обмена",
1154 + "swap_unavailable_needs_network_wallet": "Этот тип адреса не может получать средства от свопа. Для отправки на этот адрес вам понадобится кошелек ${network}.",
1155 + "swap_unavailable_switch_to_network": "Этот тип адреса не может получать средства от свопа. Чтобы продолжить, переключитесь на свой кошелек ${network}.",
1156 "sweeping_wallet": "Сканирование кошелька",
1157 "sweeping_wallet_alert": "Это не должно занять много времени. НЕ ПОКИДАЙТЕ ЭТОТ ЭКРАН, ИНАЧЕ ПЕРЕНЕСЁННЫЕ СРЕДСТВА МОГУТ БЫТЬ ПОТЕРЯНЫ.",
1158 "swipe_to_bridge": "Проведите, чтобы выполнить перенос через мост",
res/values/strings_th.arb
+3
@@ -93,6 +93,7 @@
93 "available": "พร้อมใช้งาน",
94 "available_balance": "ยอดคงเหลือที่ใช้ได้",
95 "available_balance_description": "“ยอดคงเหลือที่ใช้ได้” หรือ “ยอดคงเหลือที่ยืนยันแล้ว” คือเงินที่สามารถใช้จ่ายได้ทันที หากเงินปรากฏในยอดคงเหลือด้านล่างแต่ไม่ปรากฏในยอดคงเหลือด้านบน คุณต้องรอสักครู่เพื่อให้เงินที่เข้ามาได้รับการยืนยันจากเครือข่ายเพิ่มขึ้น เมื่อได้รับการยืนยันมากขึ้นแล้ว เงินนั้นจะสามารถใช้จ่ายได้",
96 + "available_balance_short": "จำนวน: ${amount}",
97 "avg_savings": "การประหยัดเฉลี่ย",
98 "avl": "เฉลี่ย",
99 "awaitDAppProcessing": "โปรดรอให้ dApp ประมวลผลให้เสร็จสิ้น",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "รหัส Swap",
1151 "swap_provider_initial_desc": "เลือกประเภทผู้ให้บริการ Swap ที่คุณต้องการใช้เมื่อทำการ Swap",
1152 "swap_providers": "ผู้ให้บริการสวอป",
1153 + "swap_unavailable_needs_network_wallet": "ที่อยู่ประเภทนี้ไม่สามารถรับเงินจากการแลกเปลี่ยนได้ คุณต้องมีกระเป๋าเงิน ${network} เพื่อส่งไปยังที่อยู่นี้",
1154 + "swap_unavailable_switch_to_network": "ที่อยู่ประเภทนี้ไม่สามารถรับเงินจากการแลกเปลี่ยนได้ เปลี่ยนไปใช้กระเป๋าเงิน ${network} ของคุณเพื่อดำเนินการต่อ",
1155 "sweeping_wallet": "กระเป๋าสตางค์สำหรับกวาดเหรียญ",
1156 "sweeping_wallet_alert": "การดำเนินการนี้ใช้เวลาไม่นาน ห้ามออกจากหน้าจอนี้ มิฉะนั้นเงินที่กวาดอาจสูญหาย",
1157 "swipe_to_bridge": "ปัดเพื่อบริดจ์",
res/values/strings_tl.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Available",
94 "available_balance": "Magagamit na Balanse",
95 "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.",
96 + "available_balance_short": "Avl: ${amount}",
97 "avg_savings": "Avg. Matitipid",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Mangyaring maghintay hanggang matapos ang pagproseso ng dApp.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "Swap ID",
1151 "swap_provider_initial_desc": "Piliin ang uri ng swap provider na mas gusto mong gamitin kapag nagsa-swap",
1152 "swap_providers": "Mga provider ng swap",
1153 + "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.",
1154 + "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.",
1155 "sweeping_wallet": "Sweeping wallet",
1156 "sweeping_wallet_alert": "Hindi ito dapat magtagal. HUWAG UMALIS SA SCREEN NA ITO O MAARING MAWALA ANG MGA NA-SWEEP NA PONDO.",
1157 "swipe_to_bridge": "Mag-swipe para mag-bridge",
res/values/strings_tr.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Kullanılabilir",
94 "available_balance": "Kullanılabilir Bakiye",
95 "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.",
96 + "available_balance_short": "Ortalama: ${amount}",
97 "avg_savings": "Ort. Tasarruf",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Lütfen dApp'in işlemi tamamlamasını bekleyin.",
@@ -1149,6 +1150,8 @@
1150 "swap_id": "Swap Kimliği",
1151 "swap_provider_initial_desc": "Swap yaparken kullanmayı tercih ettiğiniz swap sağlayıcı türünü seçin",
1152 "swap_providers": "Takas sağlayıcıları",
1153 + "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.",
1154 + "swap_unavailable_switch_to_network": "Bu adres türü takastan para alamaz. Devam etmek için ${network} cüzdanınıza geçin.",
1155 "sweeping_wallet": "Cüzdan süpürme",
1156 "sweeping_wallet_alert": "Bu uzun sürmemeli. BU EKRANDAN AYRILMAYIN, AKSİ TAKDİRDE SÜPÜRÜLEN FONLAR KAYBOLABİLİR.",
1157 "swipe_to_bridge": "Köprülemek için kaydırın",
res/values/strings_uk.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Доступно",
94 "available_balance": "Доступний баланс",
95 "available_balance_description": "«Доступний баланс» або «Підтверджений баланс» — це кошти, які можна витратити негайно. Якщо кошти відображаються в нижньому балансі, але не у верхньому, потрібно зачекати кілька хвилин, доки вхідні кошти отримають більше підтверджень мережі. Після отримання додаткових підтверджень їх можна буде витратити.",
96 + "available_balance_short": "Середня сума: ${amount}",
97 "avg_savings": "Сер. заощадження",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Будь ласка, зачекайте, доки dApp завершить обробку.",
@@ -1150,6 +1151,8 @@
1151 "swap_id": "ID обміну",
1152 "swap_provider_initial_desc": "Виберіть тип провайдера свопу, який ви бажаєте використовувати під час обміну",
1153 "swap_providers": "Провайдери обміну",
1154 + "swap_unavailable_needs_network_wallet": "Цей тип адреси не може отримати кошти від обміну. Щоб надіслати на цю адресу, вам потрібен гаманець ${network}.",
1155 + "swap_unavailable_switch_to_network": "Цей тип адреси не може отримати кошти від обміну. Щоб продовжити, перейдіть на гаманець ${network}.",
1156 "sweeping_wallet": "Змітання гаманця",
1157 "sweeping_wallet_alert": "Це не повинно зайняти багато часу. НЕ ЗАЛИШАЙТЕ ЦЕЙ ЕКРАН, ІНАКШЕ ЗМЕТЕНІ КОШТИ МОЖУТЬ БУТИ ВТРАЧЕНІ.",
1158 "swipe_to_bridge": "Проведіть, щоб перенести між мережами",
res/values/strings_ur.arb
+3
@@ -93,6 +93,7 @@
93 "available": "دستیاب",
94 "available_balance": "دستیاب بیلنس",
95 "available_balance_description": "”دستیاب بیلنس” یا ”تصدیق شدہ بیلنس” وہ فنڈز ہیں جنہیں فوراً خرچ کیا جا سکتا ہے۔ اگر فنڈز نیچے والے بیلنس میں نظر آئیں لیکن اوپر والے بیلنس میں نہیں، تو آپ کو آنے والے فنڈز کے مزید نیٹ ورک کنفرمیشنز حاصل ہونے کے لیے چند منٹ انتظار کرنا ہوگا۔ جب انہیں مزید کنفرمیشنز مل جائیں گی تو وہ خرچ کے قابل ہو جائیں گے۔",
96 + "available_balance_short": "Avl: ${amount}",
97 "avg_savings": "اوسط بچت",
98 "avl": "Avl",
99 "awaitDAppProcessing": "براہِ کرم dApp کی پروسیسنگ مکمل ہونے تک انتظار کریں۔",
@@ -1151,6 +1152,8 @@
1152 "swap_id": "سواپ آئی ڈی",
1153 "swap_provider_initial_desc": "سواپ کرتے وقت استعمال کے لیے اپنی پسند کے سواپ فراہم کنندہ کی قسم منتخب کریں",
1154 "swap_providers": "سویپ فراہم کنندگان",
1155 + "swap_unavailable_needs_network_wallet": "ایڈریس کی یہ قسم سویپ سے فنڈز وصول نہیں کر سکتی۔ اس پتے پر بھیجنے کے لیے آپ کو ایک ${network} والیٹ کی ضرورت ہے۔",
1156 + "swap_unavailable_switch_to_network": "ایڈریس کی یہ قسم سویپ سے فنڈز وصول نہیں کر سکتی۔ جاری رکھنے کے لیے اپنے ${network} والیٹ پر جائیں۔",
1157 "sweeping_wallet": "والٹ سویپ کرنا",
1158 "sweeping_wallet_alert": "اس میں زیادہ وقت نہیں لگنا چاہیے۔ اس اسکرین کو مت چھوڑیں، ورنہ سویپ کیے گئے فنڈز ضائع ہو سکتے ہیں۔",
1159 "swipe_to_bridge": "برج کرنے کے لیے سوائپ کریں",
res/values/strings_vi.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Khả dụng",
94 "available_balance": "Số dư khả dụng",
95 "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.",
96 + "available_balance_short": "Trung bình: ${amount}",
97 "avg_savings": "Tiết kiệm TB",
98 "avl": "trung bình",
99 "awaitDAppProcessing": "Vui lòng đợi dApp xử lý xong.",
@@ -1150,6 +1151,8 @@
1151 "swap_id": "ID Swap",
1152 "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",
1153 "swap_providers": "Nhà cung cấp hoán đổi",
1154 + "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.",
1155 + "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.",
1156 "sweeping_wallet": "Quét ví",
1157 "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.",
1158 "swipe_to_bridge": "Vuốt để bắc cầu",
res/values/strings_yo.arb
+3
@@ -93,6 +93,7 @@
93 "available": "Ó wà",
94 "available_balance": "Ìwọ̀ntúnwọ̀nsì tó wà",
95 "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.",
96 + "available_balance_short": "Avl: ${amount}",
97 "avg_savings": "Ìpamọ́ àpapọ̀",
98 "avl": "Avl",
99 "awaitDAppProcessing": "Jọ̀wọ́, dúró kí dApp parí ìṣiṣẹ́.",
@@ -1150,6 +1151,8 @@
1151 "swap_id": "ID Swap",
1152 "swap_provider_initial_desc": "Yan iru olupese swap ti o fẹ́ lò nígbà tí o bá ń swap sínú",
1153 "swap_providers": "Awọn olupese swap",
1154 + "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.",
1155 + "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.",
1156 "sweeping_wallet": "Wallet ìkó",
1157 "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Ù.",
1158 "swipe_to_bridge": "Fà láti so ìjápọ̀",
res/values/strings_zh.arb
+3
@@ -93,6 +93,7 @@
93 "available": "可用",
94 "available_balance": "可用余额",
95 "available_balance_description": "“可用余额”或“确认余额”是指可立即花费的资金。如果资金显示在下方余额中但未显示在上方余额中,则需要等待几分钟,让转入资金获得更多网络确认。获得更多确认后,这些资金即可花费。",
96 + "available_balance_short": "平均价值:${amount}",
97 "avg_savings": "平均节省",
98 "avl": "平均",
99 "awaitDAppProcessing": "请稍候,等待 dApp 完成处理。",
@@ -1150,6 +1151,8 @@
1151 "swap_id": "兑换 ID",
1152 "swap_provider_initial_desc": "选择您在换入时偏好使用的兑换服务提供商类型",
1153 "swap_providers": "兑换服务提供商",
1154 + "swap_unavailable_needs_network_wallet": "此地址类型无法从交换中接收资金。您需要一个 ${network} 钱包才能发送到此地址。",
1155 + "swap_unavailable_switch_to_network": "此地址类型无法从交换中接收资金。切换到您的 ${network} 钱包以继续。",
1156 "sweeping_wallet": "正在归集钱包",
1157 "sweeping_wallet_alert": "这应该不会花很长时间。请勿离开此屏幕,否则被归集的资金可能会丢失。",
1158 "swipe_to_bridge": "滑动以跨链桥接",
test/core/anypay/anypay_parser_test.dart new
+103
@@ -0,0 +1,103 @@
1 +import "package:cake_wallet/core/anypay/anypay_models.dart";
2 +import "package:cake_wallet/core/anypay/anypay_parser.dart";
3 +import "package:cake_wallet/utils/payment_request.dart";
4 +import "package:flutter_test/flutter_test.dart";
5 +
6 +void main() {
7 + const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
8 + const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
9 +
10 + group("chain binding", () {
11 + test("explicit ethereum chain binds explicitly", () {
12 + final request = AnyPayParser.fromRaw(
13 + "ethereum:$recipient@8453?value=1e18",
14 + );
15 +
16 + expect(request.chainBinding, isA<ExplicitEvmChain>());
17 + expect((request.chainBinding as ExplicitEvmChain).chainId, 8453);
18 + });
19 +
20 + test("ethereum without a chain id stays chainless", () {
21 + final request = AnyPayParser.fromRaw(
22 + "ethereum:$usdtEthContract/transfer?address=$recipient&uint256=1000000",
23 + );
24 +
25 + expect(request.chainBinding, isA<ChainlessEvm>());
26 + expect(request.hasContract, true);
27 + });
28 +
29 + test("non evm schemes bind no evm chain", () {
30 + final request = AnyPayParser.fromRaw(
31 + "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
32 + );
33 +
34 + expect(request.chainBinding, isA<NoEvmBinding>());
35 + });
36 +
37 + test("raw evm addresses bind no evm chain", () {
38 + final request = AnyPayParser.fromRaw(recipient);
39 +
40 + expect(request.chainBinding, isA<NoEvmBinding>());
41 + expect(request.detection.isValid, true);
42 + });
43 +
44 + test("an evm chain scheme binds its chain explicitly", () {
45 + final request = AnyPayParser.fromRaw("polygon:$recipient");
46 +
47 + expect((request.chainBinding as ExplicitEvmChain).chainId, 137);
48 + });
49 + });
50 +
51 + group("payment request reconstruction", () {
52 + test("an explicit chain request keeps its chain and raw amount", () {
53 + final original = PaymentRequest.fromUri(
54 + Uri.parse("ethereum:$usdtEthContract@8453/transfer?address=$recipient&uint256=3000000"),
55 + );
56 +
57 + final request = AnyPayParser.fromPaymentRequest(original);
58 +
59 + expect(request.rawInput.contains("@8453"), true);
60 + expect(request.paymentRequest.rawTokenAmount, "3000000");
61 + expect((request.chainBinding as ExplicitEvmChain).chainId, 8453);
62 + });
63 +
64 + test("a chainless request rebuilds with a placeholder but keeps the chainless binding", () {
65 + final original = PaymentRequest.fromUri(
66 + Uri.parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=1000000"),
67 + );
68 +
69 + final request = AnyPayParser.fromPaymentRequest(original);
70 +
71 + expect(request.rawInput.contains("@1/"), true);
72 + expect(request.chainBinding, isA<ChainlessEvm>());
73 + expect(request.detection.isValid, true);
74 + });
75 +
76 + test("a tron token request keeps its contract on the payment request", () {
77 + final original = PaymentRequest.fromUri(
78 + Uri.parse("tron:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t?token=TTokenContract111111111111111111"),
79 + );
80 +
81 + final request = AnyPayParser.fromPaymentRequest(original);
82 +
83 + expect(request.contractAddress, "TTokenContract111111111111111111");
84 + expect(request.chainBinding, isA<NoEvmBinding>());
85 + });
86 +
87 + test("a non evm request rebuilds scheme address and amount", () {
88 + final original = PaymentRequest("someaddress", "1.25", "", "monero", null);
89 +
90 + final request = AnyPayParser.fromPaymentRequest(original);
91 +
92 + expect(request.rawInput, "monero:someaddress?amount=1.25");
93 + });
94 +
95 + test("a lightning request passes the invoice through untouched", () {
96 + final original = PaymentRequest("lnbc1invoice", "", "", "lightning", null);
97 +
98 + final request = AnyPayParser.fromPaymentRequest(original);
99 +
100 + expect(request.rawInput, "lnbc1invoice");
101 + });
102 + });
103 +}
test/core/anypay/anypay_resolver_test.dart new
+201
@@ -0,0 +1,201 @@
1 +import "package:cake_wallet/core/anypay/anypay_models.dart";
2 +import "package:cake_wallet/core/anypay/anypay_parser.dart";
3 +import "package:cake_wallet/core/anypay/anypay_resolver.dart";
4 +import "package:cw_core/crypto_currency.dart";
5 +import "package:cw_core/erc20_token.dart";
6 +import "package:cw_core/wallet_type.dart";
7 +import "package:flutter_test/flutter_test.dart";
8 +
9 +class _FakeTokenLookup implements AnyPayTokenLookup {
10 + _FakeTokenLookup({this.onFindToken, this.onFindChainId});
11 +
12 + final Future<CryptoCurrency?> Function({required WalletType walletType, required String address})?
13 + onFindToken;
14 + final Future<int?> Function(String contractAddress, {int? excludingChainId})? onFindChainId;
15 +
16 + @override
17 + Future<CryptoCurrency?> findTokenByAddress({
18 + required WalletType walletType,
19 + required String address,
20 + }) {
21 + if (onFindToken == null) {
22 + fail("no token lookup expected");
23 + }
24 + return onFindToken!(walletType: walletType, address: address);
25 + }
26 +
27 + @override
28 + Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId}) {
29 + if (onFindChainId == null) {
30 + fail("no chain sweep expected");
31 + }
32 + return onFindChainId!(contractAddress, excludingChainId: excludingChainId);
33 + }
34 +}
35 +
36 +void main() {
37 + const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
38 + const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
39 + const solAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
40 + const usdcSolMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
41 +
42 + final usdt = Erc20Token(
43 + name: "Tether",
44 + symbol: "USDT",
45 + contractAddress: usdtEthContract,
46 + decimal: 6,
47 + );
48 +
49 + WalletSnapshot snapshot({
50 + WalletType type = WalletType.ethereum,
51 + int? chainId,
52 + bool hasEvmProxy = true,
53 + }) =>
54 + WalletSnapshot(
55 + type: type,
56 + currentWalletIsEvm:
57 + type == WalletType.ethereum || type == WalletType.polygon || type == WalletType.base,
58 + currentChainId: chainId,
59 + wallets: const [],
60 + // The service builds an empty chain map when the evm proxy is absent.
61 + supportedEvmChains: hasEvmProxy
62 + ? const {
63 + 1: WalletType.ethereum,
64 + 137: WalletType.polygon,
65 + 8453: WalletType.base,
66 + }
67 + : const {},
68 + hasEvmProxy: hasEvmProxy,
69 + hasSolanaProxy: true,
70 + hasTronProxy: true,
71 + );
72 +
73 + AnyPayRequest parse(String input) => AnyPayParser.fromRaw(input);
74 +
75 + test("a request without a contract needs no resolution", () async {
76 + final resolver = AnyPayResolver(tokenLookup: _FakeTokenLookup());
77 +
78 + final resolution = await resolver.resolve(
79 + parse("ethereum:$recipient@1?value=1e18"),
80 + snapshot(chainId: 1),
81 + );
82 +
83 + expect(resolution, isA<NoTokenRequested>());
84 + });
85 +
86 + test("a token on the explicit target chain resolves with its amount", () async {
87 + final lookedUp = <WalletType>[];
88 + final resolver = AnyPayResolver(
89 + tokenLookup: _FakeTokenLookup(
90 + onFindToken: ({required walletType, required address}) async {
91 + lookedUp.add(walletType);
92 + return walletType == WalletType.ethereum ? usdt : null;
93 + },
94 + ),
95 + );
96 +
97 + final resolution = await resolver.resolve(
98 + parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
99 + snapshot(type: WalletType.base, chainId: 8453),
100 + );
101 +
102 + final resolved = resolution as TokenResolved;
103 + expect(lookedUp, [WalletType.ethereum]);
104 + expect(resolved.chainId, 1);
105 + expect(resolved.token, usdt);
106 + expect(resolved.amountOverride, "1");
107 + });
108 +
109 + test("an unknown token on an explicit chain is not swept to other chains", () async {
110 + bool sweepCalled = false;
111 + final resolver = AnyPayResolver(
112 + tokenLookup: _FakeTokenLookup(
113 + onFindToken: ({required walletType, required address}) async => null,
114 + onFindChainId: (contract, {excludingChainId}) async {
115 + sweepCalled = true;
116 + return 137;
117 + },
118 + ),
119 + );
120 +
121 + final resolution = await resolver.resolve(
122 + parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
123 + snapshot(chainId: 1),
124 + );
125 +
126 + expect(resolution, isA<TokenUnknown>());
127 + expect(sweepCalled, false);
128 + });
129 +
130 + test("a chainless token unknown on the current chain reroutes through the sweep", () async {
131 + final resolver = AnyPayResolver(
132 + tokenLookup: _FakeTokenLookup(
133 + onFindToken: ({required walletType, required address}) async =>
134 + walletType == WalletType.ethereum ? usdt : null,
135 + onFindChainId: (contract, {excludingChainId}) async {
136 + expect(excludingChainId, 8453);
137 + return 1;
138 + },
139 + ),
140 + );
141 +
142 + final resolution = await resolver.resolve(
143 + parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
144 + snapshot(type: WalletType.base, chainId: 8453),
145 + );
146 +
147 + final resolved = resolution as TokenResolved;
148 + expect(resolved.chainId, 1);
149 + expect(resolved.amountOverride, "2");
150 + });
151 +
152 + test("a chainless token unknown everywhere stays unknown", () async {
153 + final resolver = AnyPayResolver(
154 + tokenLookup: _FakeTokenLookup(
155 + onFindToken: ({required walletType, required address}) async => null,
156 + onFindChainId: (contract, {excludingChainId}) async => null,
157 + ),
158 + );
159 +
160 + final resolution = await resolver.resolve(
161 + parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
162 + snapshot(chainId: 1),
163 + );
164 +
165 + expect((resolution as TokenUnknown).contract, usdtEthContract);
166 + });
167 +
168 + test("a solana token resolves against the detected wallet type", () async {
169 + const usdcSol = CryptoCurrency.usdtSol;
170 + final resolver = AnyPayResolver(
171 + tokenLookup: _FakeTokenLookup(
172 + onFindToken: ({required walletType, required address}) async {
173 + expect(walletType, WalletType.solana);
174 + expect(address, usdcSolMint);
175 + return usdcSol;
176 + },
177 + ),
178 + );
179 +
180 + final resolution = await resolver.resolve(
181 + parse("solana:$solAddress?amount=3&spl-token=$usdcSolMint"),
182 + snapshot(type: WalletType.monero),
183 + );
184 +
185 + final resolved = resolution as TokenResolved;
186 + expect(resolved.chainId, null);
187 + expect(resolved.token, usdcSol);
188 + expect(resolved.amountOverride, "3");
189 + });
190 +
191 + test("an evm contract without the evm proxy stays unknown without any lookups", () async {
192 + final resolver = AnyPayResolver(tokenLookup: _FakeTokenLookup());
193 +
194 + final resolution = await resolver.resolve(
195 + parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
196 + snapshot(hasEvmProxy: false),
197 + );
198 +
199 + expect(resolution, isA<TokenUnknown>());
200 + });
201 +}
test/core/anypay/anypay_router_test.dart new
+414
@@ -0,0 +1,414 @@
1 +import "package:cake_wallet/core/anypay/anypay_models.dart";
2 +import "package:cake_wallet/core/anypay/anypay_parser.dart";
3 +import "package:cake_wallet/core/anypay/anypay_router.dart";
4 +import "package:cake_wallet/reactions/wallet_connect.dart";
5 +import "package:cw_core/crypto_currency.dart";
6 +import "package:cw_core/erc20_token.dart";
7 +import "package:cw_core/wallet_info.dart";
8 +import "package:cw_core/wallet_type.dart";
9 +import "package:flutter_test/flutter_test.dart";
10 +
11 +void main() {
12 + const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
13 + const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
14 + const btcAddress = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq";
15 + const xmrAddress =
16 + "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A";
17 + const tronAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
18 + const solAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
19 +
20 + final usdt = Erc20Token(
21 + name: "Tether",
22 + symbol: "USDT",
23 + contractAddress: usdtEthContract,
24 + decimal: 6,
25 + );
26 +
27 + WalletInfo walletOf(WalletType type) => WalletInfo.external(
28 + id: "id-${type.name}",
29 + name: "wallet-${type.name}",
30 + type: type,
31 + isRecovery: false,
32 + restoreHeight: 0,
33 + date: DateTime(2026),
34 + dirPath: "",
35 + path: "",
36 + address: "",
37 + );
38 +
39 + WalletSnapshot snapshot({
40 + WalletType type = WalletType.ethereum,
41 + int? chainId,
42 + List<WalletInfo> wallets = const [],
43 + bool hasEvmProxy = true,
44 + bool hasSolanaProxy = true,
45 + bool hasTronProxy = true,
46 + }) =>
47 + WalletSnapshot(
48 + type: type,
49 + currentWalletIsEvm: isEVMCompatibleChain(type),
50 + currentChainId: chainId,
51 + wallets: wallets,
52 + supportedEvmChains: hasEvmProxy
53 + ? const {
54 + 1: WalletType.ethereum,
55 + 137: WalletType.polygon,
56 + 8453: WalletType.base,
57 + }
58 + : const {},
59 + hasEvmProxy: hasEvmProxy,
60 + hasSolanaProxy: hasSolanaProxy,
61 + hasTronProxy: hasTronProxy,
62 + );
63 +
64 + AnyPayRequest parse(String input) => AnyPayParser.fromRaw(input);
65 +
66 + group("evm routing", () {
67 + test("explicit chain matching the current wallet applies directly", () {
68 + final decision = AnyPayRouter.route(
69 + parse("ethereum:$recipient@1?value=1e18"),
70 + snapshot(chainId: 1),
71 + const NoTokenRequested(),
72 + );
73 +
74 + expect(decision, isA<AnyPayApplyToCurrentWallet>());
75 + });
76 +
77 + test("explicit chain differing from the current wallet goes cross chain", () {
78 + final decision = AnyPayRouter.route(
79 + parse("ethereum:$recipient@8453?value=1e18"),
80 + snapshot(chainId: 1, wallets: [walletOf(WalletType.base)]),
81 + const NoTokenRequested(),
82 + );
83 +
84 + expect(decision, isA<AnyPayCrossChainPayment>());
85 + final crossChain = decision as AnyPayCrossChainPayment;
86 + expect(crossChain.targetChainId, 8453);
87 + expect(crossChain.targetWalletType, WalletType.base);
88 + expect(crossChain.hasCompatibleWallet, true);
89 + });
90 +
91 + test("cross chain with no wallet for the target offers no compatible wallet", () {
92 + final decision = AnyPayRouter.route(
93 + parse("ethereum:$recipient@8453?value=1e18"),
94 + snapshot(chainId: 1),
95 + const NoTokenRequested(),
96 + );
97 +
98 + expect((decision as AnyPayCrossChainPayment).hasCompatibleWallet, false);
99 + });
100 +
101 + test("unsupported explicit chain is rejected with its id", () {
102 + final decision = AnyPayRouter.route(
103 + parse("ethereum:$recipient@10?value=1e18"),
104 + snapshot(chainId: 1),
105 + const NoTokenRequested(),
106 + );
107 +
108 + expect((decision as AnyPayUnsupportedNetwork).chainId, 10);
109 + });
110 +
111 + test("chainless request binds to the current evm chain", () {
112 + final decision = AnyPayRouter.route(
113 + parse("ethereum:$recipient?value=1e18"),
114 + snapshot(type: WalletType.base, chainId: 8453),
115 + const NoTokenRequested(),
116 + );
117 +
118 + expect(decision, isA<AnyPayApplyToCurrentWallet>());
119 + });
120 +
121 + test("chainless request at a non evm wallet targets mainnet cross chain", () {
122 + final decision = AnyPayRouter.route(
123 + parse("ethereum:$recipient?value=1e18"),
124 + snapshot(type: WalletType.monero, wallets: [walletOf(WalletType.ethereum)]),
125 + const NoTokenRequested(),
126 + );
127 +
128 + expect((decision as AnyPayCrossChainPayment).targetChainId, 1);
129 + });
130 +
131 + test("raw evm address at an evm wallet applies directly", () {
132 + final decision = AnyPayRouter.route(
133 + parse(recipient),
134 + snapshot(chainId: 1),
135 + const NoTokenRequested(),
136 + );
137 +
138 + expect(decision, isA<AnyPayApplyToCurrentWallet>());
139 + });
140 +
141 + test("raw evm address at a non evm wallet asks for a network choice", () {
142 + final decision = AnyPayRouter.route(
143 + parse(recipient),
144 + snapshot(type: WalletType.solana),
145 + const NoTokenRequested(),
146 + );
147 +
148 + expect(decision, isA<AnyPayEvmNetworkChoice>());
149 + });
150 +
151 + test("resolved token on the current chain applies with the token", () {
152 + final decision = AnyPayRouter.route(
153 + parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
154 + snapshot(type: WalletType.base, chainId: 8453),
155 + TokenResolved(token: usdt, chainId: 8453, amountOverride: "2"),
156 + );
157 +
158 + final apply = decision as AnyPayApplyToCurrentWallet;
159 + expect(apply.token, usdt);
160 + expect(apply.amountOverride, "2");
161 + });
162 +
163 + test("chainless token resolved on another chain reroutes there", () {
164 + final decision = AnyPayRouter.route(
165 + parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
166 + snapshot(type: WalletType.base, chainId: 8453, wallets: [walletOf(WalletType.ethereum)]),
167 + TokenResolved(token: usdt, chainId: 1, amountOverride: "2"),
168 + );
169 +
170 + final crossChain = decision as AnyPayCrossChainPayment;
171 + expect(crossChain.targetChainId, 1);
172 + expect(crossChain.token, usdt);
173 + });
174 +
175 + test("an unknown token never falls back to the native asset", () {
176 + final decision = AnyPayRouter.route(
177 + parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=2000000"),
178 + snapshot(chainId: 1),
179 + const TokenUnknown(usdtEthContract),
180 + );
181 +
182 + expect(decision, isA<AnyPayUnsupportedToken>());
183 + expect((decision as AnyPayUnsupportedToken).contract, usdtEthContract);
184 + });
185 +
186 + test("an unsupported network wins over an unknown token", () {
187 + final decision = AnyPayRouter.route(
188 + parse("ethereum:$usdtEthContract@10/transfer?address=$recipient&uint256=2000000"),
189 + snapshot(chainId: 1),
190 + const TokenUnknown(usdtEthContract),
191 + );
192 +
193 + expect((decision as AnyPayUnsupportedNetwork).chainId, 10);
194 + });
195 +
196 + test("evm address without the evm proxy applies as plain input", () {
197 + final decision = AnyPayRouter.route(
198 + parse(recipient),
199 + snapshot(type: WalletType.monero, hasEvmProxy: false),
200 + const NoTokenRequested(),
201 + );
202 +
203 + expect(decision, isA<AnyPayApplyToCurrentWallet>());
204 + expect((decision as AnyPayApplyToCurrentWallet).token, null);
205 + });
206 + });
207 +
208 + group("non evm routing", () {
209 + test("address matching the current wallet applies directly", () {
210 + final decision = AnyPayRouter.route(
211 + parse(xmrAddress),
212 + snapshot(type: WalletType.monero),
213 + const NoTokenRequested(),
214 + );
215 +
216 + expect(decision, isA<AnyPayApplyToCurrentWallet>());
217 + });
218 +
219 + test("btc address at another wallet goes cross chain with its wallets", () {
220 + final decision = AnyPayRouter.route(
221 + parse(btcAddress),
222 + snapshot(type: WalletType.monero, wallets: [walletOf(WalletType.bitcoin)]),
223 + const NoTokenRequested(),
224 + );
225 +
226 + final crossChain = decision as AnyPayCrossChainPayment;
227 + expect(crossChain.targetWalletType, WalletType.bitcoin);
228 + expect(crossChain.hasCompatibleWallet, true);
229 + });
230 +
231 + test("btc address with no bitcoin wallet leaves only the swap offer", () {
232 + final decision = AnyPayRouter.route(
233 + parse(btcAddress),
234 + snapshot(type: WalletType.monero),
235 + const NoTokenRequested(),
236 + );
237 +
238 + expect((decision as AnyPayCrossChainPayment).hasCompatibleWallet, false);
239 + });
240 +
241 + test("tron address at another wallet goes cross chain to tron", () {
242 + final decision = AnyPayRouter.route(
243 + parse(tronAddress),
244 + snapshot(chainId: 1, wallets: [walletOf(WalletType.tron)]),
245 + const NoTokenRequested(),
246 + );
247 +
248 + final crossChain = decision as AnyPayCrossChainPayment;
249 + expect(crossChain.targetWalletType, WalletType.tron);
250 + expect(crossChain.targetChainId, null);
251 + expect(crossChain.wallets.length, 1);
252 + });
253 +
254 + test("solana address without the solana proxy applies as plain input", () {
255 + final decision = AnyPayRouter.route(
256 + parse(solAddress),
257 + snapshot(chainId: 1, hasSolanaProxy: false),
258 + const NoTokenRequested(),
259 + );
260 +
261 + expect(decision, isA<AnyPayApplyToCurrentWallet>());
262 + });
263 +
264 + test("an unknown non evm token never falls back to the native asset", () {
265 + final decision = AnyPayRouter.route(
266 + parse("solana:$solAddress?amount=3&spl-token=$solAddress"),
267 + snapshot(type: WalletType.solana),
268 + const TokenUnknown(solAddress),
269 + );
270 +
271 + expect(decision, isA<AnyPayUnsupportedToken>());
272 + });
273 +
274 + test("solana token resolved on the current wallet applies with the token", () {
275 + const splToken = CryptoCurrency.usdtSol;
276 + final decision = AnyPayRouter.route(
277 + parse("solana:$solAddress?amount=3&spl-token=$solAddress"),
278 + snapshot(type: WalletType.solana),
279 + const TokenResolved(token: splToken, amountOverride: "3"),
280 + );
281 +
282 + final apply = decision as AnyPayApplyToCurrentWallet;
283 + expect(apply.token, splToken);
284 + expect(apply.amountOverride, "3");
285 + });
286 + });
287 +
288 + group("native request currency", () {
289 + test("an explicit native request with an amount carries its currency", () {
290 + final decision = AnyPayRouter.route(
291 + parse("ethereum:$recipient@1?value=2e18"),
292 + snapshot(chainId: 1),
293 + const NoTokenRequested(),
294 + );
295 +
296 + expect((decision as AnyPayApplyToCurrentWallet).fallbackCurrency, CryptoCurrency.eth);
297 + });
298 +
299 + test("a raw address carries no fallback currency", () {
300 + final decision = AnyPayRouter.route(
301 + parse(recipient),
302 + snapshot(chainId: 1),
303 + const NoTokenRequested(),
304 + );
305 +
306 + expect((decision as AnyPayApplyToCurrentWallet).fallbackCurrency, null);
307 + });
308 + });
309 +
310 + group("degenerate input", () {
311 + test("empty input is invalid", () {
312 + final decision = AnyPayRouter.route(
313 + parse(""),
314 + snapshot(chainId: 1),
315 + const NoTokenRequested(),
316 + );
317 +
318 + expect(decision, isA<AnyPayEmptyInput>());
319 + });
320 +
321 + test("undetectable text applies as plain input for the validator to reject", () {
322 + final decision = AnyPayRouter.route(
323 + parse("definitelynotanaddress"),
324 + snapshot(chainId: 1),
325 + const NoTokenRequested(),
326 + );
327 +
328 + expect(decision, isA<AnyPayApplyToCurrentWallet>());
329 + });
330 + });
331 +
332 + group("requested evm chain", () {
333 + test("explicit binding wins over the wallet chain", () {
334 + final request = parse("ethereum:$recipient@137?value=1e18");
335 +
336 + expect(AnyPayRouter.requestedEvmChainId(request, snapshot(chainId: 1)), 137);
337 + });
338 +
339 + test("chainless binding takes the wallet chain", () {
340 + final request = parse("ethereum:$recipient?value=1e18");
341 +
342 + expect(
343 + AnyPayRouter.requestedEvmChainId(
344 + request,
345 + snapshot(type: WalletType.base, chainId: 8453),
346 + ),
347 + 8453,
348 + );
349 + });
350 +
351 + test("chainless binding defaults to mainnet without an evm wallet", () {
352 + final request = parse("ethereum:$recipient?value=1e18");
353 +
354 + expect(
355 + AnyPayRouter.requestedEvmChainId(request, snapshot(type: WalletType.monero)),
356 + 1,
357 + );
358 + });
359 +
360 + test("raw addresses bind no target", () {
361 + expect(AnyPayRouter.requestedEvmChainId(parse(recipient), snapshot(chainId: 1)), null);
362 + });
363 + });
364 +
365 + group("swap incompatible recipients", () {
366 + final spAddress = "sp1${"q" * 113}";
367 + final mwebAddress = "ltcmweb1q${"a" * 90}";
368 +
369 + test("a silent payment address with a bitcoin wallet switches without a swap offer", () {
370 + final decision = AnyPayRouter.route(
371 + parse(spAddress),
372 + snapshot(type: WalletType.monero, wallets: [walletOf(WalletType.bitcoin)]),
373 + const NoTokenRequested(),
374 + );
375 +
376 + final crossChain = decision as AnyPayCrossChainPayment;
377 + expect(crossChain.targetWalletType, WalletType.bitcoin);
378 + expect(crossChain.hasCompatibleWallet, true);
379 + expect(crossChain.canSwap, false);
380 + });
381 +
382 + test("a silent payment address with no bitcoin wallet cannot swap either", () {
383 + final decision = AnyPayRouter.route(
384 + parse(spAddress),
385 + snapshot(type: WalletType.monero),
386 + const NoTokenRequested(),
387 + );
388 +
389 + final crossChain = decision as AnyPayCrossChainPayment;
390 + expect(crossChain.hasCompatibleWallet, false);
391 + expect(crossChain.canSwap, false);
392 + });
393 +
394 + test("an mweb address never offers a swap", () {
395 + final decision = AnyPayRouter.route(
396 + parse(mwebAddress),
397 + snapshot(),
398 + const NoTokenRequested(),
399 + );
400 +
401 + expect((decision as AnyPayCrossChainPayment).canSwap, false);
402 + });
403 +
404 + test("a plain btc address keeps the swap offer", () {
405 + final decision = AnyPayRouter.route(
406 + parse(btcAddress),
407 + snapshot(type: WalletType.monero),
408 + const NoTokenRequested(),
409 + );
410 +
411 + expect((decision as AnyPayCrossChainPayment).canSwap, true);
412 + });
413 + });
414 +}
test/core/universal_address_detector_test.dart
+32
@@ -196,6 +196,38 @@ void main() {
196 });
197 });
198
199 + group("Zcash Address Detection", () {
200 + test("detects Zcash transparent address", () {
201 + const address = "t1V9mnyk5Z5cTNMCkLbaDwSskgJZucTLdgW";
202 + final result = UniversalAddressDetector.detectAddress(address);
203 +
204 + expect(result.isValid, true);
205 + expect(result.detectedCurrency, CryptoCurrency.zec);
206 + expect(result.detectedWalletType, WalletType.zcash);
207 + expect(result.address, address);
208 + });
209 +
210 + test("detects Zcash shielded address", () {
211 + const address =
212 + "zsab12cd34efab12cd34efab12cd34efab12cd34efab12cd34efab12cd34efab12cd34efgh56kl";
213 + final result = UniversalAddressDetector.detectAddress(address);
214 +
215 + expect(result.isValid, true);
216 + expect(result.detectedCurrency, CryptoCurrency.zec);
217 + expect(result.detectedWalletType, WalletType.zcash);
218 + });
219 +
220 + test("detects Zcash unified address", () {
221 + const address =
222 + "u1z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5";
223 + final result = UniversalAddressDetector.detectAddress(address);
224 +
225 + expect(result.isValid, true);
226 + expect(result.detectedCurrency, CryptoCurrency.zec);
227 + expect(result.detectedWalletType, WalletType.zcash);
228 + });
229 + });
230 +
231 group('Wownero Address Detection', () {
232 test('detects Wownero address', () {
233 const address =