| 1 | import "package:cake_wallet/core/address_validator.dart"; |
| 2 | import "package:cake_wallet/core/anypay/anypay_models.dart"; |
| 3 | import "package:cake_wallet/reactions/wallet_connect.dart"; |
| 4 | import "package:cw_core/crypto_currency.dart"; |
| 5 | import "package:cw_core/wallet_type.dart"; |
| 6 | |
| 7 | class AnyPayRouter { |
| 8 | static final List<RegExp> _swapIncompatibleAddressPatterns = [ |
| 9 | RegExp("^(?:${AddressValidator.silentPaymentAddressPatternMainnet})\$"), |
| 10 | RegExp("^(?:${AddressValidator.silentPaymentAddressPatternTestnet})\$"), |
| 11 | RegExp("^(?:${AddressValidator.mWebAddressPattern})\$"), |
| 12 | ]; |
| 13 | |
| 14 | static bool _isSwapIncompatibleAddress(String address) => |
| 15 | _swapIncompatibleAddressPatterns.any((pattern) => pattern.hasMatch(address)); |
| 16 | |
| 17 | static AnyPayDecision route( |
| 18 | AnyPayRequest request, |
| 19 | WalletSnapshot snapshot, |
| 20 | AnyPayResolution resolution, |
| 21 | ) { |
| 22 | if (request.rawInput.trim().isEmpty) { |
| 23 | return const AnyPayEmptyInput(); |
| 24 | } |
| 25 | |
| 26 | final detection = request.detection; |
| 27 | final detectedType = detection.detectedWalletType; |
| 28 | |
| 29 | if (!detection.isValid || detectedType == null) { |
| 30 | return const AnyPayApplyToCurrentWallet(); |
| 31 | } |
| 32 | |
| 33 | if (detectedType == WalletType.solana && !snapshot.hasSolanaProxy) { |
| 34 | return const AnyPayApplyToCurrentWallet(); |
| 35 | } |
| 36 | |
| 37 | if (detectedType == WalletType.tron && !snapshot.hasTronProxy) { |
| 38 | return const AnyPayApplyToCurrentWallet(); |
| 39 | } |
| 40 | |
| 41 | if (isEVMCompatibleChain(detectedType)) { |
| 42 | if (!snapshot.hasEvmProxy) { |
| 43 | return const AnyPayApplyToCurrentWallet(); |
| 44 | } |
| 45 | |
| 46 | return _routeEvm(request, snapshot, resolution); |
| 47 | } |
| 48 | |
| 49 | if (resolution is TokenUnknown) { |
| 50 | return AnyPayUnsupportedToken(resolution.contract); |
| 51 | } |
| 52 | |
| 53 | if (detectedType == snapshot.type) { |
| 54 | return AnyPayApplyToCurrentWallet( |
| 55 | token: resolution is TokenResolved ? resolution.token : null, |
| 56 | amountOverride: resolution is TokenResolved ? resolution.amountOverride : null, |
| 57 | fallbackCurrency: _nativeRequestCurrency(request), |
| 58 | ); |
| 59 | } |
| 60 | |
| 61 | return AnyPayCrossChainPayment( |
| 62 | targetWalletType: detectedType, |
| 63 | wallets: snapshot.walletsOfType(detectedType), |
| 64 | token: resolution is TokenResolved ? resolution.token : null, |
| 65 | amountOverride: resolution is TokenResolved ? resolution.amountOverride : null, |
| 66 | canSwap: !_isSwapIncompatibleAddress(request.address), |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | static AnyPayDecision _routeEvm( |
| 71 | AnyPayRequest request, |
| 72 | WalletSnapshot snapshot, |
| 73 | AnyPayResolution resolution, |
| 74 | ) { |
| 75 | final requestedChainId = requestedEvmChainId(request, snapshot); |
| 76 | |
| 77 | if (requestedChainId == null) { |
| 78 | if (snapshot.currentWalletIsEvm) { |
| 79 | return const AnyPayApplyToCurrentWallet(); |
| 80 | } |
| 81 | |
| 82 | return const AnyPayEvmNetworkChoice(); |
| 83 | } |
| 84 | |
| 85 | if (!snapshot.supportedEvmChains.containsKey(requestedChainId)) { |
| 86 | return AnyPayUnsupportedNetwork(requestedChainId); |
| 87 | } |
| 88 | |
| 89 | if (resolution is TokenUnknown) { |
| 90 | return AnyPayUnsupportedToken(resolution.contract); |
| 91 | } |
| 92 | |
| 93 | int targetChainId = requestedChainId; |
| 94 | if (resolution is TokenResolved && resolution.chainId != null) { |
| 95 | targetChainId = resolution.chainId!; |
| 96 | } |
| 97 | |
| 98 | final token = resolution is TokenResolved ? resolution.token : null; |
| 99 | final amountOverride = resolution is TokenResolved ? resolution.amountOverride : null; |
| 100 | |
| 101 | if (snapshot.currentWalletIsEvm && snapshot.currentChainId == targetChainId) { |
| 102 | return AnyPayApplyToCurrentWallet( |
| 103 | token: token, |
| 104 | amountOverride: amountOverride, |
| 105 | fallbackCurrency: _nativeRequestCurrency(request), |
| 106 | ); |
| 107 | } |
| 108 | |
| 109 | final targetWalletType = snapshot.supportedEvmChains[targetChainId]!; |
| 110 | |
| 111 | return AnyPayCrossChainPayment( |
| 112 | targetWalletType: targetWalletType, |
| 113 | targetChainId: targetChainId, |
| 114 | wallets: snapshot.walletsOfType(targetWalletType), |
| 115 | token: token, |
| 116 | amountOverride: amountOverride, |
| 117 | ); |
| 118 | } |
| 119 | |
| 120 | static CryptoCurrency? _nativeRequestCurrency(AnyPayRequest request) { |
| 121 | if (request.paymentRequest.scheme.isEmpty || request.hasContract || request.amount.isEmpty) { |
| 122 | return null; |
| 123 | } |
| 124 | return request.detection.detectedCurrency; |
| 125 | } |
| 126 | |
| 127 | static int? requestedEvmChainId(AnyPayRequest request, WalletSnapshot snapshot) { |
| 128 | final binding = request.chainBinding; |
| 129 | |
| 130 | if (binding is ExplicitEvmChain) { |
| 131 | return binding.chainId; |
| 132 | } |
| 133 | |
| 134 | if (binding is ChainlessEvm) { |
| 135 | // the eip 681 spec says we should use the current chain id if there is no chainId in the incoming request |
| 136 | return snapshot.currentWalletIsEvm ? snapshot.currentChainId ?? 1 : 1; |
| 137 | } |
| 138 | |
| 139 | return null; |
| 140 | } |
| 141 | } |