| 1 | import 'dart:async'; |
| 2 | |
| 3 | import 'package:cake_wallet/core/universal_address_detector.dart'; |
| 4 | import 'package:cake_wallet/evm/evm.dart'; |
| 5 | import 'package:cake_wallet/reactions/wallet_connect.dart'; |
| 6 | import 'package:cake_wallet/solana/solana.dart'; |
| 7 | import 'package:cake_wallet/tron/tron.dart'; |
| 8 | import 'package:cake_wallet/store/app_store.dart'; |
| 9 | import 'package:cw_core/wallet_type.dart'; |
| 10 | import 'package:cw_core/wallet_info.dart'; |
| 11 | import 'package:cw_core/utils/print_verbose.dart'; |
| 12 | import 'package:cw_core/currency_for_wallet_type.dart'; |
| 13 | import 'package:cw_core/crypto_currency.dart'; |
| 14 | import 'package:mobx/mobx.dart'; |
| 15 | |
| 16 | part 'payment_view_model.g.dart'; |
| 17 | |
| 18 | class PaymentViewModel = PaymentViewModelBase with _$PaymentViewModel; |
| 19 | |
| 20 | abstract class PaymentViewModelBase with Store { |
| 21 | PaymentViewModelBase({required this.appStore}); |
| 22 | |
| 23 | final AppStore appStore; |
| 24 | |
| 25 | @observable |
| 26 | WalletType? detectedWalletType; |
| 27 | |
| 28 | @observable |
| 29 | AddressDetectionResult? _lastDetectionResult; |
| 30 | |
| 31 | @computed |
| 32 | int? get detectedChainId { |
| 33 | if (detectedWalletType == null) return null; |
| 34 | |
| 35 | if (!isEVMCompatibleChain(detectedWalletType!)) return null; |
| 36 | |
| 37 | if (_lastDetectionResult?.chainId != null) { |
| 38 | return _lastDetectionResult!.chainId; |
| 39 | } |
| 40 | |
| 41 | // If detected wallet type is EVM-compatible, get chainId from detected currency |
| 42 | if (_lastDetectionResult?.detectedCurrency != null) { |
| 43 | return getChainIdByCryptoCurrency(_lastDetectionResult!.detectedCurrency!); |
| 44 | } |
| 45 | |
| 46 | return evm?.getChainIdByWalletType(detectedWalletType!); |
| 47 | } |
| 48 | |
| 49 | @observable |
| 50 | bool isProcessing = false; |
| 51 | |
| 52 | @computed |
| 53 | WalletType get currentWalletType => appStore.wallet!.type; |
| 54 | |
| 55 | @computed |
| 56 | int? get currentChainId { |
| 57 | if (!isEVMCompatibleChain(currentWalletType) || evm == null) return null; |
| 58 | |
| 59 | return evm!.getSelectedChainId(appStore.wallet!); |
| 60 | } |
| 61 | |
| 62 | /// Main entry point - detect address type and check compatibility |
| 63 | @action |
| 64 | Future<PaymentFlowResult> processAddress(String addressData) async { |
| 65 | try { |
| 66 | detectedWalletType = null; |
| 67 | isProcessing = true; |
| 68 | |
| 69 | // Detect address type |
| 70 | final detectionResult = UniversalAddressDetector.detectAddress(addressData); |
| 71 | |
| 72 | _lastDetectionResult = detectionResult; |
| 73 | detectedWalletType = detectionResult.detectedWalletType; |
| 74 | |
| 75 | if (!detectionResult.isValid || detectedWalletType == null) { |
| 76 | return PaymentFlowResult.incompatible('Unable to detect address type'); |
| 77 | } |
| 78 | |
| 79 | if (detectedWalletType == WalletType.solana && solana == null) { |
| 80 | return PaymentFlowResult.incompatible('Solana is not available in this app build.'); |
| 81 | } |
| 82 | if (detectedWalletType == WalletType.tron && tron == null) { |
| 83 | return PaymentFlowResult.incompatible('Tron is not available in this app build.'); |
| 84 | } |
| 85 | if (isEVMCompatibleChain(detectedWalletType!) && evm == null) { |
| 86 | return PaymentFlowResult.incompatible( |
| 87 | 'Ethereum-compatible addresses are not supported in this app build.', |
| 88 | ); |
| 89 | } |
| 90 | |
| 91 | final currentWallet = appStore.wallet; |
| 92 | |
| 93 | if (currentWallet != null && |
| 94 | currentWallet.type == detectedWalletType && |
| 95 | !isEVMCompatibleChain(detectedWalletType!)) { |
| 96 | return PaymentFlowResult.currentWalletCompatible(detectionResult); |
| 97 | } |
| 98 | |
| 99 | if (detectedWalletType == WalletType.solana) { |
| 100 | final compatibleWallets = await getWalletsByType(WalletType.solana); |
| 101 | return PaymentFlowResult.solanaTokenSelection( |
| 102 | detectionResult, |
| 103 | compatibleWallets: compatibleWallets, |
| 104 | wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null, |
| 105 | ); |
| 106 | } |
| 107 | |
| 108 | if (detectedWalletType == WalletType.tron) { |
| 109 | final compatibleWallets = await getWalletsByType(WalletType.tron); |
| 110 | return PaymentFlowResult.tronTokenSelection( |
| 111 | detectionResult, |
| 112 | compatibleWallets: compatibleWallets, |
| 113 | wallet: compatibleWallets.isNotEmpty ? compatibleWallets.first : null, |
| 114 | ); |
| 115 | } |
| 116 | |
| 117 | if (isEVMCompatibleChain(detectedWalletType!)) { |
| 118 | return PaymentFlowResult.evmNetworkSelection(detectionResult); |
| 119 | } |
| 120 | |
| 121 | final compatibleWallets = await getWalletsByType(detectedWalletType!); |
| 122 | |
| 123 | switch (compatibleWallets.length) { |
| 124 | case 0: |
| 125 | return PaymentFlowResult.noWallets(detectedWalletType!, detectionResult); |
| 126 | case 1: |
| 127 | return PaymentFlowResult.singleWallet(compatibleWallets.first, detectionResult); |
| 128 | default: |
| 129 | return PaymentFlowResult.multipleWallets(compatibleWallets, detectionResult); |
| 130 | } |
| 131 | } catch (e) { |
| 132 | printV('PaymentViewModel error: $e'); |
| 133 | return PaymentFlowResult.error('Payment processing failed: $e'); |
| 134 | } finally { |
| 135 | isProcessing = false; |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | @action |
| 140 | Future<void> selectChain() async { |
| 141 | if (detectedWalletType == null || evm == null || detectedChainId == null) return; |
| 142 | |
| 143 | final node = |
| 144 | appStore.settingsStore.getCurrentNode(detectedWalletType!, chainId: detectedChainId); |
| 145 | |
| 146 | await evm!.selectChain(appStore.wallet!, detectedChainId!, node: node); |
| 147 | } |
| 148 | |
| 149 | void applyManualEvmSelection(AddressDetectionResult detection) { |
| 150 | runInAction(() { |
| 151 | _lastDetectionResult = detection; |
| 152 | detectedWalletType = detection.detectedWalletType; |
| 153 | }); |
| 154 | } |
| 155 | |
| 156 | Future<List<WalletInfo>> getWalletsByType(WalletType walletType) async { |
| 157 | return (await WalletInfo.getAll()).where((wallet) => wallet.type == walletType).toList(); |
| 158 | } |
| 159 | |
| 160 | Future<List<WalletInfo>> getEVMCompatibleWallets() async { |
| 161 | final allWallets = await WalletInfo.getAll(); |
| 162 | return allWallets.where((wallet) => isEVMCompatibleChain(wallet.type)).toList(); |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | class PaymentFlowResult { |
| 167 | final PaymentFlowType type; |
| 168 | final String? message; |
| 169 | final WalletInfo? wallet; |
| 170 | final int? chainId; |
| 171 | final List<WalletInfo> wallets; |
| 172 | final WalletType? walletType; |
| 173 | final AddressDetectionResult? addressDetectionResult; |
| 174 | |
| 175 | PaymentFlowResult._({ |
| 176 | required this.type, |
| 177 | this.message, |
| 178 | this.wallet, |
| 179 | this.chainId, |
| 180 | this.wallets = const [], |
| 181 | this.walletType, |
| 182 | this.addressDetectionResult, |
| 183 | }); |
| 184 | |
| 185 | /// EVM address detected - needs network selection |
| 186 | /// We'll also take note of the number of compatible wallets for EVM ecosystem |
| 187 | factory PaymentFlowResult.evmNetworkSelection( |
| 188 | AddressDetectionResult addressDetectionResult, { |
| 189 | List<WalletInfo>? compatibleWallets, |
| 190 | WalletInfo? wallet, |
| 191 | }) { |
| 192 | int? chainId = addressDetectionResult.chainId; |
| 193 | if (chainId == null && addressDetectionResult.detectedCurrency != null) { |
| 194 | chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!); |
| 195 | } |
| 196 | if (chainId == null && addressDetectionResult.detectedWalletType != null) { |
| 197 | chainId = evm?.getChainIdByWalletType(addressDetectionResult.detectedWalletType!); |
| 198 | } |
| 199 | |
| 200 | return PaymentFlowResult._( |
| 201 | type: PaymentFlowType.evmNetworkSelection, |
| 202 | addressDetectionResult: addressDetectionResult, |
| 203 | walletType: addressDetectionResult.detectedWalletType, |
| 204 | chainId: chainId, |
| 205 | wallets: compatibleWallets ?? [], |
| 206 | wallet: wallet, |
| 207 | ); |
| 208 | } |
| 209 | |
| 210 | /// Solana address detected - needs token selection |
| 211 | factory PaymentFlowResult.solanaTokenSelection( |
| 212 | AddressDetectionResult addressDetectionResult, { |
| 213 | List<WalletInfo>? compatibleWallets, |
| 214 | WalletInfo? wallet, |
| 215 | }) => |
| 216 | PaymentFlowResult._( |
| 217 | type: PaymentFlowType.solanaTokenSelection, |
| 218 | addressDetectionResult: addressDetectionResult, |
| 219 | walletType: WalletType.solana, |
| 220 | wallets: compatibleWallets ?? [], |
| 221 | wallet: wallet, |
| 222 | ); |
| 223 | |
| 224 | /// Tron address detected - needs token selection |
| 225 | factory PaymentFlowResult.tronTokenSelection( |
| 226 | AddressDetectionResult addressDetectionResult, { |
| 227 | List<WalletInfo>? compatibleWallets, |
| 228 | WalletInfo? wallet, |
| 229 | }) => |
| 230 | PaymentFlowResult._( |
| 231 | type: PaymentFlowType.tronTokenSelection, |
| 232 | addressDetectionResult: addressDetectionResult, |
| 233 | walletType: WalletType.tron, |
| 234 | wallets: compatibleWallets ?? [], |
| 235 | wallet: wallet, |
| 236 | ); |
| 237 | |
| 238 | /// Current wallet is compatible |
| 239 | factory PaymentFlowResult.currentWalletCompatible( |
| 240 | AddressDetectionResult addressDetectionResult) => |
| 241 | PaymentFlowResult._( |
| 242 | type: PaymentFlowType.currentWalletCompatible, |
| 243 | addressDetectionResult: addressDetectionResult, |
| 244 | ); |
| 245 | |
| 246 | /// Single compatible wallet available |
| 247 | factory PaymentFlowResult.singleWallet( |
| 248 | WalletInfo wallet, |
| 249 | AddressDetectionResult addressDetectionResult, |
| 250 | ) { |
| 251 | int? chainId = addressDetectionResult.chainId; |
| 252 | if (chainId == null && addressDetectionResult.detectedCurrency != null) { |
| 253 | chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!); |
| 254 | } |
| 255 | |
| 256 | if (chainId == null && isEVMCompatibleChain(wallet.type)) { |
| 257 | chainId = evm?.getChainIdByWalletType(wallet.type); |
| 258 | } |
| 259 | |
| 260 | return PaymentFlowResult._( |
| 261 | type: PaymentFlowType.singleWallet, |
| 262 | wallet: wallet, |
| 263 | walletType: wallet.type, |
| 264 | chainId: chainId, |
| 265 | addressDetectionResult: addressDetectionResult, |
| 266 | ); |
| 267 | } |
| 268 | |
| 269 | /// Multiple compatible wallets available |
| 270 | factory PaymentFlowResult.multipleWallets( |
| 271 | List<WalletInfo> wallets, |
| 272 | AddressDetectionResult addressDetectionResult, |
| 273 | ) { |
| 274 | int? chainId = addressDetectionResult.chainId; |
| 275 | if (chainId == null && addressDetectionResult.detectedCurrency != null) { |
| 276 | chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!); |
| 277 | } |
| 278 | if (chainId == null && isEVMCompatibleChain(wallets.first.type)) { |
| 279 | chainId = evm?.getChainIdByWalletType(wallets.first.type); |
| 280 | } |
| 281 | |
| 282 | return PaymentFlowResult._( |
| 283 | type: PaymentFlowType.multipleWallets, |
| 284 | wallets: wallets, |
| 285 | walletType: wallets.first.type, |
| 286 | addressDetectionResult: addressDetectionResult, |
| 287 | chainId: chainId, |
| 288 | ); |
| 289 | } |
| 290 | |
| 291 | /// No compatible wallets available |
| 292 | factory PaymentFlowResult.noWallets( |
| 293 | WalletType walletType, |
| 294 | AddressDetectionResult addressDetectionResult, |
| 295 | ) { |
| 296 | int? chainId = addressDetectionResult.chainId; |
| 297 | if (chainId == null && addressDetectionResult.detectedCurrency != null) { |
| 298 | chainId = getChainIdByCryptoCurrency(addressDetectionResult.detectedCurrency!); |
| 299 | } |
| 300 | if (chainId == null && isEVMCompatibleChain(walletType)) { |
| 301 | chainId = evm?.getChainIdByWalletType(walletType); |
| 302 | } |
| 303 | |
| 304 | return PaymentFlowResult._( |
| 305 | type: PaymentFlowType.noWallets, |
| 306 | walletType: walletType, |
| 307 | addressDetectionResult: addressDetectionResult, |
| 308 | chainId: chainId, |
| 309 | ); |
| 310 | } |
| 311 | |
| 312 | /// Error occurred |
| 313 | factory PaymentFlowResult.error(String message) => |
| 314 | PaymentFlowResult._(type: PaymentFlowType.error, message: message); |
| 315 | |
| 316 | /// Incompatible address |
| 317 | factory PaymentFlowResult.incompatible(String message) => |
| 318 | PaymentFlowResult._(type: PaymentFlowType.incompatible, message: message); |
| 319 | |
| 320 | CryptoCurrency? get detectedCurrency { |
| 321 | if (type == PaymentFlowType.evmNetworkSelection || |
| 322 | type == PaymentFlowType.solanaTokenSelection || |
| 323 | type == PaymentFlowType.tronTokenSelection) { |
| 324 | return addressDetectionResult?.detectedCurrency; |
| 325 | } |
| 326 | if (walletType != null) { |
| 327 | if (isEVMCompatibleChain(walletType!)) { |
| 328 | return walletTypeToCryptoCurrency(walletType!, chainId: chainId); |
| 329 | } |
| 330 | |
| 331 | return walletTypeToCryptoCurrency(walletType!); |
| 332 | } |
| 333 | return null; |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | enum PaymentFlowType { |
| 338 | currentWalletCompatible, |
| 339 | singleWallet, |
| 340 | multipleWallets, |
| 341 | noWallets, |
| 342 | evmNetworkSelection, |
| 343 | solanaTokenSelection, |
| 344 | tronTokenSelection, |
| 345 | error, |
| 346 | incompatible, |
| 347 | } |