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>
This commit is contained in:
David Adegoke
2026-08-20 09:35:44 +02:00
committed by GitHub
co-authored by Czarek Nakamoto
parent 4cc1495686
commit 3b100eae33
67 changed files with 2672 additions and 1210 deletions
@@ -90,7 +90,9 @@
<data android:scheme="polygon" />
<data android:scheme="polygon-wallet" />
<data android:scheme="polygon_wallet" />
<data android:scheme="solana" />
<data android:scheme="solana-wallet" />
<data android:scheme="solana_wallet" />
<data android:scheme="tron" />
<data android:scheme="tron-wallet" />
<data android:scheme="tron_wallet" />
+1 -1
View File
@@ -88,7 +88,7 @@ dependency_overrides:
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v11
ref: bda0e2904effa799ec835b4578cc3fc5d31caa78
pointycastle: 3.7.4
ffi: 2.1.0
intl: any
+1 -1
View File
@@ -43,7 +43,7 @@ dependency_overrides:
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v11
ref: bda0e2904effa799ec835b4578cc3fc5d31caa78
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
+1
View File
@@ -1007,6 +1007,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
'bitcoin': btc,
'bitcoincash': bch,
'polygon': maticpoly,
'solana': sol,
'nano-gpt': nano,
'secret': scrt,
'stellar': xlm,
+9
View File
@@ -70,3 +70,12 @@ class WalletDeprecationException implements Exception {
WalletDeprecationException({required this.seed, required this.curr});
}
class WalletSwitchException implements Exception {
WalletSwitchException(this.message);
final String message;
@override
String toString() => message;
}
+1 -1
View File
@@ -40,7 +40,7 @@ dependency_overrides:
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: cake-update-v11
ref: bda0e2904effa799ec835b4578cc3fc5d31caa78
# For information on the generic Dart part of this file, see the
+2
View File
@@ -199,7 +199,9 @@
<string>solana-wallet</string>
<key>CFBundleURLSchemes</key>
<array>
<string>solana</string>
<string>solana-wallet</string>
<string>solana_wallet</string>
</array>
</dict>
<dict>
+4 -14
View File
@@ -177,13 +177,7 @@ class AddressValidator extends TextValidator {
if (ZcashNetworkType.isDevNetwork(network)) {
return null;
}
pattern = '(?:'
't1[0-9A-Za-z]{33}'
'|t3[0-9A-Za-z]{33}'
'|zs[a-z0-9]{76}'
'|zxviews[a-z0-9]{278}'
'|u1[a-z0-9]{1,300}'
')';
pattern = "(?:$zcashAddressPattern|zxviews[a-z0-9]{278})";
case CryptoCurrency.dcr:
pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+';
case CryptoCurrency.rvn:
@@ -355,6 +349,8 @@ class AddressValidator extends TextValidator {
static String get silentPaymentAddressPatternMainnet => 'sp1[0-9a-zA-Z]{113}';
static String get silentPaymentAddressPatternTestnet => '(tsp|sprt)1[0-9a-zA-Z]{113}';
static String get mWebAddressPattern => MwebAddress.regex.pattern;
static String get zcashAddressPattern =>
"t1[0-9A-Za-z]{33}|t3[0-9A-Za-z]{33}|zs[a-z0-9]{76}|u1[a-z0-9]{1,300}";
static const String bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$';
static const String bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$';
@@ -401,13 +397,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.zano:
pattern = '([1-9A-HJ-NP-Za-km-z]{90,200})|(@[\w\d.-]+)';
case CryptoCurrency.zec:
pattern = '(?:'
't1[0-9A-Za-z]{33}'
'|t3[0-9A-Za-z]{33}'
'|zs[a-z0-9]{76}'
'|zxviews[a-z0-9]{278}'
'|u1[a-z0-9]{1,300}'
')';
pattern = "(?:$zcashAddressPattern|zxviews[a-z0-9]{278})";
default:
if (type.tag == CryptoCurrency.eth.title) {
pattern = '0x[0-9a-zA-Z]+';
+174
View File
@@ -0,0 +1,174 @@
import "package:cake_wallet/core/universal_address_detector.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
sealed class ChainBinding {
const ChainBinding();
}
class ExplicitEvmChain extends ChainBinding {
const ExplicitEvmChain(this.chainId);
final int chainId;
}
class ChainlessEvm extends ChainBinding {
const ChainlessEvm();
}
class NoEvmBinding extends ChainBinding {
const NoEvmBinding();
}
class AnyPayRequest {
AnyPayRequest({
required this.rawInput,
required this.paymentRequest,
required this.detection,
required this.chainBinding,
});
final String rawInput;
final PaymentRequest paymentRequest;
final AddressDetectionResult detection;
final ChainBinding chainBinding;
String get address => paymentRequest.address;
String get amount => paymentRequest.amount;
String? get contractAddress => paymentRequest.contractAddress;
bool get hasContract => contractAddress != null && contractAddress!.isNotEmpty;
bool get isLightning => detection.detectedCurrency == CryptoCurrency.btcln;
}
class WalletSnapshot {
WalletSnapshot({
required this.type,
required this.currentWalletIsEvm,
required this.currentChainId,
required this.wallets,
required this.supportedEvmChains,
required this.hasEvmProxy,
required this.hasSolanaProxy,
required this.hasTronProxy,
});
final WalletType type;
final bool currentWalletIsEvm;
final int? currentChainId;
final List<WalletInfo> wallets;
final Map<int, WalletType> supportedEvmChains;
final bool hasEvmProxy;
final bool hasSolanaProxy;
final bool hasTronProxy;
List<WalletInfo> walletsOfType(WalletType walletType) =>
wallets.where((wallet) => wallet.type == walletType).toList();
}
sealed class AnyPayResolution {
const AnyPayResolution();
}
class NoTokenRequested extends AnyPayResolution {
const NoTokenRequested();
}
class TokenResolved extends AnyPayResolution {
const TokenResolved({required this.token, this.chainId, this.amountOverride});
final CryptoCurrency token;
final int? chainId;
final String? amountOverride;
}
class TokenUnknown extends AnyPayResolution {
const TokenUnknown(this.contract);
final String contract;
}
sealed class AnyPayDecision {
const AnyPayDecision();
}
class AnyPayApplyToCurrentWallet extends AnyPayDecision {
const AnyPayApplyToCurrentWallet({
this.token,
this.amountOverride,
this.fallbackCurrency,
});
final CryptoCurrency? token;
final String? amountOverride;
final CryptoCurrency? fallbackCurrency;
}
class AnyPayEvmNetworkChoice extends AnyPayDecision {
const AnyPayEvmNetworkChoice();
}
class AnyPayCrossChainPayment extends AnyPayDecision {
const AnyPayCrossChainPayment({
required this.targetWalletType,
required this.wallets,
this.targetChainId,
this.token,
this.amountOverride,
this.canSwap = true,
});
final WalletType targetWalletType;
final int? targetChainId;
final List<WalletInfo> wallets;
final CryptoCurrency? token;
final String? amountOverride;
final bool canSwap;
bool get hasCompatibleWallet => wallets.isNotEmpty;
}
class AnyPayUnsupportedNetwork extends AnyPayDecision {
const AnyPayUnsupportedNetwork(this.chainId);
final int chainId;
}
class AnyPayUnsupportedToken extends AnyPayDecision {
const AnyPayUnsupportedToken(this.contract);
final String contract;
}
class AnyPayEmptyInput extends AnyPayDecision {
const AnyPayEmptyInput();
}
class AnyPayEvaluation {
AnyPayEvaluation({required this.request, required this.decision});
final AnyPayRequest request;
final AnyPayDecision decision;
}
class AnyPaySwapIntent {
AnyPaySwapIntent({
required this.request,
required this.receiveCurrency,
required this.targetWalletType,
this.receiveAmount,
});
final AnyPayRequest request;
final CryptoCurrency receiveCurrency;
final WalletType targetWalletType;
final Money? receiveAmount;
String get recipientAddress => request.address;
}
+71
View File
@@ -0,0 +1,71 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/universal_address_detector.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/payment_uris.dart";
class AnyPayParser {
static AnyPayRequest fromRaw(String input) {
final paymentRequest = PaymentRequest.fromString(input);
return AnyPayRequest(
rawInput: input,
paymentRequest: paymentRequest,
detection: UniversalAddressDetector.detectAddress(input),
chainBinding: _bindingFor(paymentRequest),
);
}
static AnyPayRequest fromPaymentRequest(PaymentRequest request) {
final raw = _reconstructRawInput(request);
return AnyPayRequest(
rawInput: raw,
paymentRequest: request,
detection: UniversalAddressDetector.detectAddress(raw),
chainBinding: _bindingFor(request),
);
}
static String _reconstructRawInput(PaymentRequest request) {
final scheme = request.scheme.toLowerCase();
if (scheme.isEmpty || scheme == "lightning") {
return request.address;
}
if (scheme == "ethereum") {
return ERC681URI(
address: request.address,
amount: request.amount,
contractAddress: request.contractAddress,
chainId: request.chainId ?? 1,
rawTokenAmount: request.rawTokenAmount,
).toString();
}
final amount = request.amount.isNotEmpty ? "?amount=${request.amount}" : "";
return "${request.scheme}:${request.address}$amount";
}
static ChainBinding _bindingFor(PaymentRequest request) {
final scheme = request.scheme.toLowerCase();
if (scheme == "ethereum") {
final chainId = request.chainId;
return chainId != null ? ExplicitEvmChain(chainId) : const ChainlessEvm();
}
if (scheme.isNotEmpty) {
try {
final chainId = getChainIdByCryptoCurrency(CryptoCurrency.fromString(scheme));
if (chainId != null) {
return ExplicitEvmChain(chainId);
}
} catch (_) {}
}
return const NoEvmBinding();
}
}
+109
View File
@@ -0,0 +1,109 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_router.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/utils/token_utilities.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayResolver {
AnyPayResolver({AnyPayTokenLookup? tokenLookup})
: _tokenLookup = tokenLookup ?? const TokenUtilitiesLookup();
final AnyPayTokenLookup _tokenLookup;
Future<AnyPayResolution> resolve(AnyPayRequest request, WalletSnapshot snapshot) async {
if (!request.hasContract) {
return const NoTokenRequested();
}
final contract = request.contractAddress!;
final detectedType = request.detection.detectedWalletType;
if (detectedType != null && isEVMCompatibleChain(detectedType)) {
return _resolveEvm(request, snapshot, contract);
}
final token = await _tokenLookup.findTokenByAddress(
walletType: detectedType ?? snapshot.type,
address: contract,
);
if (token == null) {
return TokenUnknown(contract);
}
return TokenResolved(
token: token,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
Future<AnyPayResolution> _resolveEvm(
AnyPayRequest request,
WalletSnapshot snapshot,
String contract,
) async {
final requestedChainId = AnyPayRouter.requestedEvmChainId(request, snapshot) ?? 1;
final lookupType = snapshot.supportedEvmChains[requestedChainId];
if (lookupType != null) {
final token =
await _tokenLookup.findTokenByAddress(walletType: lookupType, address: contract);
if (token != null) {
return TokenResolved(
token: token,
chainId: requestedChainId,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
}
// The QRs from our old app versions omit the chainId on mainnet, so a contract the target
// network does not know may still belong to another EVM network.
if (request.chainBinding is ChainlessEvm) {
final reboundChainId = await _tokenLookup.findEvmChainIdForContract(
contract,
excludingChainId: requestedChainId,
);
final reboundType =
reboundChainId != null ? snapshot.supportedEvmChains[reboundChainId] : null;
if (reboundType != null) {
final token =
await _tokenLookup.findTokenByAddress(walletType: reboundType, address: contract);
if (token != null) {
return TokenResolved(
token: token,
chainId: reboundChainId,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
}
}
return TokenUnknown(contract);
}
}
abstract class AnyPayTokenLookup {
Future<CryptoCurrency?> findTokenByAddress({
required WalletType walletType,
required String address,
});
Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId});
}
class TokenUtilitiesLookup implements AnyPayTokenLookup {
const TokenUtilitiesLookup();
@override
Future<CryptoCurrency?> findTokenByAddress({
required WalletType walletType,
required String address,
}) =>
TokenUtilities.findTokenByAddress(walletType: walletType, address: address);
@override
Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId}) =>
TokenUtilities.findEvmChainIdForContract(contractAddress, excludingChainId: excludingChainId);
}
+141
View File
@@ -0,0 +1,141 @@
import "package:cake_wallet/core/address_validator.dart";
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayRouter {
static final List<RegExp> _swapIncompatibleAddressPatterns = [
RegExp("^(?:${AddressValidator.silentPaymentAddressPatternMainnet})\$"),
RegExp("^(?:${AddressValidator.silentPaymentAddressPatternTestnet})\$"),
RegExp("^(?:${AddressValidator.mWebAddressPattern})\$"),
];
static bool _isSwapIncompatibleAddress(String address) =>
_swapIncompatibleAddressPatterns.any((pattern) => pattern.hasMatch(address));
static AnyPayDecision route(
AnyPayRequest request,
WalletSnapshot snapshot,
AnyPayResolution resolution,
) {
if (request.rawInput.trim().isEmpty) {
return const AnyPayEmptyInput();
}
final detection = request.detection;
final detectedType = detection.detectedWalletType;
if (!detection.isValid || detectedType == null) {
return const AnyPayApplyToCurrentWallet();
}
if (detectedType == WalletType.solana && !snapshot.hasSolanaProxy) {
return const AnyPayApplyToCurrentWallet();
}
if (detectedType == WalletType.tron && !snapshot.hasTronProxy) {
return const AnyPayApplyToCurrentWallet();
}
if (isEVMCompatibleChain(detectedType)) {
if (!snapshot.hasEvmProxy) {
return const AnyPayApplyToCurrentWallet();
}
return _routeEvm(request, snapshot, resolution);
}
if (resolution is TokenUnknown) {
return AnyPayUnsupportedToken(resolution.contract);
}
if (detectedType == snapshot.type) {
return AnyPayApplyToCurrentWallet(
token: resolution is TokenResolved ? resolution.token : null,
amountOverride: resolution is TokenResolved ? resolution.amountOverride : null,
fallbackCurrency: _nativeRequestCurrency(request),
);
}
return AnyPayCrossChainPayment(
targetWalletType: detectedType,
wallets: snapshot.walletsOfType(detectedType),
token: resolution is TokenResolved ? resolution.token : null,
amountOverride: resolution is TokenResolved ? resolution.amountOverride : null,
canSwap: !_isSwapIncompatibleAddress(request.address),
);
}
static AnyPayDecision _routeEvm(
AnyPayRequest request,
WalletSnapshot snapshot,
AnyPayResolution resolution,
) {
final requestedChainId = requestedEvmChainId(request, snapshot);
if (requestedChainId == null) {
if (snapshot.currentWalletIsEvm) {
return const AnyPayApplyToCurrentWallet();
}
return const AnyPayEvmNetworkChoice();
}
if (!snapshot.supportedEvmChains.containsKey(requestedChainId)) {
return AnyPayUnsupportedNetwork(requestedChainId);
}
if (resolution is TokenUnknown) {
return AnyPayUnsupportedToken(resolution.contract);
}
int targetChainId = requestedChainId;
if (resolution is TokenResolved && resolution.chainId != null) {
targetChainId = resolution.chainId!;
}
final token = resolution is TokenResolved ? resolution.token : null;
final amountOverride = resolution is TokenResolved ? resolution.amountOverride : null;
if (snapshot.currentWalletIsEvm && snapshot.currentChainId == targetChainId) {
return AnyPayApplyToCurrentWallet(
token: token,
amountOverride: amountOverride,
fallbackCurrency: _nativeRequestCurrency(request),
);
}
final targetWalletType = snapshot.supportedEvmChains[targetChainId]!;
return AnyPayCrossChainPayment(
targetWalletType: targetWalletType,
targetChainId: targetChainId,
wallets: snapshot.walletsOfType(targetWalletType),
token: token,
amountOverride: amountOverride,
);
}
static CryptoCurrency? _nativeRequestCurrency(AnyPayRequest request) {
if (request.paymentRequest.scheme.isEmpty || request.hasContract || request.amount.isEmpty) {
return null;
}
return request.detection.detectedCurrency;
}
static int? requestedEvmChainId(AnyPayRequest request, WalletSnapshot snapshot) {
final binding = request.chainBinding;
if (binding is ExplicitEvmChain) {
return binding.chainId;
}
if (binding is ChainlessEvm) {
// the eip 681 spec says we should use the current chain id if there is no chainId in the incoming request
return snapshot.currentWalletIsEvm ? snapshot.currentChainId ?? 1 : 1;
}
return null;
}
}
+140
View File
@@ -0,0 +1,140 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_parser.dart";
import "package:cake_wallet/core/anypay/anypay_resolver.dart";
import "package:cake_wallet/core/anypay/anypay_router.dart";
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/new-ui/services/wallet_switch_service.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/solana/solana.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cake_wallet/tron/tron.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayService {
AnyPayService({
required this.appStore,
required this.walletSwitchService,
AnyPayResolver? resolver,
}) : resolver = resolver ?? AnyPayResolver();
final AppStore appStore;
final WalletSwitchService walletSwitchService;
final AnyPayResolver resolver;
Future<AnyPayEvaluation> evaluateRawInput(String input) => _evaluate(AnyPayParser.fromRaw(input));
Future<AnyPayEvaluation> evaluatePaymentRequest(PaymentRequest request) =>
_evaluate(AnyPayParser.fromPaymentRequest(request));
Future<AnyPayEvaluation> evaluateForEvmChain(AnyPayRequest request, int chainId) => _evaluate(
AnyPayRequest(
rawInput: request.rawInput,
paymentRequest: request.paymentRequest,
detection: request.detection,
chainBinding: ExplicitEvmChain(chainId),
),
);
Future<AnyPayEvaluation> _evaluate(AnyPayRequest request) async {
try {
final snapshot = await _buildWalletSnapshot();
final resolution = await resolver.resolve(request, snapshot);
return AnyPayEvaluation(
request: request,
decision: AnyPayRouter.route(request, snapshot, resolution),
);
} catch (e) {
printV("anypay evaluation failed: $e");
return AnyPayEvaluation(
request: request,
decision: const AnyPayApplyToCurrentWallet(),
);
}
}
Future<WalletSnapshot> _buildWalletSnapshot() async {
final wallet = appStore.wallet!;
final currentWalletIsEvm = isEVMCompatibleChain(wallet.type);
final supportedEvmChains = <int, WalletType>{};
if (evm != null) {
for (final chain in evm!.getAllChains()) {
final walletType = evm!.getWalletTypeByChainId(chain.chainId);
if (walletType != null) {
supportedEvmChains[chain.chainId] = walletType;
}
}
}
return WalletSnapshot(
type: wallet.type,
currentWalletIsEvm: currentWalletIsEvm,
currentChainId: currentWalletIsEvm && evm != null
? evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type)
: null,
wallets: await WalletInfo.getAll(),
supportedEvmChains: supportedEvmChains,
hasEvmProxy: evm != null,
hasSolanaProxy: solana != null,
hasTronProxy: tron != null,
);
}
AnyPaySwapIntent buildSwapIntent(
AnyPayRequest request, {
required WalletType targetWalletType,
int? targetChainId,
CryptoCurrency? token,
}) {
final receiveCurrency = token ??
(targetChainId != null
? walletTypeToCryptoCurrency(targetWalletType, chainId: targetChainId)
: request.detection.detectedCurrency ?? walletTypeToCryptoCurrency(targetWalletType));
final amountValue = token != null
? request.paymentRequest.resolveTokenAmount(token)
: (request.amount.isNotEmpty ? request.amount : null);
return AnyPaySwapIntent(
request: request,
receiveCurrency: receiveCurrency,
targetWalletType: targetWalletType,
receiveAmount: amountValue != null ? Money.tryParse(amountValue, receiveCurrency) : null,
);
}
Future<bool> switchWalletForPayment(WalletInfo walletInfo, {int? chainId}) async {
try {
await walletSwitchService.switchToWallet(walletInfo);
} catch (e) {
printV("wallet switch failed: $e");
return false;
}
final wallet = appStore.wallet!;
if (chainId != null && evm != null && isEVMCompatibleChain(wallet.type)) {
try {
final node = appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId);
await evm!.selectChain(wallet, chainId, node: node);
} catch (e, s) {
printV("switchWalletForPayment chain select failed: $e\n$s");
return false;
}
}
try {
await wallet.updateBalance();
} catch (e) {
printV("balance refresh after wallet switch failed: $e");
}
return true;
}
}
+6
View File
@@ -238,6 +238,12 @@ class UniversalAddressDetector {
currency: CryptoCurrency.doge,
),
// Zcash (transparent, Sapling shielded, unified)
_DetectionPattern(
pattern: RegExp("^(?:${AddressValidator.zcashAddressPattern})\$"),
currency: CryptoCurrency.zec,
),
// Solana (Base58 format)
_DetectionPattern(
pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{43,44}$'),
+14 -6
View File
@@ -7,6 +7,7 @@ import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/core/anypay/anypay_service.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
@@ -55,12 +56,11 @@ import 'package:cake_wallet/new-ui/new_dashboard.dart';
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
import 'package:cake_wallet/new-ui/pages/home_page.dart';
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import "package:cake_wallet/new-ui/services/wallet_switch_service.dart";
import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
import 'package:cake_wallet/new-ui/pages/receive_page.dart';
import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart';
@@ -169,7 +169,6 @@ import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
@@ -193,7 +192,6 @@ import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
import 'package:cake_wallet/store/bridge_transfers_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
import 'package:cake_wallet/store/node_list_store.dart';
import 'package:cake_wallet/store/seed_settings_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
@@ -250,7 +248,6 @@ import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_cr
import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
import 'package:cake_wallet/view_model/order_details_view_model.dart';
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
@@ -926,7 +923,8 @@ Future<void> setup({
authService: getIt.get<AuthService>(),
params: params,
contactListViewModel: getIt.get<ContactListViewModel>(),
paymentViewModel: getIt.get<PaymentViewModel>(),
anyPayService: getIt.get<AnyPayService>(),
linkViewModel: getIt.get<LinkViewModel>(),
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
);
});
@@ -1291,6 +1289,16 @@ Future<void> setup({
appStore: getIt.get<AppStore>(),
));
getIt.registerFactory(() => WalletSwitchService(
walletLoadingService: getIt.get<WalletLoadingService>(),
appStore: getIt.get<AppStore>(),
));
getIt.registerFactory(() => AnyPayService(
appStore: getIt.get<AppStore>(),
walletSwitchService: getIt.get<WalletSwitchService>(),
));
getIt.registerFactory(() => WalletSwitcherViewModel(
appStore: getIt.get<AppStore>(),
walletLoadingService: getIt.get<WalletLoadingService>(),
File diff suppressed because it is too large Load Diff
+36 -8
View File
@@ -7,6 +7,7 @@ import "package:cake_wallet/exchange/exchange_trade_state.dart";
import "package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart";
import "package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
@@ -27,6 +28,7 @@ import "package:cake_wallet/src/widgets/primary_button.dart";
import "package:cake_wallet/utils/debounce.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/dashboard/balance_view_model.dart";
import "package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart";
import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
@@ -51,6 +53,7 @@ class NewSwapPage extends StatefulWidget {
required this.walletSwitcherViewModel,
CryptoCurrency? initialCurrency,
this.fromSend,
this.balanceViewModel,
}) {
depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
? exchangeViewModel.wallet.name
@@ -69,6 +72,7 @@ class NewSwapPage extends StatefulWidget {
final AddressResolverService adrResService;
final PaymentRequest? initialPaymentRequest;
final SwapFromSendArgs? fromSend;
final BalanceViewModel? balanceViewModel;
late final String? depositWalletName;
late final String? receiveWalletName;
@@ -93,6 +97,25 @@ class _NewSwapPageState extends State<NewSwapPage> {
.contains(widget.exchangeViewModel.depositCurrency) &&
!(widget.exchangeViewModel.status is SyncedSyncStatus);
Map<CryptoCurrency, CurrencyPickerBalance>? _depositBalanceByAsset() {
final balanceViewModel = widget.balanceViewModel;
if (balanceViewModel == null) {
return null;
}
return {
for (final r in balanceViewModel.formattedBalances)
r.asset: CurrencyPickerBalance(
amount: "${r.availableBalance} ${r.asset.title}",
fiat: balanceViewModel.isFiatDisabled
? null
: "${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}",
fiatValue:
balanceViewModel.isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw),
),
};
}
@override
void initState() {
super.initState();
@@ -258,8 +281,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
(bool isReceiveAmountEditable) {},
));
_disposers
.add(reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) {
_disposers.add(
reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) async {
if (state is TradeIsCreatedFailure) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
@@ -298,11 +321,15 @@ class _NewSwapPageState extends State<NewSwapPage> {
exchangeTradeViewModel: vm,
receiveAmount: receiveAmount,
);
showMaterialModalBottomSheet(
context: context,
builder: (context) => page,
backgroundColor: Colors.transparent,
);
try {
await showMaterialModalBottomSheet<void>(
context: context,
builder: (context) => page,
backgroundColor: Colors.transparent,
);
} finally {
widget.exchangeViewModel.tradeStarted = false;
}
}
}));
@@ -629,7 +656,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
walletName: fromSend != null
? widget.exchangeViewModel.wallet.name
: null,
balanceByAsset: fromSend?.depositBalanceByAsset,
balanceByAsset:
fromSend != null ? _depositBalanceByAsset() : null,
useSingleNetworkLayout: fromSend != null,
filteredNetwork: fromSend != null
? widget.exchangeViewModel.wallet.type
@@ -0,0 +1,29 @@
import "package:cake_wallet/core/wallet_loading_service.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cw_core/exceptions.dart";
import "package:cw_core/wallet_info.dart";
class WalletSwitchService {
WalletSwitchService({
required WalletLoadingService walletLoadingService,
required AppStore appStore,
}) : _walletLoadingService = walletLoadingService,
_appStore = appStore;
final AppStore _appStore;
final WalletLoadingService _walletLoadingService;
Future<void> switchToWallet(WalletInfo walletInfo) async {
final wallet = await _walletLoadingService.load(walletInfo.type, walletInfo.name);
// load() recovers from a corrupted wallet by opening any other wallet it can,
// so the one it returns may not be the one that was requested.
if (wallet.name != walletInfo.name || wallet.type != walletInfo.type) {
throw WalletSwitchException(
"wallet switch loaded ${wallet.name} instead of ${walletInfo.name}",
);
}
await _appStore.changeCurrentWallet(wallet);
}
}
+391
View File
@@ -0,0 +1,391 @@
import "package:cake_wallet/core/address_resolver/address_resolver_service.dart";
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_service.dart";
import "package:cake_wallet/core/auth_service.dart";
import "package:cake_wallet/di.dart";
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/pages/swap_page.dart";
import "package:cake_wallet/new-ui/widgets/anypay/evm_address_detected_sheet.dart";
import "package:cake_wallet/new-ui/widgets/anypay/network_decision_page.dart";
import "package:cake_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
import "package:cake_wallet/new-ui/widgets/anypay/switch_network_wallet_page.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
import "package:cake_wallet/view_model/send/send_view_model.dart";
import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/unspent_coin_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_info.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
class AnyPayFormFill {
const AnyPayFormFill({
required this.request,
this.token,
this.fallbackCurrency,
this.amountOverride,
});
final AnyPayRequest request;
final CryptoCurrency? token;
final CryptoCurrency? fallbackCurrency;
final String? amountOverride;
}
class AnyPayFlow {
AnyPayFlow({
required this.anyPayService,
required this.sendViewModel,
required this.authService,
required this.walletSwitcherViewModel,
});
final AnyPayService anyPayService;
final SendViewModel sendViewModel;
final AuthService authService;
final WalletSwitcherViewModel walletSwitcherViewModel;
bool _isSwitchingWallet = false;
bool get isSwitchingWallet => _isSwitchingWallet;
Future<AnyPayFormFill?> handleEvaluation(
BuildContext context,
AnyPayEvaluation evaluation, {
CryptoCurrency? fallbackCurrency,
}) async {
final request = evaluation.request;
if (!request.rawInput.toLowerCase().startsWith("ethereum:") &&
(request.rawInput.contains("@") || request.address.contains("@"))) {
return null;
}
try {
return await _handleDecision(context, evaluation, fallbackCurrency);
} catch (e) {
printV("Payment flow error: $e");
return AnyPayFormFill(request: request);
}
}
Future<AnyPayFormFill?> _handleDecision(
BuildContext context,
AnyPayEvaluation evaluation,
CryptoCurrency? fallbackCurrency,
) async {
final request = evaluation.request;
switch (evaluation.decision) {
case final AnyPayApplyToCurrentWallet decision:
if (request.isLightning) {
_enterLightningMode();
}
return AnyPayFormFill(
request: request,
token: decision.token,
fallbackCurrency: fallbackCurrency ?? decision.fallbackCurrency,
amountOverride: decision.amountOverride,
);
case AnyPayEvmNetworkChoice():
return _pickEvmNetwork(context, request);
case final AnyPayCrossChainPayment decision:
return _handleCrossChain(context, request, decision, fallbackCurrency);
case final AnyPayUnsupportedNetwork decision:
_showError(
context,
S.of(context).unsupported_network_requested(decision.chainId.toString()),
);
return null;
case AnyPayUnsupportedToken():
_showError(context, S.of(context).unsupported_token_requested);
return null;
case AnyPayEmptyInput():
return null;
}
}
Future<AnyPayFormFill?> _pickEvmNetwork(BuildContext context, AnyPayRequest request) async {
final chains = evm!.getAllChains();
final networks = chains.map((chain) {
final walletType = evm!.getWalletTypeByChainId(chain.chainId) ?? sendViewModel.wallet.type;
return RecipientNetworkItem(
chainId: chain.chainId,
name: chain.name,
iconPath: getCryptoCurrencyIconForWalletListItem(walletType, chainId: chain.chainId),
chainBadgeIconPath: symbolIconPathForWalletType(walletType),
);
}).toList();
final selectedChainId =
await EvmAddressDetectedSheet.show(context: context, networks: networks);
if (!context.mounted || selectedChainId == null) {
return null;
}
final target = chains.firstWhere(
(chain) => chain.chainId == selectedChainId,
orElse: () => chains.first,
);
final evaluation = await anyPayService.evaluateForEvmChain(request, target.chainId);
if (!context.mounted) {
return null;
}
return handleEvaluation(context, evaluation, fallbackCurrency: target.currency);
}
Future<AnyPayFormFill?> _handleCrossChain(
BuildContext context,
AnyPayRequest request,
AnyPayCrossChainPayment decision,
CryptoCurrency? fallbackCurrency,
) async {
final destinationType = decision.targetWalletType;
final isEvmTarget = isEVMCompatibleChain(destinationType);
final destinationNetworkName = networkDisplayName(destinationType, decision.targetChainId);
final destinationNetworkIcon = symbolIconPathForWalletType(destinationType) ?? "";
if (!decision.canSwap && !decision.hasCompatibleWallet) {
_showError(
context,
S.of(context).swap_unavailable_needs_network_wallet(destinationNetworkName),
);
return null;
}
final currentType = sendViewModel.wallet.type;
final currentChainId = isEVMCompatibleChain(currentType) ? _currentEvmChainId() : null;
final currentNetworkName = networkDisplayName(currentType, currentChainId);
final currentNetworkIcon = symbolIconPathForWalletType(currentType) ?? "";
if (!decision.hasCompatibleWallet) {
final swapConfirmTitle = isEvmTarget
? S.of(context).swap_from_network(currentNetworkName)
: S.of(context).network_address_detected(destinationNetworkName);
final swapConfirmPrimary = isEvmTarget
? S.of(context).continue_to_swap
: S.of(context).swap_from_network(currentNetworkName);
await Navigator.of(context).push<void>(
CupertinoPageRoute(
builder: (pageContext) => NetworkDecisionPage(
title: swapConfirmTitle,
description: S.of(context).swap_from_network_description(
destinationNetworkName,
currentNetworkName,
),
destinationIconPath: destinationNetworkIcon,
currentIconPath: currentNetworkIcon,
primaryText: swapConfirmPrimary,
primaryIconPath: isEvmTarget ? null : "assets/new-ui/swap_arrows.svg",
onPrimary: () => _openSwap(pageContext, request, decision),
secondaryText: S.of(context).cancel,
),
),
);
return null;
}
final decisionTitle = isEvmTarget
? S.of(context).send_to_network(destinationNetworkName)
: S.of(context).network_address_detected(destinationNetworkName);
final selectedWallet = await Navigator.of(context).push<WalletInfo>(
CupertinoPageRoute(
builder: (pageContext) => NetworkDecisionPage(
title: decisionTitle,
description: decision.canSwap
? S.of(context).send_to_network_description(
destinationNetworkName,
currentNetworkName,
)
: S.of(context).swap_unavailable_switch_to_network(destinationNetworkName),
destinationIconPath: destinationNetworkIcon,
primaryText: S.of(context).switch_to_x_wallet(destinationNetworkName),
primaryIconPath: "assets/new-ui/wallet_filled.svg",
onPrimary: () => _pickDestinationWallet(pageContext, decision, destinationNetworkName),
secondaryText: decision.canSwap
? S.of(context).swap_from_network(currentNetworkName)
: S.of(context).cancel,
secondaryIconPath: decision.canSwap ? "assets/new-ui/swap_arrows.svg" : null,
onSecondary: decision.canSwap ? () => _openSwap(pageContext, request, decision) : null,
),
),
);
if (selectedWallet == null || !context.mounted) {
return null;
}
return _switchWallet(context, selectedWallet, request, decision, fallbackCurrency);
}
Future<void> _pickDestinationWallet(
BuildContext pageContext,
AnyPayCrossChainPayment decision,
String networkName,
) async {
WalletInfo? destinationWalletInfo;
if (decision.wallets.length > 1) {
destinationWalletInfo = await SwitchNetworkWalletPage.push(
context: pageContext,
networkName: networkName,
targetIconPath: symbolIconPathForWalletType(decision.targetWalletType) ?? "",
wallets: decision.wallets,
);
} else {
destinationWalletInfo = decision.wallets.first;
}
if (destinationWalletInfo == null || !pageContext.mounted) {
return;
}
Navigator.of(pageContext).pop(destinationWalletInfo);
}
Future<AnyPayFormFill?> _switchWallet(
BuildContext context,
WalletInfo walletInfo,
AnyPayRequest request,
AnyPayCrossChainPayment decision,
CryptoCurrency? fallbackCurrency,
) async {
if (request.isLightning) {
_enterLightningMode();
}
_isSwitchingWallet = true;
BuildContext? loadingSheetContext;
bool completedFlow = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!context.mounted || completedFlow) {
return;
}
showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (sheetContext) {
if (completedFlow) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (Navigator.canPop(sheetContext)) {
Navigator.of(sheetContext).pop();
}
});
return const SizedBox.shrink();
}
loadingSheetContext = sheetContext;
return LoadingBottomSheet(titleText: S.of(sheetContext).loading_your_wallet);
},
);
});
bool success = false;
try {
success = await anyPayService.switchWalletForPayment(
walletInfo,
chainId: decision.targetChainId,
);
} finally {
_isSwitchingWallet = false;
completedFlow = true;
if (loadingSheetContext != null &&
loadingSheetContext!.mounted &&
Navigator.canPop(loadingSheetContext!)) {
Navigator.of(loadingSheetContext!).pop();
}
loadingSheetContext = null;
}
if (!context.mounted) {
return null;
}
if (!success) {
_showError(context, S.of(context).network_switch_failed);
return null;
}
return AnyPayFormFill(
request: request,
token: decision.token,
fallbackCurrency: fallbackCurrency ?? request.detection.detectedCurrency,
amountOverride: decision.amountOverride,
);
}
Future<void> _openSwap(
BuildContext presentContext,
AnyPayRequest request,
AnyPayCrossChainPayment decision,
) async {
if (!presentContext.mounted) {
return;
}
if (request.hasContract && decision.token == null) {
_showError(presentContext, S.of(presentContext).unsupported_token_requested);
return;
}
final intent = anyPayService.buildSwapIntent(
request,
targetWalletType: decision.targetWalletType,
targetChainId: decision.targetChainId,
token: decision.token,
);
final page = NewSwapPage(
getIt.get<ExchangeViewModel>(),
authService,
getIt.get<AddressResolverService>(),
null,
walletSwitcherViewModel: walletSwitcherViewModel,
fromSend: SwapFromSendArgs.fromIntent(intent),
balanceViewModel: sendViewModel.balanceViewModel,
);
await Navigator.of(presentContext).push<void>(
CupertinoPageRoute(
builder: (context) => Material(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(bottom: false, child: page),
),
),
);
}
void _showError(BuildContext context, String message) {
if (!context.mounted) {
return;
}
showPopUp<void>(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: message,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
),
);
}
void _enterLightningMode() {
sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
}
int _currentEvmChainId() {
final wallet = sendViewModel.wallet;
return evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
}
}
@@ -5,31 +5,37 @@ import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:flutter/material.dart";
class SwapFromNetworkPage extends StatelessWidget {
const SwapFromNetworkPage({
class NetworkDecisionPage extends StatelessWidget {
const NetworkDecisionPage({
required this.title,
required this.primaryButtonText,
required this.destinationNetworkName,
required this.destinationNetworkIconPath,
required this.currentNetworkName,
required this.currentNetworkIconPath,
required this.onProceed,
required this.description,
required this.destinationIconPath,
required this.primaryText,
required this.onPrimary,
required this.secondaryText,
this.currentIconPath,
this.primaryIconPath,
this.secondaryIconPath,
this.onSecondary,
super.key,
this.primaryHasSwapIcon = false,
});
final String title;
final String primaryButtonText;
final bool primaryHasSwapIcon;
final String destinationNetworkName;
final String destinationNetworkIconPath;
final String currentNetworkName;
final String currentNetworkIconPath;
final VoidCallback onProceed;
final String description;
final String destinationIconPath;
final String? currentIconPath;
final String primaryText;
final VoidCallback onPrimary;
final String? primaryIconPath;
final String secondaryText;
final String? secondaryIconPath;
final VoidCallback? onSecondary;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final primaryIcon = primaryIconPath;
final secondaryIcon = secondaryIconPath;
return Material(
color: colors.surface,
child: SafeArea(
@@ -40,9 +46,6 @@ class SwapFromNetworkPage extends StatelessWidget {
leadingIcon: const Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).maybePop(),
trailingIcon: const Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => Navigator.of(context).maybePop(),
),
Expanded(
child: Center(
@@ -51,30 +54,11 @@ class SwapFromNetworkPage extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
CakeImageWidget(
imageUrl: currentNetworkIconPath,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(currentNetworkIconPath)
? colors.primary
: null,
),
Icon(Icons.arrow_forward, color: colors.primary, size: 28),
CakeImageWidget(
imageUrl: destinationNetworkIconPath,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationNetworkIconPath)
? colors.primary
: null,
),
],
ExcludeSemantics(
child: _DecisionHeader(
destinationIconPath: destinationIconPath,
currentIconPath: currentIconPath,
),
),
const SizedBox(height: 24),
Text(
@@ -88,10 +72,7 @@ class SwapFromNetworkPage extends StatelessWidget {
),
const SizedBox(height: 16),
Text(
S.of(context).swap_from_network_description(
destinationNetworkName,
currentNetworkName,
),
description,
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
@@ -109,23 +90,31 @@ class SwapFromNetworkPage extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
NewPrimaryButton(
onPressed: onProceed,
image: primaryHasSwapIcon
onPressed: onPrimary,
image: primaryIcon != null
? CakeImageWidget(
imageUrl: "assets/new-ui/swap_arrows.svg",
imageUrl: primaryIcon,
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
)
: null,
text: primaryButtonText,
text: primaryText,
color: colors.primary,
textColor: colors.onPrimary,
),
const SizedBox(height: 12),
NewPrimaryButton(
onPressed: () => Navigator.of(context).maybePop(),
text: S.of(context).cancel,
onPressed: onSecondary ?? () => Navigator.of(context).maybePop(),
image: secondaryIcon != null
? CakeImageWidget(
imageUrl: secondaryIcon,
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
)
: null,
text: secondaryText,
color: colors.surfaceContainer,
textColor: colors.primary,
),
@@ -138,3 +127,48 @@ class SwapFromNetworkPage extends StatelessWidget {
);
}
}
class _DecisionHeader extends StatelessWidget {
const _DecisionHeader({required this.destinationIconPath, this.currentIconPath});
final String destinationIconPath;
final String? currentIconPath;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final current = currentIconPath;
if (current == null) {
return CakeImageWidget(
imageUrl: destinationIconPath,
width: 75,
height: 75,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
);
}
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
CakeImageWidget(
imageUrl: current,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(current) ? colors.primary : null,
),
Icon(Icons.arrow_forward, color: colors.primary, size: 28),
CakeImageWidget(
imageUrl: destinationIconPath,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
),
],
);
}
}
@@ -1,46 +1,14 @@
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import "package:flutter/material.dart";
class RecipientNetworkSelector extends StatelessWidget {
const RecipientNetworkSelector({
super.key,
required this.wallet,
required this.onTap,
});
final WalletBase wallet;
final void Function(ChainInfo currentChain) onTap;
@override
Widget build(BuildContext context) {
if (evm == null) return const SizedBox.shrink();
final currentChainId =
evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
final currentChain = evm!.getChainInfoByChainId(currentChainId);
if (currentChain == null) {
printV("RecipientNetworkSelector: no chain info for chainId $currentChainId");
return const SizedBox.shrink();
}
return _RecipientNetworkRow(
networkName: currentChain.name,
networkIconPath: symbolIconPathForWalletType(wallet.type) ?? "",
onTap: () => onTap(currentChain),
);
}
}
class _RecipientNetworkRow extends StatelessWidget {
const _RecipientNetworkRow({
required this.networkName,
required this.networkIconPath,
required this.onTap,
super.key,
});
final String networkName;
@@ -50,50 +18,56 @@ class _RecipientNetworkRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
S.of(context).network_prefix_on,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
),
const SizedBox(width: 12),
CakeImageWidget(
imageUrl: networkIconPath,
width: 16,
height: 16,
fit: BoxFit.cover,
color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
),
const SizedBox(width: 8),
Text(
networkName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,
letterSpacing: -0.07,
color: colors.primary,
return Semantics(
button: true,
label: "${S.of(context).network_prefix_on} $networkName",
child: GestureDetector(
onTap: onTap,
child: ExcludeSemantics(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
S.of(context).network_prefix_on,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
),
const SizedBox(width: 12),
CakeImageWidget(
imageUrl: networkIconPath,
width: 16,
height: 16,
fit: BoxFit.cover,
color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
),
const SizedBox(width: 8),
Text(
networkName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,
letterSpacing: -0.07,
color: colors.primary,
),
),
const SizedBox(width: 8),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: colors.surfaceContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.keyboard_arrow_down,
size: 18,
color: colors.onSurfaceVariant,
),
),
],
),
const SizedBox(width: 8),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: colors.surfaceContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.keyboard_arrow_down,
size: 18,
color: colors.onSurfaceVariant,
),
),
],
),
),
);
}
@@ -1,122 +0,0 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:flutter/material.dart";
class SendToNetworkPage extends StatelessWidget {
const SendToNetworkPage({
required this.title,
required this.destinationNetworkName,
required this.destinationIconPath,
required this.currentNetworkName,
required this.onSwitchWallet,
required this.onSwap,
super.key,
});
final String title;
final String destinationNetworkName;
final String destinationIconPath;
final String currentNetworkName;
final VoidCallback onSwitchWallet;
final VoidCallback onSwap;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Material(
color: colors.surface,
child: SafeArea(
child: Column(
children: [
ModalTopBar(
title: "",
leadingIcon: const Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).maybePop(),
trailingIcon: const Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => Navigator.of(context).maybePop(),
),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CakeImageWidget(
imageUrl: destinationIconPath,
width: 75,
height: 75,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
),
const SizedBox(height: 24),
Text(
title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: -0.1,
),
),
const SizedBox(height: 16),
Text(
S.of(context).send_to_network_description(
destinationNetworkName,
currentNetworkName,
),
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NewPrimaryButton(
onPressed: onSwitchWallet,
image: CakeImageWidget(
imageUrl: "assets/new-ui/wallet_filled.svg",
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
),
text: S.of(context).switch_to_x_wallet(destinationNetworkName),
color: colors.primary,
textColor: colors.onPrimary,
),
const SizedBox(height: 12),
NewPrimaryButton(
onPressed: onSwap,
image: CakeImageWidget(
imageUrl: "assets/new-ui/swap_arrows.svg",
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
),
text: S.of(context).swap_from_network(currentNetworkName),
color: colors.surfaceContainer,
textColor: colors.primary,
),
],
),
),
],
),
),
);
}
}
@@ -162,7 +162,8 @@ class CoinActionRow extends StatelessWidget {
if (code == null || code.isEmpty) return;
late final PaymentRequest req;
PaymentRequest? req;
String? rawInput;
var unspentCoinType = UnspentCoinType.any;
if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) {
unspentCoinType = UnspentCoinType.lightning;
@@ -172,8 +173,6 @@ class CoinActionRow extends StatelessWidget {
unspentCoinType = UnspentCoinType.lightning;
final amount = (await LNURL.getPayRequestAmount(code))?.toString() ?? "0";
req = PaymentRequest(code, amount, "", "", "");
} else if (OpenCryptoPayService.isOpenCryptoPayQR(code)) {
req = PaymentRequest(code, "", "", "", "");
} else if (Uri.tryParse(code)?.scheme == "wc") {
if (!isWalletConnectCompatibleChain(walletType)) {
showPopUp<void>(
@@ -190,19 +189,19 @@ class CoinActionRow extends StatelessWidget {
Navigator.of(context)
.pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
return;
} else if (["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
} else if (!OpenCryptoPayService.isOpenCryptoPayQR(code) &&
["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
Navigator.of(context).pushNamed(Routes.newNode,
arguments: {"editingNode": Node.fromUri(Uri.parse(code), walletType)});
return;
} else {
final uri = Uri.tryParse(code);
if (uri == null) return;
req = PaymentRequest.fromUri(uri);
rawInput = code;
}
final sendPage = getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: req,
initialRawInput: rawInput,
unspentCoinType: unspentCoinType,
),
);
@@ -18,6 +18,22 @@ class AnyPaySwapFooter extends StatelessWidget {
final textTheme = Theme.of(context).textTheme;
return Observer(
builder: (_) {
if (exchangeViewModel.noProviderForPair) {
return Container(
alignment: Alignment.center,
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
border: Border.all(color: colors.error),
),
child: Text(
S.of(context).no_providers_available,
style: textTheme.bodyMedium?.copyWith(color: colors.error, letterSpacing: -0.07),
),
);
}
if (exchangeViewModel.depositAmount.isEmpty) {
return const SizedBox.shrink();
}
+157 -205
View File
@@ -8,6 +8,7 @@ import "package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart";
import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/refund_address_modal.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/swap_address_selection_modal.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/swap_source_selector.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/utils/decimal_input_formatter.dart";
import "package:cake_wallet/utils/permission_handler.dart";
@@ -82,10 +83,14 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
final fiatAmountController = TextEditingController();
final amountFocusNode = FocusNode();
final memoController = TextEditingController();
final _amountFieldKey = GlobalKey<FormFieldState<String>>();
mobx.ReactionDisposer? _memoReactionDisposer;
@override
void initState() {
amountController
.addListener(() => _amountFieldKey.currentState?.didChange(amountController.text));
if (widget.isReceiverCard) {
memoController.text = widget.exchangeViewModel.receiveAddressExtraId;
@@ -145,95 +150,13 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
final colors = Theme.of(context).colorScheme;
if (widget.sourceSelectorMode) {
final available = _sourceBalance()?.amount.split(" ").first ?? "—";
return Column(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: _presentCurrencyPicker,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
CakeImageWidget(
imageUrl: widget.currency.iconPath ?? "",
width: 24,
height: 24,
),
const SizedBox(width: 8),
Text(
currencyToShow,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(letterSpacing: -0.08),
),
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIconPath,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
const Spacer(),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/wallet_filled.svg",
width: 16,
height: 16,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
const SizedBox(width: 4),
Flexible(
child: Text(
widget.walletName ?? "",
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.onSurfaceVariant,
),
),
),
],
),
),
const SizedBox(width: 8),
Text(
"${S.of(context).avl}: $available",
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.primary,
),
),
],
),
),
],
return SwapSourceSelector(
currencyIconPath: widget.currency.iconPath ?? "",
currencyLabel: currencyToShow,
chainIconPath: chainIconPath,
walletName: widget.walletName,
availableBalance: _sourceBalance()?.amount.split(" ").first ?? "—",
onTap: _presentCurrencyPicker,
);
}
@@ -260,139 +183,168 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
child: Column(
spacing: 12,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: amountFocusNode.requestFocus,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 4,
FormField<String>(
key: _amountFieldKey,
initialValue: amountController.text,
validator: _fiatInputMode ? null : widget.currencyValueValidator,
builder: (state) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: amountFocusNode.requestFocus,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: IntrinsicWidth(
child: Observer(
builder: (_) {
final showFetching = !widget.isReceiverCard &&
widget.exchangeViewModel.isFixedRateMode &&
widget.exchangeViewModel.receiveAmount.isNotEmpty &&
widget.exchangeViewModel.depositAmount.isEmpty;
return TextFormField(
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: !widget.useBaseUnit,
Expanded(
child: Row(
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: IntrinsicWidth(
child: Observer(
builder: (_) {
final showFetching = !widget.isReceiverCard &&
widget.exchangeViewModel.isFixedRateMode &&
widget.exchangeViewModel.receiveAmount.isNotEmpty &&
widget.exchangeViewModel.depositAmount.isEmpty;
return TextField(
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: !widget.useBaseUnit,
),
onChanged: state.didChange,
controller: _fiatInputMode
? fiatAmountController
: amountController,
focusNode: amountFocusNode,
style: TextStyle(
fontSize: 28,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
isDense: true,
hintText: showFetching ? S.of(context).fetching : "0",
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
inputFormatters: <TextInputFormatter>[
DecimalInputFormatter(
maxDecimals: widget.useBaseUnit
? 0
: widget.currency.decimals,
),
],
);
},
),
validator:
_fiatInputMode ? null : widget.currencyValueValidator,
controller:
_fiatInputMode ? fiatAmountController : amountController,
focusNode: amountFocusNode,
style: TextStyle(
fontSize: 28,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
isDense: true,
hintText: showFetching ? S.of(context).fetching : "0",
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
inputFormatters: <TextInputFormatter>[
DecimalInputFormatter(
maxDecimals:
widget.useBaseUnit ? 0 : widget.currency.decimals,
),
),
if (_fiatInputMode)
Center(
child: Text(
widget.exchangeViewModel.fiat.title,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
},
),
),
),
],
),
),
if (_fiatInputMode)
Center(
child: Text(
widget.exchangeViewModel.fiat.title,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
GestureDetector(
onTap: _presentCurrencyPicker,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(999999),
),
child: Padding(
padding:
const EdgeInsets.only(top: 4, bottom: 4, left: 4, right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CakeImageWidget(
imageUrl: widget.currency.iconPath ?? "",
width: 28,
height: 28,
),
const SizedBox(width: 10),
Text(
currencyToShow,
textAlign: TextAlign.center,
),
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIconPath,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
const SizedBox(width: 6),
] else
const SizedBox(width: 10),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999999999),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: Padding(
padding: const EdgeInsets.all(4),
child: RotatedBox(
quarterTurns: 2,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg",
width: 4,
height: 4,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
),
),
const SizedBox(width: 4),
],
),
),
),
),
],
),
),
GestureDetector(
onTap: _presentCurrencyPicker,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(999999),
),
child: Padding(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 4, right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CakeImageWidget(
imageUrl: widget.currency.iconPath ?? "",
width: 28,
height: 28,
),
const SizedBox(width: 10),
Text(
currencyToShow,
textAlign: TextAlign.center,
),
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIconPath,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
const SizedBox(width: 6),
] else
const SizedBox(width: 10),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999999999),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: Padding(
padding: const EdgeInsets.all(4),
child: RotatedBox(
quarterTurns: 2,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg",
width: 4,
height: 4,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
),
),
const SizedBox(width: 4),
],
if (state.hasError)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Semantics(
container: true,
liveRegion: true,
label: "${S.of(context).amount}${state.errorText!}",
excludeSemantics: true,
child: Text(
state.errorText!,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.error,
),
),
),
),
),
],
),
),
@@ -1,4 +1,4 @@
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
@@ -8,13 +8,18 @@ class SwapFromSendArgs {
required this.recipientAddress,
required this.receiveCurrency,
required this.targetWalletType,
this.depositBalanceByAsset,
this.receiveAmount,
});
factory SwapFromSendArgs.fromIntent(AnyPaySwapIntent intent) => SwapFromSendArgs(
recipientAddress: intent.recipientAddress,
receiveCurrency: intent.receiveCurrency,
targetWalletType: intent.targetWalletType,
receiveAmount: intent.receiveAmount,
);
final String recipientAddress;
final CryptoCurrency receiveCurrency;
final WalletType targetWalletType;
final Map<CryptoCurrency, CurrencyPickerBalance>? depositBalanceByAsset;
final Money? receiveAmount;
}
@@ -0,0 +1,118 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:flutter/material.dart";
class SwapSourceSelector extends StatelessWidget {
const SwapSourceSelector({
required this.currencyIconPath,
required this.currencyLabel,
required this.availableBalance,
required this.onTap,
this.chainIconPath,
this.walletName,
super.key,
});
final String currencyIconPath;
final String currencyLabel;
final String availableBalance;
final VoidCallback onTap;
final String? chainIconPath;
final String? walletName;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final chainIcon = chainIconPath;
return Column(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: onTap,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
CakeImageWidget(
imageUrl: currencyIconPath,
width: 24,
height: 24,
),
const SizedBox(width: 8),
Text(
currencyLabel,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(letterSpacing: -0.08),
),
if (chainIcon != null && chainIcon.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIcon,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
const Spacer(),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/wallet_filled.svg",
width: 16,
height: 16,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
const SizedBox(width: 4),
Flexible(
child: Text(
walletName ?? "",
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.onSurfaceVariant,
),
),
),
],
),
),
const SizedBox(width: 8),
Text(
S.of(context).available_balance_short(availableBalance),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.primary,
),
),
],
),
),
],
);
}
}
+4 -5
View File
@@ -149,7 +149,6 @@ import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_mode
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
@@ -445,6 +444,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.send:
final args = settings.arguments as Map<String, dynamic>?;
final initialPaymentRequest = args?['paymentRequest'] as PaymentRequest?;
final initialRawInput = args?["rawLink"] as String?;
final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?;
return handleRouteWithPlatformAwareness(
@@ -452,6 +452,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
child: getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: initialPaymentRequest,
initialRawInput: initialRawInput,
unspentCoinType: coinTypeToSpendFrom ?? UnspentCoinType.any),
),
),
@@ -473,7 +474,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.newReceivePage:
if (FeatureFlag.hasNewUi) {
return handleRouteWithPlatformAwareness(
(context) => Material(child: getIt.get<NewReceivePage>(param1: false, param2: null)),
(context) => Material(child: getIt.get<NewReceivePage>(param1: false, param2: null)),
settings: settings,
);
}
@@ -835,9 +836,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
final useTestnet = args['useTestnet'] as bool;
final toggleTestnet = args['toggleTestnet'] as Function(bool? val);
final zcashNetwork = args['zcashNetwork'] as int? ?? ZcashNetworkType.mainnet;
final setZcashNetwork =
args['setZcashNetwork'] as void Function(int network)? ?? (_) {};
final restoredWallet = args['restoredWallet'] as RestoredWallet?;
final setZcashNetwork = args['setZcashNetwork'] as void Function(int network)? ?? (_) {};
final viewModelParam = {'type': type, 'isPow': false};
+25
View File
@@ -125,6 +125,31 @@ class PaymentRequest {
);
}
PaymentRequest copyWith({
String? address,
String? amount,
String? note,
String? scheme,
String? pjUri,
String? callbackUrl,
String? callbackMessage,
String? contractAddress,
int? chainId,
String? rawTokenAmount,
}) =>
PaymentRequest(
address ?? this.address,
amount ?? this.amount,
note ?? this.note,
scheme ?? this.scheme,
pjUri ?? this.pjUri,
callbackUrl: callbackUrl ?? this.callbackUrl,
callbackMessage: callbackMessage ?? this.callbackMessage,
contractAddress: contractAddress ?? this.contractAddress,
chainId: chainId ?? this.chainId,
rawTokenAmount: rawTokenAmount ?? this.rawTokenAmount,
);
final String address;
final String amount;
final String note;
@@ -2,12 +2,10 @@ import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:developer';
import 'dart:math' show min;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/create_trade_result.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
@@ -415,6 +413,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@observable
bool tradeStarted = false;
@observable
bool noProviderForPair = false;
@observable
bool isSendFromExternal = false;
@@ -980,6 +981,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
bestRate = _sortedAvailableProviders.keys.first;
bestRateProvider = _sortedAvailableProviders.values.first;
}
noProviderForPair = _sortedAvailableProviders.isEmpty;
}
@action
@@ -1603,24 +1605,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
}
String? _addressTypeValidation(String refundAddress, String receiveAddress) {
final isRefundAddressSP =
RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(refundAddress);
if (isRefundAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_refund}';
final isReceiveAddressSP =
RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(receiveAddress);
if (isReceiveAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_receive}';
final isRefundAddressMWEB = RegExp(AddressValidator.mWebAddressPattern).hasMatch(refundAddress);
if (isRefundAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_refund}';
final isReceiveAddressMWEB =
RegExp(AddressValidator.mWebAddressPattern).hasMatch(receiveAddress);
if (isReceiveAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_receive}';
return null;
}
void _defineIsReceiveAmountEditable() {
/*if ((provider is ChangeNowExchangeProvider)
&&(depositCurrency == CryptoCurrency.xmr)
+21 -2
View File
@@ -25,6 +25,10 @@ class LinkViewModel {
Uri? currentLink;
bool readyToOpenPage = false;
final StreamController<Uri> _incomingPaymentLinksController = StreamController<Uri>.broadcast();
Stream<Uri> get incomingPaymentLinks => _incomingPaymentLinksController.stream;
bool get _isValidPaymentUri => currentLink?.path.isNotEmpty ?? false;
bool get isWalletConnectLink => currentLink?.authority == 'wc';
bool get isNanoGptLink => currentLink?.scheme == 'nano-gpt';
@@ -96,7 +100,10 @@ class LinkViewModel {
}
if (_isValidPaymentUri) {
return {"paymentRequest": PaymentRequest.fromUri(currentLink)};
return {
"paymentRequest": PaymentRequest.fromUri(currentLink),
"rawLink": currentLink.toString(),
};
}
return null;
@@ -132,8 +139,20 @@ class LinkViewModel {
return;
}
// Prevent navigating to the same route again.
if (route == Routes.send &&
!isQuickActionLink &&
currentLink != null &&
_incomingPaymentLinksController.hasListener) {
final link = currentLink!;
currentLink = null;
_incomingPaymentLinksController.add(link);
return;
}
// Prevent navigating to the same route again and clear the link so a later auth
// event cannot refire it.
if (appStore.currentRouteName == route) {
currentLink = null;
return;
}
+5
View File
@@ -1730,6 +1730,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
}
@action
void applyAnyPayCurrency(CryptoCurrency currency) {
selectedCryptoCurrency = currency;
}
String _decodeMethodSelector(String s) =>
(s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
}
+3
View File
@@ -93,6 +93,7 @@
"available": "متاح",
"available_balance": "الرصيد المتاح",
"available_balance_description": "يشير “Available Balance” أو “Confirmed Balance” إلى الأموال التي يمكن إنفاقها فورًا. إذا ظهرت الأموال في الرصيد السفلي ولكن ليس في الرصيد العلوي، فعليك الانتظار بضع دقائق حتى تحصل الأموال الواردة على المزيد من تأكيدات الشبكة. بعد حصولها على المزيد من التأكيدات، ستصبح قابلة للإنفاق.",
"available_balance_short": "المبلغ الإجمالي: ${amount}",
"avg_savings": "متوسط التوفير",
"avl": "AVL",
"awaitDAppProcessing": "يرجى الانتظار حتى ينتهي الـ dApp من المعالجة.",
@@ -1149,6 +1150,8 @@
"swap_id": "معرّف المبادلة",
"swap_provider_initial_desc": "اختر نوع مزوّد المبادلة الذي تفضّل استخدامه عند إجراء المبادلة داخل التطبيق",
"swap_providers": "مزودو المبادلة",
"swap_unavailable_needs_network_wallet": "لا يمكن لهذا النوع من العنوان تلقي الأموال من المبادلة. أنت بحاجة إلى محفظة ${network} لإرسالها إلى هذا العنوان.",
"swap_unavailable_switch_to_network": "لا يمكن لهذا النوع من العنوان تلقي الأموال من المبادلة. قم بالتبديل إلى محفظة ${network} الخاصة بك للمتابعة.",
"sweeping_wallet": "محفظة السحب الشامل",
"sweeping_wallet_alert": "لن يستغرق هذا وقتًا طويلًا. لا تغادر هذه الشاشة وإلا فقد تُفقد الأموال المُسحوبة.",
"swipe_to_bridge": "اسحب لإجراء الجسر",
+3
View File
@@ -93,6 +93,7 @@
"available": "Налично",
"available_balance": "Наличен баланс",
"available_balance_description": "„Наличният баланс“ или „Потвърденият баланс“ са средства, които могат да бъдат изразходвани веднага. Ако средствата се появяват в долния баланс, но не и в горния, трябва да изчакате няколко минути, докато входящите средства получат повече мрежови потвърждения. След като получат повече потвърждения, те ще могат да бъдат изразходвани.",
"available_balance_short": "Ср.: ${amount}",
"avg_savings": "Средни спестявания",
"avl": "Avl",
"awaitDAppProcessing": "Моля, изчакайте dApp да завърши обработката.",
@@ -1149,6 +1150,8 @@
"swap_id": "ID на суап",
"swap_provider_initial_desc": "Изберете типа доставчик за суап, който предпочитате да използвате при суапване в",
"swap_providers": "Доставчици за суап",
"swap_unavailable_needs_network_wallet": "Този тип адрес не може да получава средства от суап. Имате нужда от ${network} портфейл, за да изпратите на този адрес.",
"swap_unavailable_switch_to_network": "Този тип адрес не може да получава средства от суап. Превключете към вашия ${network} портфейл, за да продължите.",
"sweeping_wallet": "Изчистване на портфейл",
"sweeping_wallet_alert": "Това не би трябвало да отнеме много време. НЕ НАПУСКАЙТЕ ТОЗИ ЕКРАН, ИНАЧЕ ИЗМЕТЕНИТЕ СРЕДСТВА МОГАТ ДА БЪДАТ ЗАГУБЕНИ.",
"swipe_to_bridge": "Плъзнете, за да прехвърлите",
+3
View File
@@ -93,6 +93,7 @@
"available": "Dostupné",
"available_balance": "Dostupný zůstatek",
"available_balance_description": "„Dostupný zůstatek“ nebo „Potvrzený zůstatek“ jsou prostředky, které lze okamžitě utratit. Pokud se prostředky zobrazují ve spodním zůstatku, ale ne v horním, musíte několik minut počkat, než příchozí prostředky získají více síťových potvrzení. Jakmile získají více potvrzení, bude možné je utratit.",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "Prům. úspora",
"avl": "Avl",
"awaitDAppProcessing": "Počkejte prosím, až dApp dokončí zpracování.",
@@ -1149,6 +1150,8 @@
"swap_id": "ID směny",
"swap_provider_initial_desc": "Vyberte typ poskytovatele swapu, kterého chcete používat při swapování v",
"swap_providers": "Poskytovatelé swapu",
"swap_unavailable_needs_network_wallet": "Tento typ adresy nemůže přijímat prostředky ze swapu. K odeslání na tuto adresu potřebujete peněženku ${network}.",
"swap_unavailable_switch_to_network": "Tento typ adresy nemůže přijímat prostředky ze swapu. Chcete-li pokračovat, přepněte na svou peněženku ${network}.",
"sweeping_wallet": "Peněženka pro sweep",
"sweeping_wallet_alert": "To by nemělo trvat dlouho. NEOPUŠTĚJTE TUTO OBRAZOVKU, JINAK MOHOU BÝT PŘESUNUTÉ PROSTŘEDKY ZTRACENY.",
"swipe_to_bridge": "Přemostění přejetím prstem",
+3
View File
@@ -96,6 +96,7 @@
"available": "Verfügbar",
"available_balance": "Verfügbares Guthaben",
"available_balance_description": "Der „Verfügbare Kontostand“ bzw. „Bestätigter Kontostand“ sind Guthaben, die sofort ausgegeben werden können. Wenn Guthaben im unteren Kontostand angezeigt wird, aber nicht im oberen, müssen Sie ein paar Minuten warten, bis die eingehenden Gelder weitere Netzwerkbestätigungen erhalten. Sobald sie mehr Bestätigungen haben, können sie ausgegeben werden.",
"available_balance_short": "Durchschnitt: ${amount}",
"avg_savings": "Durchschn. Ersparnis",
"avl": "Avl",
"awaitDAppProcessing": "Bitte warten Sie, bis die dApp die Verarbeitung abgeschlossen hat.",
@@ -1183,6 +1184,8 @@
"swap_provider_initial_desc": "Wählen Sie die Art des Swap-Anbieters aus, die Sie beim Tauschen bevorzugen.",
"swap_providers": "Swap-Anbieter",
"swap_reverse_direction": "Swap-Richtung umkehren",
"swap_unavailable_needs_network_wallet": "Dieser Adresstyp kann keine Gelder von einem Swap erhalten. Sie benötigen ein ${network}-Wallet, um es an diese Adresse zu senden.",
"swap_unavailable_switch_to_network": "Dieser Adresstyp kann keine Gelder von einem Swap erhalten. Wechseln Sie zu Ihrem ${network}-Wallet, um fortzufahren.",
"sweeping_wallet": "Wallet leeren",
"sweeping_wallet_alert": "Das sollte nicht lange dauern. Verlassen Sie diesen Bildschirm nicht, sonst können die geleerten Guthaben verloren gehen.",
"swipe_to_bridge": "Zum Bridging wischen",
+4 -1
View File
@@ -96,6 +96,7 @@
"available": "Available",
"available_balance": "Available Balance",
"available_balance_description": "The “Available Balance” or “Confirmed Balance” are funds that can be spent immediately. If funds appear in the lower balance but not the top balance, then you must wait a few minutes for the incoming funds to get more network confirmations. After they get more confirmations, they will be spendable.",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "Avg. Savings",
"avl": "Avl",
"awaitDAppProcessing": "Kindly wait for the dApp to finish processing.",
@@ -1180,6 +1181,8 @@
"swap_provider_initial_desc": "Select the type of swap provider you prefer to use when swapping in",
"swap_providers": "Swap providers",
"swap_reverse_direction": "Reverse swap direction",
"swap_unavailable_needs_network_wallet": "This address type can't receive funds from a swap. You need a ${network} wallet to send to this address.",
"swap_unavailable_switch_to_network": "This address type can't receive funds from a swap. Switch to your ${network} wallet to continue.",
"sweeping_wallet": "Sweeping wallet",
"sweeping_wallet_alert": "This shouldn’t take long. DO NOT LEAVE THIS SCREEN OR THE SWEPT FUNDS MAY BE LOST.",
"swipe_to_bridge": "Swipe to bridge",
@@ -1514,4 +1517,4 @@
"zcash_network_upgrade_desc_1": "Zcash has undergone a network upgrade called Ironwood that requires migration of funds before they can be spent. This migration is automatic, privacy-preserving, and starts as soon as you open your Zcash wallet.",
"zcash_network_upgrade_desc_2": "When the migration finishes, your Available Balance will return to normal, and you can continue to send and receive Zcash as usual.",
"zcash_network_upgrade_desc_3": "Please allow your Zcash wallet to finishing syncing, and then leave the app open until migration finishes which can take a few minutes depending on the balance."
}
}
+3
View File
@@ -93,6 +93,7 @@
"available": "Disponible",
"available_balance": "Saldo disponible",
"available_balance_description": "El “Saldo Disponible” o “Saldo Confirmado” son fondos que pueden gastarse de inmediato. Si los fondos aparecen en el saldo inferior pero no en el saldo superior, deberás esperar unos minutos a que los fondos entrantes obtengan más confirmaciones de la red. Una vez que tengan más confirmaciones, podrás gastarlos.",
"available_balance_short": "Valor medio: ${amount}",
"avg_savings": "Ahorro prom.",
"avl": "Avl",
"awaitDAppProcessing": "Espere a que la dApp termine de procesar.",
@@ -1151,6 +1152,8 @@
"swap_id": "ID de intercambio",
"swap_provider_initial_desc": "Seleccione el tipo de proveedor de intercambio que prefiere usar al intercambiar",
"swap_providers": "Proveedores de intercambio",
"swap_unavailable_needs_network_wallet": "Este tipo de dirección no puede recibir fondos de un swap. Necesitas una billetera ${network} para enviar a esta dirección.",
"swap_unavailable_switch_to_network": "Este tipo de dirección no puede recibir fondos de un swap. Cambie a su billetera ${network} para continuar.",
"sweeping_wallet": "Cartera de barrido",
"sweeping_wallet_alert": "Esto no debería tardar mucho. NO SALGAS DE ESTA PANTALLA O LOS FONDOS BARRIDOS PODRÍAN PERDERSE.",
"swipe_to_bridge": "Desliza para transferir entre cadenas",
+3
View File
@@ -93,6 +93,7 @@
"available": "در دسترس",
"available_balance": "موجودی در دسترس",
"available_balance_description": "«موجودی قابل‌دسترس» یا «موجودی تأییدشده» وجوهی هستند که می‌توان بلافاصله خرج کرد.\nاگر وجوه در موجودی پایین‌تر ظاهر شوند اما در موجودی بالا نباشند، باید چند دقیقه صبر کنید تا وجوه ورودی تأییدهای شبکه بیشتری دریافت کنند.\nپس از دریافت تأییدهای بیشتر، قابل خرج کردن خواهند بود.",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "میانگین صرفه‌جویی",
"avl": "Avl",
"awaitDAppProcessing": "لطفاً منتظر بمانید تا پردازش dApp به پایان برسد.",
@@ -1149,6 +1150,8 @@
"swap_id": "شناسهٔ سواپ",
"swap_provider_initial_desc": "نوع ارائه‌دهندهٔ سواپ را که ترجیح می‌دهید هنگام سواپ از آن استفاده کنید، انتخاب کنید",
"swap_providers": "ارائه‌دهندگان سواپ",
"swap_unavailable_needs_network_wallet": "این نوع آدرس نمی‌تواند وجوهی را از مبادله دریافت کند. برای ارسال به این آدرس به یک کیف پول ${network} نیاز دارید.",
"swap_unavailable_switch_to_network": "این نوع آدرس نمی‌تواند وجوهی را از مبادله دریافت کند. برای ادامه به کیف پول ${network} خود بروید.",
"sweeping_wallet": "در حال انتقال موجودی کیف پول",
"sweeping_wallet_alert": "این کار نباید طول بکشد.\nاین صفحه را ترک نکنید، وگرنه ممکن است وجوه انتقال‌یافته از دست برود.",
"swipe_to_bridge": "برای پل‌زدن، انگشت خود را بکشید",
+3
View File
@@ -93,6 +93,7 @@
"available": "Disponible",
"available_balance": "Solde disponible",
"available_balance_description": "Le « Solde disponible » ou le « Solde confirmé » correspond aux fonds que vous pouvez dépenser immédiatement. Si des fonds apparaissent dans le solde inférieur mais pas dans le solde supérieur, vous devez attendre quelques minutes pour que les fonds entrants obtiennent davantage de confirmations sur le réseau. Une fois qu’ils auront reçu plus de confirmations, ils pourront être dépensés.",
"available_balance_short": "Valeur moyenne : ${amount}",
"avg_savings": "Économies moy.",
"avl": "Avl",
"awaitDAppProcessing": "Veuillez patienter pendant que la dApp termine le traitement.",
@@ -1149,6 +1150,8 @@
"swap_id": "ID d’échange",
"swap_provider_initial_desc": "Sélectionnez le type de fournisseur de swap que vous préférez utiliser lors d’un swap",
"swap_providers": "Fournisseurs de swap",
"swap_unavailable_needs_network_wallet": "Ce type d'adresse ne peut pas recevoir de fonds provenant d'un swap. Vous avez besoin d'un portefeuille ${network} pour envoyer à cette adresse.",
"swap_unavailable_switch_to_network": "Ce type d'adresse ne peut pas recevoir de fonds provenant d'un swap. Basculez vers votre portefeuille ${network} pour continuer.",
"sweeping_wallet": "Balayage du portefeuille",
"sweeping_wallet_alert": "Cela ne devrait pas prendre longtemps. NE QUITTEZ PAS CET ÉCRAN, SINON LES FONDS BALAYÉS RISQUENT D’ÊTRE PERDUS.",
"swipe_to_bridge": "Glissez pour transférer (bridge)",
+3
View File
@@ -93,6 +93,7 @@
"available": "Oĩva",
"available_balance": "Saldo ojepurukuaáva",
"available_balance_description": "Pe “Available Balance” térã “Confirmed Balance” ha'e umi fondo ikatúva reipuru pya'e. Umi fondo ojehecháramo pe saldo yvýpe ha ndaha'éi pe saldo yvate gotyo, upéicharõ eha'arõ sapy'ami hag̃ua umi fondo og̃uahẽva ohupytyve hag̃ua confirmación de red. Oguerekóvo hetave confirmación, ikatúta reipuru.",
"available_balance_short": "Avl: ${amount}.",
"avg_savings": "Ñongatu mbytegua",
"avl": "Avl",
"awaitDAppProcessing": "Eha'arõmi pe dApp omohu'ã hag̃ua hembiapo.",
@@ -1155,6 +1156,8 @@
"swap_id": "ID ñemoambue rehegua",
"swap_provider_initial_desc": "Eiporavo mba’eichagua swap puruhára reipuruséva rembojopyru hag̃ua swap jave.",
"swap_providers": "Proveedor-kuéra intercambio rehegua",
"swap_unavailable_needs_network_wallet": "Ko tipo dirección ndikatúi ohupyty fondo peteî swap-gui. Tekotevẽ peteĩ ${network} billetera emondo hag̃ua ko dirección-pe.",
"swap_unavailable_switch_to_network": "Ko tipo dirección ndikatúi ohupyty fondo peteî swap-gui. Embohasa nde ${network} billetera-pe eñepyrũ hag̃ua.",
"sweeping_wallet": "Billetera ñemopotĩ",
"sweeping_wallet_alert": "Kóva ndoguerahámo’ãi heta aravo. ANI REHEJA KO PANTALLA, térã umi fondo oñemopotĩva ikatu okañy.",
"swipe_to_bridge": "Embosyryry embridge hag̃ua",
+3
View File
@@ -93,6 +93,7 @@
"available": "Akwai",
"available_balance": "Ma’aunin da ake da shi",
"available_balance_description": "“Ma'aunin da ake da shi” ko “Tabbataccen Ma'auni” su ne kuɗaɗen da za a iya kashewa nan take. Idan kuɗaɗe sun bayyana a ƙananan ma'auni amma ba a babban ma'auni ba, to dole ne ku jira 'yan mintoci kaɗan domin kuɗaɗen da ke shigowa su sami ƙarin tabbaci daga hanyar sadarwa. Bayan sun sami ƙarin tabbaci, za a iya kashe su.",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "Matsakaitan Ajiya",
"avl": "Avl",
"awaitDAppProcessing": "Da fatan za a jira dApp ya kammala sarrafawa.",
@@ -1151,6 +1152,8 @@
"swap_id": "ID na Musanya",
"swap_provider_initial_desc": "Zaɓi nau'in mai ba da sabis na musanyawa da ka fi so ka yi amfani da shi lokacin musanyawa",
"swap_providers": "Masu samar da Swap",
"swap_unavailable_needs_network_wallet": "Wannan nau'in adireshin ba zai iya karɓar kuɗi daga musanyawa ba. Kuna buƙatar walat ɗin ${network} don aikawa zuwa wannan adireshin.",
"swap_unavailable_switch_to_network": "Wannan nau'in adireshin ba zai iya karɓar kuɗi daga musanyawa ba. Canja zuwa jakar kuɗin ku na ${network} don ci gaba.",
"sweeping_wallet": "Ana kwashe kuɗi daga walat",
"sweeping_wallet_alert": "Wannan bai kamata ya ɗauki lokaci mai tsawo ba. KADA KA BAR WANNAN ALLON KO KUDADEN DA AKA KWASO NA IYA ɓACEWA.",
"swipe_to_bridge": "Zame don gada",
+3
View File
@@ -93,6 +93,7 @@
"available": "उपलब्ध",
"available_balance": "उपलब्ध शेष राशि",
"available_balance_description": "“Available Balance” या “Confirmed Balance” वे धनराशि हैं जिन्हें तुरंत खर्च किया जा सकता है। यदि फंड नीचे वाले बैलेंस में दिखाई देते हैं, लेकिन ऊपर वाले बैलेंस में नहीं, तो आने वाले फंड को नेटवर्क पर और अधिक पुष्टिकरण मिलने के लिए आपको कुछ मिनट इंतज़ार करना होगा। अधिक पुष्टिकरण मिलने के बाद, वे खर्च करने योग्य हो जाएंगे।",
"available_balance_short": "एवीएल: ${amount}",
"avg_savings": "औसत बचत",
"avl": "अवल",
"awaitDAppProcessing": "कृपया dApp की प्रोसेसिंग पूरी होने तक प्रतीक्षा करें।",
@@ -1151,6 +1152,8 @@
"swap_id": "स्वैप आईडी",
"swap_provider_initial_desc": "स्वैप करते समय आप जिस प्रकार के स्वैप प्रदाता का उपयोग करना पसंद करते हैं, उसका चयन करें",
"swap_providers": "स्वैप प्रदाता",
"swap_unavailable_needs_network_wallet": "यह पता प्रकार स्वैप से धनराशि प्राप्त नहीं कर सकता। इस पते पर भेजने के लिए आपको एक ${network} वॉलेट की आवश्यकता है।",
"swap_unavailable_switch_to_network": "यह पता प्रकार स्वैप से धनराशि प्राप्त नहीं कर सकता। जारी रखने के लिए अपने ${network} वॉलेट पर स्विच करें।",
"sweeping_wallet": "वॉलेट स्वीप करना",
"sweeping_wallet_alert": "इसमें ज़्यादा समय नहीं लगना चाहिए। इस स्क्रीन को न छोड़ें, वरना स्वीप किए गए फंड खो सकते हैं।",
"swipe_to_bridge": "ब्रिज करने के लिए स्वाइप करें",
+3
View File
@@ -93,6 +93,7 @@
"available": "Dostupno",
"available_balance": "Raspoloživo stanje",
"available_balance_description": "“Dostupno stanje” ili “Potvrđeno stanje” su sredstva koja možete odmah potrošiti. Ako se sredstva prikazuju u donjem stanju, ali ne i u gornjem, morate pričekati nekoliko minuta da dolazna sredstva dobiju više mrežnih potvrda. Nakon što dobiju više potvrda, moći ćete ih potrošiti.",
"available_balance_short": "Prosjek: ${amount}",
"avg_savings": "Prosj. ušteda",
"avl": "Avl",
"awaitDAppProcessing": "Molimo pričekajte da dApp završi s obradom.",
@@ -1149,6 +1150,8 @@
"swap_id": "ID zamjene",
"swap_provider_initial_desc": "Odaberite vrstu pružatelja zamjene koju želite koristiti pri zamjeni",
"swap_providers": "Pružatelji usluga zamjene",
"swap_unavailable_needs_network_wallet": "Ova vrsta adrese ne može primiti sredstva od zamjene. Potreban vam je ${network} novčanik za slanje na ovu adresu.",
"swap_unavailable_switch_to_network": "Ova vrsta adrese ne može primiti sredstva od zamjene. Za nastavak prebacite se na svoj ${network} novčanik.",
"sweeping_wallet": "Prebacivanje sredstava u novčanik",
"sweeping_wallet_alert": "Ovo ne bi trebalo dugo trajati. NE NAPUŠTAJTE OVAJ ZASLON ILI PREBAČENA SREDSTVA MOGU BITI IZGUBLJENA.",
"swipe_to_bridge": "Prijeđite prstom za premošćivanje",
+3
View File
@@ -93,6 +93,7 @@
"available": "Հասանելի",
"available_balance": "Հասանելի մնացորդ",
"available_balance_description": "«Հասանելի մնացորդը» կամ «Հաստատված մնացորդը» միջոցներ են, որոնք կարելի է անմիջապես ծախսել։ Եթե միջոցները երևում են ստորին մնացորդում, բայց ոչ վերևի մնացորդում, ապա պետք է մի քանի րոպե սպասեք, մինչև մուտքագրվող միջոցները ստանան ավելի շատ ցանցային հաստատումներ։ Ավելի շատ հաստատումներ ստանալուց հետո դրանք հնարավոր կլինի ծախսել։",
"available_balance_short": "Ավլ՝ ${amount}",
"avg_savings": "Միջին խնայողություն",
"avl": "Ավլ",
"awaitDAppProcessing": "Խնդրում ենք սպասել, մինչև dApp-ը ավարտի մշակումը։",
@@ -1149,6 +1150,8 @@
"swap_id": "Փոխանակման ID",
"swap_provider_initial_desc": "Ընտրեք փոխանակման մատակարարի տեսակը, որը նախընտրում եք օգտագործել փոխանակում կատարելիս",
"swap_providers": "Փոխանակման մատակարարներ",
"swap_unavailable_needs_network_wallet": "Այս հասցեի տեսակը չի կարող գումարներ ստանալ փոխանակումից: Այս հասցեին ուղարկելու համար ձեզ հարկավոր է ${network} դրամապանակ:",
"swap_unavailable_switch_to_network": "Այս հասցեի տեսակը չի կարող գումարներ ստանալ փոխանակումից: Շարունակելու համար անցեք ձեր ${network} դրամապանակին:",
"sweeping_wallet": "Դրամապանակի սվիպինգ",
"sweeping_wallet_alert": "Սա երկար չի տևի։ ՄԻ ԼՔԵՔ ԱՅՍ ԷԿՐԱՆԸ, ՀԱԿԱՌԱԿ ԴԵՊՔՈՒՄ ՄԱՔՐՎՈՂ ՄԻՋՈՑՆԵՐԸ ԿԱՐՈՂ ԵՆ ԿՈՐՉԵԼ։",
"swipe_to_bridge": "Սահեցրեք՝ կամրջելու համար",
+3
View File
@@ -93,6 +93,7 @@
"available": "Tersedia",
"available_balance": "Saldo Tersedia",
"available_balance_description": "“Saldo Tersedia” atau “Saldo Terkonfirmasi” adalah dana yang dapat langsung dibelanjakan. Jika dana muncul di saldo bawah tetapi tidak di saldo atas, maka Anda harus menunggu beberapa menit agar dana masuk mendapatkan konfirmasi jaringan tambahan. Setelah mendapatkan lebih banyak konfirmasi, dana tersebut akan dapat dibelanjakan.",
"available_balance_short": "Rata-rata: ${amount}",
"avg_savings": "Rata-rata Penghematan",
"avl": "Rata-rata",
"awaitDAppProcessing": "Mohon tunggu hingga dApp selesai memproses.",
@@ -1152,6 +1153,8 @@
"swap_id": "ID Swap",
"swap_provider_initial_desc": "Pilih jenis penyedia swap yang Anda inginkan untuk digunakan saat melakukan swap",
"swap_providers": "Penyedia swap",
"swap_unavailable_needs_network_wallet": "Jenis alamat ini tidak dapat menerima dana dari swap. Anda memerlukan dompet ${network} untuk mengirim ke alamat ini.",
"swap_unavailable_switch_to_network": "Jenis alamat ini tidak dapat menerima dana dari swap. Beralih ke dompet ${network} Anda untuk melanjutkan.",
"sweeping_wallet": "Dompet sweep",
"sweeping_wallet_alert": "Ini seharusnya tidak memakan waktu lama. JANGAN TINGGALKAN LAYAR INI ATAU DANA YANG DISAPU DAPAT HILANG.",
"swipe_to_bridge": "Geser untuk menjembatani",
+3
View File
@@ -93,6 +93,7 @@
"available": "Disponibile",
"available_balance": "Saldo disponibile",
"available_balance_description": "Il “Saldo disponibile” o “Saldo confermato” sono fondi che possono essere spesi immediatamente. Se i fondi compaiono nel saldo in basso ma non in quello in alto, devi attendere qualche minuto affinché i fondi in arrivo ottengano ulteriori conferme di rete. Dopo aver ricevuto più conferme, saranno spendibili.",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "Risparmio medio",
"avl": "Avl",
"awaitDAppProcessing": "Attendi che la dApp completi l'elaborazione.",
@@ -1150,6 +1151,8 @@
"swap_id": "ID swap",
"swap_provider_initial_desc": "Seleziona il tipo di fornitore di swap che preferisci utilizzare quando effettui uno swap in",
"swap_providers": "Provider di swap",
"swap_unavailable_needs_network_wallet": "Questo tipo di indirizzo non può ricevere fondi da uno swap. Hai bisogno di un portafoglio ${network} per inviare a questo indirizzo.",
"swap_unavailable_switch_to_network": "Questo tipo di indirizzo non può ricevere fondi da uno swap. Passa al tuo portafoglio ${network} per continuare.",
"sweeping_wallet": "Svuotamento del portafoglio",
"sweeping_wallet_alert": "Questo non dovrebbe richiedere molto tempo. NON LASCIARE QUESTA SCHERMATA O I FONDI SPOSTATI POTREBBERO ANDARE PERSI.",
"swipe_to_bridge": "Scorri per il bridge",
+3
View File
@@ -93,6 +93,7 @@
"available": "利用可能",
"available_balance": "利用可能残高",
"available_balance_description": "「利用可能な残高」または「承認済み残高」は、すぐに使用できる資金です。下の残高には表示されているのに上の残高に表示されない場合は、受信した資金がネットワークでさらに承認(確認)されるまで数分待つ必要があります。承認数が増えると、使用できるようになります。",
"available_balance_short": "平均: ${amount}",
"avg_savings": "平均節約額",
"avl": "アヴル",
"awaitDAppProcessing": "dAppの処理が完了するまでお待ちください。",
@@ -1150,6 +1151,8 @@
"swap_id": "スワップID",
"swap_provider_initial_desc": "スワップイン時に使用するスワッププロバイダーの種類を選択してください",
"swap_providers": "スワッププロバイダー",
"swap_unavailable_needs_network_wallet": "このアドレスタイプはスワップから資金を受け取ることができません。このアドレスに送信するには ${network} ウォレットが必要です。",
"swap_unavailable_switch_to_network": "このアドレスタイプはスワップから資金を受け取ることができません。 ${network} ウォレットに切り替えて続行してください。",
"sweeping_wallet": "スイーピングウォレット",
"sweeping_wallet_alert": "すぐに完了します。この画面から離れないでください。離れると、スイープした資金が失われる可能性があります。",
"swipe_to_bridge": "スワイプしてブリッジ",
+3
View File
@@ -93,6 +93,7 @@
"available": "사용 가능",
"available_balance": "사용 가능 잔액",
"available_balance_description": "“사용 가능 잔액” 또는 “확정 잔액”은 즉시 사용할 수 있는 자금입니다. 자금이 아래 잔액에는 표시되지만 위 잔액에는 표시되지 않는 경우, 입금된 자금이 네트워크에서 더 많은 컨펌(확인)을 받을 때까지 몇 분 정도 기다려야 합니다. 더 많은 컨펌을 받은 후에는 사용할 수 있습니다.",
"available_balance_short": "평균: ${amount}",
"avg_savings": "평균 절감액",
"avl": "Avl",
"awaitDAppProcessing": "dApp의 처리가 완료될 때까지 잠시 기다려 주세요.",
@@ -1149,6 +1150,8 @@
"swap_id": "스왑 ID",
"swap_provider_initial_desc": "스왑 인할 때 사용하려는 스왑 제공자 유형을 선택하세요.",
"swap_providers": "스왑 제공업체",
"swap_unavailable_needs_network_wallet": "이 주소 유형은 스왑에서 자금을 받을 수 없습니다. 이 주소로 보내려면 ${network} 지갑이 필요합니다.",
"swap_unavailable_switch_to_network": "이 주소 유형은 스왑에서 자금을 받을 수 없습니다. 계속하려면 ${network} 지갑으로 전환하세요.",
"sweeping_wallet": "지갑 스윕",
"sweeping_wallet_alert": "오래 걸리지 않습니다. 이 화면을 떠나지 마세요. 그렇지 않으면 스윕된 자금이 손실될 수 있습니다.",
"swipe_to_bridge": "스와이프하여 브리지",
+3
View File
@@ -93,6 +93,7 @@
"available": "ရရှိနိုင်သည်",
"available_balance": "ရရှိနိုင်သော လက်ကျန်",
"available_balance_description": "“Available Balance” သို့မဟုတ် “Confirmed Balance” သည် ချက်ချင်းသုံးစွဲနိုင်သော လက်ကျန်ငွေဖြစ်သည်။ လက်ကျန်အောက်ပိုင်းတွင်သာ ဝင်လာသော်လည်း အပေါ်ပိုင်းလက်ကျန်တွင် မပေါ်သေးပါက ကွန်ယက်အတည်ပြုချက် (network confirmations) ပိုမိုရရှိရန် မိနစ်အနည်းငယ် စောင့်ရပါမည်။ အတည်ပြုချက်များ ပိုမိုရရှိပြီးနောက် ထိုငွေများကို သုံးစွဲနိုင်ပါလိမ့်မည်။",
"available_balance_short": "Avl- ${amount}",
"avg_savings": "ပျမ်းမျှ ချွေတာမှု",
"avl": "Avl",
"awaitDAppProcessing": "ကျေးဇူးပြု၍ dApp ၏ စီမံဆောင်ရွက်မှု ပြီးဆုံးသည်အထိ စောင့်ပါ။",
@@ -1149,6 +1150,8 @@
"swap_id": "Swap ID",
"swap_provider_initial_desc": "Swap လုပ်ရာတွင် သင်အသုံးပြုလိုသည့် swap provider အမျိုးအစားကို ရွေးချယ်ပါ။",
"swap_providers": "Swap ဝန်ဆောင်မှုပေးသူများ",
"swap_unavailable_needs_network_wallet": "ဤလိပ်စာအမျိုးအစားသည် လဲလှယ်မှုတစ်ခုမှ ရံပုံငွေများကို လက်ခံမရနိုင်ပါ။ ဤလိပ်စာသို့ ပေးပို့ရန် ${network} ပိုက်ဆံအိတ်တစ်ခု လိုအပ်ပါသည်။",
"swap_unavailable_switch_to_network": "ဤလိပ်စာအမျိုးအစားသည် လဲလှယ်မှုတစ်ခုမှ ရံပုံငွေများကို လက်ခံမရနိုင်ပါ။ ရှေ့ဆက်ရန် သင်၏ ${network} ပိုက်ဆံအိတ်သို့ ပြောင်းပါ။",
"sweeping_wallet": "ပိုက်ဆံအိတ် ရှင်းလင်းခြင်း",
"sweeping_wallet_alert": "ဒါက ကြာကြာမခံသင့်ပါဘူး။ ဒီစခရင်ကနေ မထွက်ပါနဲ့၊ မဟုတ်ရင် sweep လုပ်ထားတဲ့ ရန်ပုံငွေတွေ ပျောက်ဆုံးနိုင်ပါတယ်။",
"swipe_to_bridge": "Bridge လုပ်ရန် ပွတ်ဆွဲပါ",
+3
View File
@@ -93,6 +93,7 @@
"available": "Beschikbaar",
"available_balance": "Beschikbaar saldo",
"available_balance_description": "Het “Beschikbare saldo” of “Bevestigde saldo” zijn fondsen die direct kunnen worden uitgegeven. Als fondsen in het lagere saldo verschijnen maar niet in het bovenste saldo, dan moet je een paar minuten wachten tot de inkomende fondsen meer netwerkbevestigingen krijgen. Daarna kan het worden uitgegeven.",
"available_balance_short": "Gem.: ${amount}",
"avg_savings": "Gem. besparingen",
"avl": "Gem",
"awaitDAppProcessing": "Wacht tot de dApp klaar is met verwerken.",
@@ -1149,6 +1150,8 @@
"swap_id": "Swap-ID",
"swap_provider_initial_desc": "Selecteer het type swapprovider dat je wilt gebruiken bij het swappen",
"swap_providers": "Swapproviders",
"swap_unavailable_needs_network_wallet": "Dit adrestype kan geen geld ontvangen van een swap. U heeft een portemonnee van ${network} nodig om naar dit adres te kunnen verzenden.",
"swap_unavailable_switch_to_network": "Dit adrestype kan geen geld ontvangen van een swap. Schakel over naar uw ${network}-portemonnee om door te gaan.",
"sweeping_wallet": "Wallet wordt geveegd",
"sweeping_wallet_alert": "Dit duurt niet lang. VERLAAT DIT SCHERM NIET, ANDERS KUNNEN DE FONDSEN VERLOREN GAAN.",
"swipe_to_bridge": "Veeg om te bridgen",
+3
View File
@@ -93,6 +93,7 @@
"available": "Dostępne",
"available_balance": "Dostępne saldo",
"available_balance_description": "„Dostępne saldo” lub „Potwierdzone saldo” to środki, które można wydać od razu. Jeśli środki pojawiają się w dolnym saldzie, ale nie w górnym, musisz poczekać kilka minut, aż przychodzące środki uzyskają więcej potwierdzeń sieci. Gdy uzyskają więcej potwierdzeń, będzie można je wydać.",
"available_balance_short": "Średnia: ${amount}",
"avg_savings": "Śr. oszczędności",
"avl": "Avl",
"awaitDAppProcessing": "Proszę poczekać, aż dApp zakończy przetwarzanie.",
@@ -1149,6 +1150,8 @@
"swap_id": "ID wymiany",
"swap_provider_initial_desc": "Wybierz typ dostawcy swapów, którego wolisz używać podczas swapowania w",
"swap_providers": "Dostawcy wymiany",
"swap_unavailable_needs_network_wallet": "Ten typ adresu nie może otrzymywać środków z wymiany. Aby wysłać przesyłkę na ten adres, potrzebujesz portfela ${network}.",
"swap_unavailable_switch_to_network": "Ten typ adresu nie może otrzymywać środków z wymiany. Aby kontynuować, przejdź do portfela ${network}.",
"sweeping_wallet": "Opróżnianie portfela",
"sweeping_wallet_alert": "To nie powinno zająć dużo czasu. NIE OPUSZCZAJ TEGO EKRANU, W PRZECIWNYM RAZIE ZAMIATANE ŚRODKI MOGĄ ZOSTAĆ UTRACONE.",
"swipe_to_bridge": "Przesuń, aby zmostkować",
+3
View File
@@ -93,6 +93,7 @@
"available": "Disponível",
"available_balance": "Saldo disponível",
"available_balance_description": "O “Saldo Disponível” ou “Saldo Confirmado” são fundos que podem ser gastos imediatamente. Se os fundos aparecerem no saldo inferior, mas não no saldo superior, você deve aguardar alguns minutos para que os fundos recebidos obtenham mais confirmações na rede. Depois que tiverem mais confirmações, eles poderão ser gastos.",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "Poupança média",
"avl": "Avl",
"awaitDAppProcessing": "Por favor, aguarde o dApp concluir o processamento.",
@@ -1151,6 +1152,8 @@
"swap_id": "ID da troca",
"swap_provider_initial_desc": "Selecione o tipo de provedor de swap que você prefere usar ao fazer swaps em",
"swap_providers": "Provedores de swap",
"swap_unavailable_needs_network_wallet": "Este tipo de endereço não pode receber fundos de um swap. Você precisa de uma carteira ${network} para enviar para este endereço.",
"swap_unavailable_switch_to_network": "Este tipo de endereço não pode receber fundos de um swap. Mude para sua carteira ${network} para continuar.",
"sweeping_wallet": "Carteira de varredura",
"sweeping_wallet_alert": "Isso não deve demorar muito. NÃO SAIA DESTA TELA OU OS FUNDOS VARRIDOS PODEM SER PERDIDOS.",
"swipe_to_bridge": "Deslize para fazer bridge",
+3
View File
@@ -88,6 +88,7 @@
"automatic": "Automático",
"available_balance": "Saldo disponível",
"available_balance_description": "O “Saldo disponível” ou “Saldo confirmado” são fundos que podem ser gastos imediatamente. Se os fundos aparecerem no saldo inferior, mas não no saldo superior, você deve aguardar alguns minutos para que os fundos recebidos obtenham mais confirmações na rede. Depois que tiverem mais confirmações, eles poderão ser gastos.",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "Economia média",
"awaitDAppProcessing": "Aguarde até que o dApp termine de processar.",
"awaiting_payment_confirmation": "Aguardando confirmação do pagamento",
@@ -1099,6 +1100,8 @@
"swap_id": "ID do Swap",
"swap_provider_initial_desc": "Selecione o tipo de provedor de swap que você prefere usar ao realizar swaps",
"swap_providers": "Provedores de swap",
"swap_unavailable_needs_network_wallet": "Este tipo de endereço não pode receber fundos de um swap. Você precisa de uma carteira ${network} para enviar para este endereço.",
"swap_unavailable_switch_to_network": "Este tipo de endereço não pode receber fundos de um swap. Mude para sua carteira ${network} para continuar.",
"sweeping_wallet": "Carteira em varredura",
"sweeping_wallet_alert": "Isso não deve demorar muito. NÃO SAIA DESTA TELA OU OS FUNDOS VARRIDOS PODERÃO SER PERDIDOS.",
"swipe_to_send": "Deslize para enviar",
+3
View File
@@ -93,6 +93,7 @@
"available": "Доступно",
"available_balance": "Доступный баланс",
"available_balance_description": "«Доступный баланс» или «Подтверждённый баланс» — это средства, которые можно потратить сразу. Если средства отображаются в нижнем балансе, но не в верхнем, значит, нужно подождать несколько минут, пока входящие средства получат больше подтверждений сети. После получения дополнительных подтверждений их можно будет потратить.",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "Средняя экономия",
"avl": "Авл",
"awaitDAppProcessing": "Пожалуйста, подождите, пока dApp завершит обработку.",
@@ -1150,6 +1151,8 @@
"swap_id": "ID обмена",
"swap_provider_initial_desc": "Выберите тип провайдера свопа, который вы предпочитаете использовать при свопе в",
"swap_providers": "Провайдеры обмена",
"swap_unavailable_needs_network_wallet": "Этот тип адреса не может получать средства от свопа. Для отправки на этот адрес вам понадобится кошелек ${network}.",
"swap_unavailable_switch_to_network": "Этот тип адреса не может получать средства от свопа. Чтобы продолжить, переключитесь на свой кошелек ${network}.",
"sweeping_wallet": "Сканирование кошелька",
"sweeping_wallet_alert": "Это не должно занять много времени. НЕ ПОКИДАЙТЕ ЭТОТ ЭКРАН, ИНАЧЕ ПЕРЕНЕСЁННЫЕ СРЕДСТВА МОГУТ БЫТЬ ПОТЕРЯНЫ.",
"swipe_to_bridge": "Проведите, чтобы выполнить перенос через мост",
+3
View File
@@ -93,6 +93,7 @@
"available": "พร้อมใช้งาน",
"available_balance": "ยอดคงเหลือที่ใช้ได้",
"available_balance_description": "“ยอดคงเหลือที่ใช้ได้” หรือ “ยอดคงเหลือที่ยืนยันแล้ว” คือเงินที่สามารถใช้จ่ายได้ทันที หากเงินปรากฏในยอดคงเหลือด้านล่างแต่ไม่ปรากฏในยอดคงเหลือด้านบน คุณต้องรอสักครู่เพื่อให้เงินที่เข้ามาได้รับการยืนยันจากเครือข่ายเพิ่มขึ้น เมื่อได้รับการยืนยันมากขึ้นแล้ว เงินนั้นจะสามารถใช้จ่ายได้",
"available_balance_short": "จำนวน: ${amount}",
"avg_savings": "การประหยัดเฉลี่ย",
"avl": "เฉลี่ย",
"awaitDAppProcessing": "โปรดรอให้ dApp ประมวลผลให้เสร็จสิ้น",
@@ -1149,6 +1150,8 @@
"swap_id": "รหัส Swap",
"swap_provider_initial_desc": "เลือกประเภทผู้ให้บริการ Swap ที่คุณต้องการใช้เมื่อทำการ Swap",
"swap_providers": "ผู้ให้บริการสวอป",
"swap_unavailable_needs_network_wallet": "ที่อยู่ประเภทนี้ไม่สามารถรับเงินจากการแลกเปลี่ยนได้ คุณต้องมีกระเป๋าเงิน ${network} เพื่อส่งไปยังที่อยู่นี้",
"swap_unavailable_switch_to_network": "ที่อยู่ประเภทนี้ไม่สามารถรับเงินจากการแลกเปลี่ยนได้ เปลี่ยนไปใช้กระเป๋าเงิน ${network} ของคุณเพื่อดำเนินการต่อ",
"sweeping_wallet": "กระเป๋าสตางค์สำหรับกวาดเหรียญ",
"sweeping_wallet_alert": "การดำเนินการนี้ใช้เวลาไม่นาน ห้ามออกจากหน้าจอนี้ มิฉะนั้นเงินที่กวาดอาจสูญหาย",
"swipe_to_bridge": "ปัดเพื่อบริดจ์",
+3
View File
@@ -93,6 +93,7 @@
"available": "Available",
"available_balance": "Magagamit na Balanse",
"available_balance_description": "Ang “Magagamit na Balanse” o “Nakumpirmang Balanse” ay mga pondong maaari nang gastusin kaagad. Kung lumalabas ang mga pondo sa mas mababang balanse ngunit wala sa itaas na balanse, kailangan mong maghintay ng ilang minuto para makakuha ng mas maraming kumpirmasyon sa network ang mga papasok na pondo. Kapag nakakuha na sila ng mas maraming kumpirmasyon, maaari na silang gastusin.",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "Avg. Matitipid",
"avl": "Avl",
"awaitDAppProcessing": "Mangyaring maghintay hanggang matapos ang pagproseso ng dApp.",
@@ -1149,6 +1150,8 @@
"swap_id": "Swap ID",
"swap_provider_initial_desc": "Piliin ang uri ng swap provider na mas gusto mong gamitin kapag nagsa-swap",
"swap_providers": "Mga provider ng swap",
"swap_unavailable_needs_network_wallet": "Ang uri ng address na ito ay hindi makakatanggap ng mga pondo mula sa isang swap. Kailangan mo ng ${network} wallet upang maipadala sa address na ito.",
"swap_unavailable_switch_to_network": "Ang uri ng address na ito ay hindi makakatanggap ng mga pondo mula sa isang swap. Lumipat sa iyong ${network} wallet upang magpatuloy.",
"sweeping_wallet": "Sweeping wallet",
"sweeping_wallet_alert": "Hindi ito dapat magtagal. HUWAG UMALIS SA SCREEN NA ITO O MAARING MAWALA ANG MGA NA-SWEEP NA PONDO.",
"swipe_to_bridge": "Mag-swipe para mag-bridge",
+3
View File
@@ -93,6 +93,7 @@
"available": "Kullanılabilir",
"available_balance": "Kullanılabilir Bakiye",
"available_balance_description": "“Kullanılabilir Bakiye” veya “Onaylanmış Bakiye”, hemen harcanabilen fonlardır. Fonlar alt bakiyede görünüp üst bakiyede görünmüyorsa, gelen fonların ağda daha fazla onay alması için birkaç dakika beklemeniz gerekir. Daha fazla onay aldıktan sonra harcanabilir olacaktır.",
"available_balance_short": "Ortalama: ${amount}",
"avg_savings": "Ort. Tasarruf",
"avl": "Avl",
"awaitDAppProcessing": "Lütfen dApp'in işlemi tamamlamasını bekleyin.",
@@ -1149,6 +1150,8 @@
"swap_id": "Swap Kimliği",
"swap_provider_initial_desc": "Swap yaparken kullanmayı tercih ettiğiniz swap sağlayıcı türünü seçin",
"swap_providers": "Takas sağlayıcıları",
"swap_unavailable_needs_network_wallet": "Bu adres türü takastan para alamaz. Bu adrese göndermek için bir ${network} cüzdanına ihtiyacınız var.",
"swap_unavailable_switch_to_network": "Bu adres türü takastan para alamaz. Devam etmek için ${network} cüzdanınıza geçin.",
"sweeping_wallet": "Cüzdan süpürme",
"sweeping_wallet_alert": "Bu uzun sürmemeli. BU EKRANDAN AYRILMAYIN, AKSİ TAKDİRDE SÜPÜRÜLEN FONLAR KAYBOLABİLİR.",
"swipe_to_bridge": "Köprülemek için kaydırın",
+3
View File
@@ -93,6 +93,7 @@
"available": "Доступно",
"available_balance": "Доступний баланс",
"available_balance_description": "«Доступний баланс» або «Підтверджений баланс» — це кошти, які можна витратити негайно. Якщо кошти відображаються в нижньому балансі, але не у верхньому, потрібно зачекати кілька хвилин, доки вхідні кошти отримають більше підтверджень мережі. Після отримання додаткових підтверджень їх можна буде витратити.",
"available_balance_short": "Середня сума: ${amount}",
"avg_savings": "Сер. заощадження",
"avl": "Avl",
"awaitDAppProcessing": "Будь ласка, зачекайте, доки dApp завершить обробку.",
@@ -1150,6 +1151,8 @@
"swap_id": "ID обміну",
"swap_provider_initial_desc": "Виберіть тип провайдера свопу, який ви бажаєте використовувати під час обміну",
"swap_providers": "Провайдери обміну",
"swap_unavailable_needs_network_wallet": "Цей тип адреси не може отримати кошти від обміну. Щоб надіслати на цю адресу, вам потрібен гаманець ${network}.",
"swap_unavailable_switch_to_network": "Цей тип адреси не може отримати кошти від обміну. Щоб продовжити, перейдіть на гаманець ${network}.",
"sweeping_wallet": "Змітання гаманця",
"sweeping_wallet_alert": "Це не повинно зайняти багато часу. НЕ ЗАЛИШАЙТЕ ЦЕЙ ЕКРАН, ІНАКШЕ ЗМЕТЕНІ КОШТИ МОЖУТЬ БУТИ ВТРАЧЕНІ.",
"swipe_to_bridge": "Проведіть, щоб перенести між мережами",
+3
View File
@@ -93,6 +93,7 @@
"available": "دستیاب",
"available_balance": "دستیاب بیلنس",
"available_balance_description": "”دستیاب بیلنس” یا ”تصدیق شدہ بیلنس” وہ فنڈز ہیں جنہیں فوراً خرچ کیا جا سکتا ہے۔ اگر فنڈز نیچے والے بیلنس میں نظر آئیں لیکن اوپر والے بیلنس میں نہیں، تو آپ کو آنے والے فنڈز کے مزید نیٹ ورک کنفرمیشنز حاصل ہونے کے لیے چند منٹ انتظار کرنا ہوگا۔ جب انہیں مزید کنفرمیشنز مل جائیں گی تو وہ خرچ کے قابل ہو جائیں گے۔",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "اوسط بچت",
"avl": "Avl",
"awaitDAppProcessing": "براہِ کرم dApp کی پروسیسنگ مکمل ہونے تک انتظار کریں۔",
@@ -1151,6 +1152,8 @@
"swap_id": "سواپ آئی ڈی",
"swap_provider_initial_desc": "سواپ کرتے وقت استعمال کے لیے اپنی پسند کے سواپ فراہم کنندہ کی قسم منتخب کریں",
"swap_providers": "سویپ فراہم کنندگان",
"swap_unavailable_needs_network_wallet": "ایڈریس کی یہ قسم سویپ سے فنڈز وصول نہیں کر سکتی۔ اس پتے پر بھیجنے کے لیے آپ کو ایک ${network} والیٹ کی ضرورت ہے۔",
"swap_unavailable_switch_to_network": "ایڈریس کی یہ قسم سویپ سے فنڈز وصول نہیں کر سکتی۔ جاری رکھنے کے لیے اپنے ${network} والیٹ پر جائیں۔",
"sweeping_wallet": "والٹ سویپ کرنا",
"sweeping_wallet_alert": "اس میں زیادہ وقت نہیں لگنا چاہیے۔ اس اسکرین کو مت چھوڑیں، ورنہ سویپ کیے گئے فنڈز ضائع ہو سکتے ہیں۔",
"swipe_to_bridge": "برج کرنے کے لیے سوائپ کریں",
+3
View File
@@ -93,6 +93,7 @@
"available": "Khả dụng",
"available_balance": "Số dư khả dụng",
"available_balance_description": "“Số dư khả dụng” hoặc “Số dư đã xác nhận” là số tiền có thể chi tiêu ngay lập tức. Nếu tiền xuất hiện ở số dư phía dưới nhưng không xuất hiện ở số dư phía trên, thì bạn phải đợi vài phút để khoản tiền đến có thêm xác nhận từ mạng. Sau khi có thêm xác nhận, chúng sẽ có thể chi tiêu.",
"available_balance_short": "Trung bình: ${amount}",
"avg_savings": "Tiết kiệm TB",
"avl": "trung bình",
"awaitDAppProcessing": "Vui lòng đợi dApp xử lý xong.",
@@ -1150,6 +1151,8 @@
"swap_id": "ID Swap",
"swap_provider_initial_desc": "Chọn loại nhà cung cấp hoán đổi mà bạn muốn sử dụng khi hoán đổi trong ứng dụng",
"swap_providers": "Nhà cung cấp hoán đổi",
"swap_unavailable_needs_network_wallet": "Loại địa chỉ này không thể nhận tiền từ trao đổi. Bạn cần có ví ${network} để gửi tới địa chỉ này.",
"swap_unavailable_switch_to_network": "Loại địa chỉ này không thể nhận tiền từ trao đổi. Chuyển sang ví ${network} của bạn để tiếp tục.",
"sweeping_wallet": "Quét ví",
"sweeping_wallet_alert": "Việc này sẽ không mất nhiều thời gian. KHÔNG RỜI KHỎI MÀN HÌNH NÀY, NẾU KHÔNG SỐ TIỀN ĐÃ QUÉT CÓ THỂ BỊ MẤT.",
"swipe_to_bridge": "Vuốt để bắc cầu",
+3
View File
@@ -93,6 +93,7 @@
"available": "Ó wà",
"available_balance": "Ìwọ̀ntúnwọ̀nsì tó wà",
"available_balance_description": "“Iwọntunwọnsi Wa” tabi “Iwọntunwọnsi Ti a Jẹ́rìí” jẹ́ àwọn owó tí o lè na lẹ́sẹ̀kẹsẹ. Bí àwọn owó bá hàn ní iwọntunwọnsi ìsàlẹ̀ ṣùgbọ́n kì í hàn ní iwọntunwọnsi òkè, nígbà náà o gbọdọ̀ dúró fún ìṣẹ́jú díẹ̀ kí àwọn owó tí ń wọlé lè gba ìjẹ́rìí nẹ́tíwọ́ọ̀kì púpọ̀ síi. Lẹ́yìn tí wọ́n bá ti gba ìjẹ́rìí púpọ̀ síi, o lè na wọ́n.",
"available_balance_short": "Avl: ${amount}",
"avg_savings": "Ìpamọ́ àpapọ̀",
"avl": "Avl",
"awaitDAppProcessing": "Jọ̀wọ́, dúró kí dApp parí ìṣiṣẹ́.",
@@ -1150,6 +1151,8 @@
"swap_id": "ID Swap",
"swap_provider_initial_desc": "Yan iru olupese swap ti o fẹ́ lò nígbà tí o bá ń swap sínú",
"swap_providers": "Awọn olupese swap",
"swap_unavailable_needs_network_wallet": "Iru adirẹsi yii ko le gba owo lati swap kan. O nilo apamọwọ ${network} lati fi ranṣẹ si adirẹsi yii.",
"swap_unavailable_switch_to_network": "Iru adirẹsi yii ko le gba owo lati swap kan. Yipada si apamọwọ ${network} rẹ lati tẹsiwaju.",
"sweeping_wallet": "Wallet ìkó",
"sweeping_wallet_alert": "Eyi kò yẹ kí ó pẹ́. MÁ ṢE KÚRÒ LÓRÍ IBÓJÚ YÌÍ TÁBÍ OWÓ TÍ A TI SWEEP LÈ SỌ́NÙ.",
"swipe_to_bridge": "Fà láti so ìjápọ̀",
+3
View File
@@ -93,6 +93,7 @@
"available": "可用",
"available_balance": "可用余额",
"available_balance_description": "“可用余额”或“确认余额”是指可立即花费的资金。如果资金显示在下方余额中但未显示在上方余额中,则需要等待几分钟,让转入资金获得更多网络确认。获得更多确认后,这些资金即可花费。",
"available_balance_short": "平均价值:${amount}",
"avg_savings": "平均节省",
"avl": "平均",
"awaitDAppProcessing": "请稍候,等待 dApp 完成处理。",
@@ -1150,6 +1151,8 @@
"swap_id": "兑换 ID",
"swap_provider_initial_desc": "选择您在换入时偏好使用的兑换服务提供商类型",
"swap_providers": "兑换服务提供商",
"swap_unavailable_needs_network_wallet": "此地址类型无法从交换中接收资金。您需要一个 ${network} 钱包才能发送到此地址。",
"swap_unavailable_switch_to_network": "此地址类型无法从交换中接收资金。切换到您的 ${network} 钱包以继续。",
"sweeping_wallet": "正在归集钱包",
"sweeping_wallet_alert": "这应该不会花很长时间。请勿离开此屏幕,否则被归集的资金可能会丢失。",
"swipe_to_bridge": "滑动以跨链桥接",
+103
View File
@@ -0,0 +1,103 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_parser.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:flutter_test/flutter_test.dart";
void main() {
const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
group("chain binding", () {
test("explicit ethereum chain binds explicitly", () {
final request = AnyPayParser.fromRaw(
"ethereum:$recipient@8453?value=1e18",
);
expect(request.chainBinding, isA<ExplicitEvmChain>());
expect((request.chainBinding as ExplicitEvmChain).chainId, 8453);
});
test("ethereum without a chain id stays chainless", () {
final request = AnyPayParser.fromRaw(
"ethereum:$usdtEthContract/transfer?address=$recipient&uint256=1000000",
);
expect(request.chainBinding, isA<ChainlessEvm>());
expect(request.hasContract, true);
});
test("non evm schemes bind no evm chain", () {
final request = AnyPayParser.fromRaw(
"bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
);
expect(request.chainBinding, isA<NoEvmBinding>());
});
test("raw evm addresses bind no evm chain", () {
final request = AnyPayParser.fromRaw(recipient);
expect(request.chainBinding, isA<NoEvmBinding>());
expect(request.detection.isValid, true);
});
test("an evm chain scheme binds its chain explicitly", () {
final request = AnyPayParser.fromRaw("polygon:$recipient");
expect((request.chainBinding as ExplicitEvmChain).chainId, 137);
});
});
group("payment request reconstruction", () {
test("an explicit chain request keeps its chain and raw amount", () {
final original = PaymentRequest.fromUri(
Uri.parse("ethereum:$usdtEthContract@8453/transfer?address=$recipient&uint256=3000000"),
);
final request = AnyPayParser.fromPaymentRequest(original);
expect(request.rawInput.contains("@8453"), true);
expect(request.paymentRequest.rawTokenAmount, "3000000");
expect((request.chainBinding as ExplicitEvmChain).chainId, 8453);
});
test("a chainless request rebuilds with a placeholder but keeps the chainless binding", () {
final original = PaymentRequest.fromUri(
Uri.parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=1000000"),
);
final request = AnyPayParser.fromPaymentRequest(original);
expect(request.rawInput.contains("@1/"), true);
expect(request.chainBinding, isA<ChainlessEvm>());
expect(request.detection.isValid, true);
});
test("a tron token request keeps its contract on the payment request", () {
final original = PaymentRequest.fromUri(
Uri.parse("tron:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t?token=TTokenContract111111111111111111"),
);
final request = AnyPayParser.fromPaymentRequest(original);
expect(request.contractAddress, "TTokenContract111111111111111111");
expect(request.chainBinding, isA<NoEvmBinding>());
});
test("a non evm request rebuilds scheme address and amount", () {
final original = PaymentRequest("someaddress", "1.25", "", "monero", null);
final request = AnyPayParser.fromPaymentRequest(original);
expect(request.rawInput, "monero:someaddress?amount=1.25");
});
test("a lightning request passes the invoice through untouched", () {
final original = PaymentRequest("lnbc1invoice", "", "", "lightning", null);
final request = AnyPayParser.fromPaymentRequest(original);
expect(request.rawInput, "lnbc1invoice");
});
});
}
+201
View File
@@ -0,0 +1,201 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_parser.dart";
import "package:cake_wallet/core/anypay/anypay_resolver.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/erc20_token.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
class _FakeTokenLookup implements AnyPayTokenLookup {
_FakeTokenLookup({this.onFindToken, this.onFindChainId});
final Future<CryptoCurrency?> Function({required WalletType walletType, required String address})?
onFindToken;
final Future<int?> Function(String contractAddress, {int? excludingChainId})? onFindChainId;
@override
Future<CryptoCurrency?> findTokenByAddress({
required WalletType walletType,
required String address,
}) {
if (onFindToken == null) {
fail("no token lookup expected");
}
return onFindToken!(walletType: walletType, address: address);
}
@override
Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId}) {
if (onFindChainId == null) {
fail("no chain sweep expected");
}
return onFindChainId!(contractAddress, excludingChainId: excludingChainId);
}
}
void main() {
const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const solAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
const usdcSolMint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
final usdt = Erc20Token(
name: "Tether",
symbol: "USDT",
contractAddress: usdtEthContract,
decimal: 6,
);
WalletSnapshot snapshot({
WalletType type = WalletType.ethereum,
int? chainId,
bool hasEvmProxy = true,
}) =>
WalletSnapshot(
type: type,
currentWalletIsEvm:
type == WalletType.ethereum || type == WalletType.polygon || type == WalletType.base,
currentChainId: chainId,
wallets: const [],
// The service builds an empty chain map when the evm proxy is absent.
supportedEvmChains: hasEvmProxy
? const {
1: WalletType.ethereum,
137: WalletType.polygon,
8453: WalletType.base,
}
: const {},
hasEvmProxy: hasEvmProxy,
hasSolanaProxy: true,
hasTronProxy: true,
);
AnyPayRequest parse(String input) => AnyPayParser.fromRaw(input);
test("a request without a contract needs no resolution", () async {
final resolver = AnyPayResolver(tokenLookup: _FakeTokenLookup());
final resolution = await resolver.resolve(
parse("ethereum:$recipient@1?value=1e18"),
snapshot(chainId: 1),
);
expect(resolution, isA<NoTokenRequested>());
});
test("a token on the explicit target chain resolves with its amount", () async {
final lookedUp = <WalletType>[];
final resolver = AnyPayResolver(
tokenLookup: _FakeTokenLookup(
onFindToken: ({required walletType, required address}) async {
lookedUp.add(walletType);
return walletType == WalletType.ethereum ? usdt : null;
},
),
);
final resolution = await resolver.resolve(
parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
snapshot(type: WalletType.base, chainId: 8453),
);
final resolved = resolution as TokenResolved;
expect(lookedUp, [WalletType.ethereum]);
expect(resolved.chainId, 1);
expect(resolved.token, usdt);
expect(resolved.amountOverride, "1");
});
test("an unknown token on an explicit chain is not swept to other chains", () async {
bool sweepCalled = false;
final resolver = AnyPayResolver(
tokenLookup: _FakeTokenLookup(
onFindToken: ({required walletType, required address}) async => null,
onFindChainId: (contract, {excludingChainId}) async {
sweepCalled = true;
return 137;
},
),
);
final resolution = await resolver.resolve(
parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
snapshot(chainId: 1),
);
expect(resolution, isA<TokenUnknown>());
expect(sweepCalled, false);
});
test("a chainless token unknown on the current chain reroutes through the sweep", () async {
final resolver = AnyPayResolver(
tokenLookup: _FakeTokenLookup(
onFindToken: ({required walletType, required address}) async =>
walletType == WalletType.ethereum ? usdt : null,
onFindChainId: (contract, {excludingChainId}) async {
expect(excludingChainId, 8453);
return 1;
},
),
);
final resolution = await resolver.resolve(
parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
snapshot(type: WalletType.base, chainId: 8453),
);
final resolved = resolution as TokenResolved;
expect(resolved.chainId, 1);
expect(resolved.amountOverride, "2");
});
test("a chainless token unknown everywhere stays unknown", () async {
final resolver = AnyPayResolver(
tokenLookup: _FakeTokenLookup(
onFindToken: ({required walletType, required address}) async => null,
onFindChainId: (contract, {excludingChainId}) async => null,
),
);
final resolution = await resolver.resolve(
parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
snapshot(chainId: 1),
);
expect((resolution as TokenUnknown).contract, usdtEthContract);
});
test("a solana token resolves against the detected wallet type", () async {
const usdcSol = CryptoCurrency.usdtSol;
final resolver = AnyPayResolver(
tokenLookup: _FakeTokenLookup(
onFindToken: ({required walletType, required address}) async {
expect(walletType, WalletType.solana);
expect(address, usdcSolMint);
return usdcSol;
},
),
);
final resolution = await resolver.resolve(
parse("solana:$solAddress?amount=3&spl-token=$usdcSolMint"),
snapshot(type: WalletType.monero),
);
final resolved = resolution as TokenResolved;
expect(resolved.chainId, null);
expect(resolved.token, usdcSol);
expect(resolved.amountOverride, "3");
});
test("an evm contract without the evm proxy stays unknown without any lookups", () async {
final resolver = AnyPayResolver(tokenLookup: _FakeTokenLookup());
final resolution = await resolver.resolve(
parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=1000000"),
snapshot(hasEvmProxy: false),
);
expect(resolution, isA<TokenUnknown>());
});
}
+414
View File
@@ -0,0 +1,414 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_parser.dart";
import "package:cake_wallet/core/anypay/anypay_router.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/erc20_token.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
void main() {
const recipient = "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41";
const usdtEthContract = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const btcAddress = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq";
const xmrAddress =
"44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A";
const tronAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
const solAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
final usdt = Erc20Token(
name: "Tether",
symbol: "USDT",
contractAddress: usdtEthContract,
decimal: 6,
);
WalletInfo walletOf(WalletType type) => WalletInfo.external(
id: "id-${type.name}",
name: "wallet-${type.name}",
type: type,
isRecovery: false,
restoreHeight: 0,
date: DateTime(2026),
dirPath: "",
path: "",
address: "",
);
WalletSnapshot snapshot({
WalletType type = WalletType.ethereum,
int? chainId,
List<WalletInfo> wallets = const [],
bool hasEvmProxy = true,
bool hasSolanaProxy = true,
bool hasTronProxy = true,
}) =>
WalletSnapshot(
type: type,
currentWalletIsEvm: isEVMCompatibleChain(type),
currentChainId: chainId,
wallets: wallets,
supportedEvmChains: hasEvmProxy
? const {
1: WalletType.ethereum,
137: WalletType.polygon,
8453: WalletType.base,
}
: const {},
hasEvmProxy: hasEvmProxy,
hasSolanaProxy: hasSolanaProxy,
hasTronProxy: hasTronProxy,
);
AnyPayRequest parse(String input) => AnyPayParser.fromRaw(input);
group("evm routing", () {
test("explicit chain matching the current wallet applies directly", () {
final decision = AnyPayRouter.route(
parse("ethereum:$recipient@1?value=1e18"),
snapshot(chainId: 1),
const NoTokenRequested(),
);
expect(decision, isA<AnyPayApplyToCurrentWallet>());
});
test("explicit chain differing from the current wallet goes cross chain", () {
final decision = AnyPayRouter.route(
parse("ethereum:$recipient@8453?value=1e18"),
snapshot(chainId: 1, wallets: [walletOf(WalletType.base)]),
const NoTokenRequested(),
);
expect(decision, isA<AnyPayCrossChainPayment>());
final crossChain = decision as AnyPayCrossChainPayment;
expect(crossChain.targetChainId, 8453);
expect(crossChain.targetWalletType, WalletType.base);
expect(crossChain.hasCompatibleWallet, true);
});
test("cross chain with no wallet for the target offers no compatible wallet", () {
final decision = AnyPayRouter.route(
parse("ethereum:$recipient@8453?value=1e18"),
snapshot(chainId: 1),
const NoTokenRequested(),
);
expect((decision as AnyPayCrossChainPayment).hasCompatibleWallet, false);
});
test("unsupported explicit chain is rejected with its id", () {
final decision = AnyPayRouter.route(
parse("ethereum:$recipient@10?value=1e18"),
snapshot(chainId: 1),
const NoTokenRequested(),
);
expect((decision as AnyPayUnsupportedNetwork).chainId, 10);
});
test("chainless request binds to the current evm chain", () {
final decision = AnyPayRouter.route(
parse("ethereum:$recipient?value=1e18"),
snapshot(type: WalletType.base, chainId: 8453),
const NoTokenRequested(),
);
expect(decision, isA<AnyPayApplyToCurrentWallet>());
});
test("chainless request at a non evm wallet targets mainnet cross chain", () {
final decision = AnyPayRouter.route(
parse("ethereum:$recipient?value=1e18"),
snapshot(type: WalletType.monero, wallets: [walletOf(WalletType.ethereum)]),
const NoTokenRequested(),
);
expect((decision as AnyPayCrossChainPayment).targetChainId, 1);
});
test("raw evm address at an evm wallet applies directly", () {
final decision = AnyPayRouter.route(
parse(recipient),
snapshot(chainId: 1),
const NoTokenRequested(),
);
expect(decision, isA<AnyPayApplyToCurrentWallet>());
});
test("raw evm address at a non evm wallet asks for a network choice", () {
final decision = AnyPayRouter.route(
parse(recipient),
snapshot(type: WalletType.solana),
const NoTokenRequested(),
);
expect(decision, isA<AnyPayEvmNetworkChoice>());
});
test("resolved token on the current chain applies with the token", () {
final decision = AnyPayRouter.route(
parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
snapshot(type: WalletType.base, chainId: 8453),
TokenResolved(token: usdt, chainId: 8453, amountOverride: "2"),
);
final apply = decision as AnyPayApplyToCurrentWallet;
expect(apply.token, usdt);
expect(apply.amountOverride, "2");
});
test("chainless token resolved on another chain reroutes there", () {
final decision = AnyPayRouter.route(
parse("ethereum:$usdtEthContract/transfer?address=$recipient&uint256=2000000"),
snapshot(type: WalletType.base, chainId: 8453, wallets: [walletOf(WalletType.ethereum)]),
TokenResolved(token: usdt, chainId: 1, amountOverride: "2"),
);
final crossChain = decision as AnyPayCrossChainPayment;
expect(crossChain.targetChainId, 1);
expect(crossChain.token, usdt);
});
test("an unknown token never falls back to the native asset", () {
final decision = AnyPayRouter.route(
parse("ethereum:$usdtEthContract@1/transfer?address=$recipient&uint256=2000000"),
snapshot(chainId: 1),
const TokenUnknown(usdtEthContract),
);
expect(decision, isA<AnyPayUnsupportedToken>());
expect((decision as AnyPayUnsupportedToken).contract, usdtEthContract);
});
test("an unsupported network wins over an unknown token", () {
final decision = AnyPayRouter.route(
parse("ethereum:$usdtEthContract@10/transfer?address=$recipient&uint256=2000000"),
snapshot(chainId: 1),
const TokenUnknown(usdtEthContract),
);
expect((decision as AnyPayUnsupportedNetwork).chainId, 10);
});
test("evm address without the evm proxy applies as plain input", () {
final decision = AnyPayRouter.route(
parse(recipient),
snapshot(type: WalletType.monero, hasEvmProxy: false),
const NoTokenRequested(),
);
expect(decision, isA<AnyPayApplyToCurrentWallet>());
expect((decision as AnyPayApplyToCurrentWallet).token, null);
});
});
group("non evm routing", () {
test("address matching the current wallet applies directly", () {
final decision = AnyPayRouter.route(
parse(xmrAddress),
snapshot(type: WalletType.monero),
const NoTokenRequested(),
);
expect(decision, isA<AnyPayApplyToCurrentWallet>());
});
test("btc address at another wallet goes cross chain with its wallets", () {
final decision = AnyPayRouter.route(
parse(btcAddress),
snapshot(type: WalletType.monero, wallets: [walletOf(WalletType.bitcoin)]),
const NoTokenRequested(),
);
final crossChain = decision as AnyPayCrossChainPayment;
expect(crossChain.targetWalletType, WalletType.bitcoin);
expect(crossChain.hasCompatibleWallet, true);
});
test("btc address with no bitcoin wallet leaves only the swap offer", () {
final decision = AnyPayRouter.route(
parse(btcAddress),
snapshot(type: WalletType.monero),
const NoTokenRequested(),
);
expect((decision as AnyPayCrossChainPayment).hasCompatibleWallet, false);
});
test("tron address at another wallet goes cross chain to tron", () {
final decision = AnyPayRouter.route(
parse(tronAddress),
snapshot(chainId: 1, wallets: [walletOf(WalletType.tron)]),
const NoTokenRequested(),
);
final crossChain = decision as AnyPayCrossChainPayment;
expect(crossChain.targetWalletType, WalletType.tron);
expect(crossChain.targetChainId, null);
expect(crossChain.wallets.length, 1);
});
test("solana address without the solana proxy applies as plain input", () {
final decision = AnyPayRouter.route(
parse(solAddress),
snapshot(chainId: 1, hasSolanaProxy: false),
const NoTokenRequested(),
);
expect(decision, isA<AnyPayApplyToCurrentWallet>());
});
test("an unknown non evm token never falls back to the native asset", () {
final decision = AnyPayRouter.route(
parse("solana:$solAddress?amount=3&spl-token=$solAddress"),
snapshot(type: WalletType.solana),
const TokenUnknown(solAddress),
);
expect(decision, isA<AnyPayUnsupportedToken>());
});
test("solana token resolved on the current wallet applies with the token", () {
const splToken = CryptoCurrency.usdtSol;
final decision = AnyPayRouter.route(
parse("solana:$solAddress?amount=3&spl-token=$solAddress"),
snapshot(type: WalletType.solana),
const TokenResolved(token: splToken, amountOverride: "3"),
);
final apply = decision as AnyPayApplyToCurrentWallet;
expect(apply.token, splToken);
expect(apply.amountOverride, "3");
});
});
group("native request currency", () {
test("an explicit native request with an amount carries its currency", () {
final decision = AnyPayRouter.route(
parse("ethereum:$recipient@1?value=2e18"),
snapshot(chainId: 1),
const NoTokenRequested(),
);
expect((decision as AnyPayApplyToCurrentWallet).fallbackCurrency, CryptoCurrency.eth);
});
test("a raw address carries no fallback currency", () {
final decision = AnyPayRouter.route(
parse(recipient),
snapshot(chainId: 1),
const NoTokenRequested(),
);
expect((decision as AnyPayApplyToCurrentWallet).fallbackCurrency, null);
});
});
group("degenerate input", () {
test("empty input is invalid", () {
final decision = AnyPayRouter.route(
parse(""),
snapshot(chainId: 1),
const NoTokenRequested(),
);
expect(decision, isA<AnyPayEmptyInput>());
});
test("undetectable text applies as plain input for the validator to reject", () {
final decision = AnyPayRouter.route(
parse("definitelynotanaddress"),
snapshot(chainId: 1),
const NoTokenRequested(),
);
expect(decision, isA<AnyPayApplyToCurrentWallet>());
});
});
group("requested evm chain", () {
test("explicit binding wins over the wallet chain", () {
final request = parse("ethereum:$recipient@137?value=1e18");
expect(AnyPayRouter.requestedEvmChainId(request, snapshot(chainId: 1)), 137);
});
test("chainless binding takes the wallet chain", () {
final request = parse("ethereum:$recipient?value=1e18");
expect(
AnyPayRouter.requestedEvmChainId(
request,
snapshot(type: WalletType.base, chainId: 8453),
),
8453,
);
});
test("chainless binding defaults to mainnet without an evm wallet", () {
final request = parse("ethereum:$recipient?value=1e18");
expect(
AnyPayRouter.requestedEvmChainId(request, snapshot(type: WalletType.monero)),
1,
);
});
test("raw addresses bind no target", () {
expect(AnyPayRouter.requestedEvmChainId(parse(recipient), snapshot(chainId: 1)), null);
});
});
group("swap incompatible recipients", () {
final spAddress = "sp1${"q" * 113}";
final mwebAddress = "ltcmweb1q${"a" * 90}";
test("a silent payment address with a bitcoin wallet switches without a swap offer", () {
final decision = AnyPayRouter.route(
parse(spAddress),
snapshot(type: WalletType.monero, wallets: [walletOf(WalletType.bitcoin)]),
const NoTokenRequested(),
);
final crossChain = decision as AnyPayCrossChainPayment;
expect(crossChain.targetWalletType, WalletType.bitcoin);
expect(crossChain.hasCompatibleWallet, true);
expect(crossChain.canSwap, false);
});
test("a silent payment address with no bitcoin wallet cannot swap either", () {
final decision = AnyPayRouter.route(
parse(spAddress),
snapshot(type: WalletType.monero),
const NoTokenRequested(),
);
final crossChain = decision as AnyPayCrossChainPayment;
expect(crossChain.hasCompatibleWallet, false);
expect(crossChain.canSwap, false);
});
test("an mweb address never offers a swap", () {
final decision = AnyPayRouter.route(
parse(mwebAddress),
snapshot(),
const NoTokenRequested(),
);
expect((decision as AnyPayCrossChainPayment).canSwap, false);
});
test("a plain btc address keeps the swap offer", () {
final decision = AnyPayRouter.route(
parse(btcAddress),
snapshot(type: WalletType.monero),
const NoTokenRequested(),
);
expect((decision as AnyPayCrossChainPayment).canSwap, true);
});
});
}
@@ -196,6 +196,38 @@ void main() {
});
});
group("Zcash Address Detection", () {
test("detects Zcash transparent address", () {
const address = "t1V9mnyk5Z5cTNMCkLbaDwSskgJZucTLdgW";
final result = UniversalAddressDetector.detectAddress(address);
expect(result.isValid, true);
expect(result.detectedCurrency, CryptoCurrency.zec);
expect(result.detectedWalletType, WalletType.zcash);
expect(result.address, address);
});
test("detects Zcash shielded address", () {
const address =
"zsab12cd34efab12cd34efab12cd34efab12cd34efab12cd34efab12cd34efab12cd34efgh56kl";
final result = UniversalAddressDetector.detectAddress(address);
expect(result.isValid, true);
expect(result.detectedCurrency, CryptoCurrency.zec);
expect(result.detectedWalletType, WalletType.zcash);
});
test("detects Zcash unified address", () {
const address =
"u1z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5z9y8x7w6v5";
final result = UniversalAddressDetector.detectAddress(address);
expect(result.isValid, true);
expect(result.detectedCurrency, CryptoCurrency.zec);
expect(result.detectedWalletType, WalletType.zcash);
});
});
group('Wownero Address Detection', () {
test('detects Wownero address', () {
const address =