diff --git a/lib/buy/buy_quote.dart b/lib/buy/buy_quote.dart index f2f53212e..0a0645e7d 100644 --- a/lib/buy/buy_quote.dart +++ b/lib/buy/buy_quote.dart @@ -22,19 +22,6 @@ extension RecommendationTitle on ProviderRecommendation { } } -ProviderRecommendation? getRecommendationFromString(String title) { - switch (title) { - case 'BEST RATE': - return ProviderRecommendation.bestRate; - case 'LowKyc': - return ProviderRecommendation.lowKyc; - case 'SuccessRate': - return ProviderRecommendation.successRate; - default: - return null; - } -} - class Quote extends SelectableOption { Quote({ required this.rate, @@ -135,69 +122,6 @@ class Quote extends SelectableOption { set setLimits(Limits limits) => this.limits = limits; - factory Quote.fromOnramperJson(Map json, bool isBuyAction, - Map metaData, PaymentType paymentType, String? customPaymentMethodType) { - final rate = _toDouble(json['rate']) ?? 0.0; - final networkFee = _toDouble(json['networkFee']) ?? 0.0; - final transactionFee = _toDouble(json['transactionFee']) ?? 0.0; - final feeAmount = double.parse((networkFee + transactionFee).toStringAsFixed(2)); - - final rampId = json['ramp'] as String? ?? ''; - final rampData = metaData[rampId] ?? {}; - final rampName = rampData['displayName'] as String? ?? ''; - final rampIconPath = rampData['svg'] as String? ?? ''; - - final recommendations = json['recommendations'] != null - ? List.from(json['recommendations'] as List) - : []; - - final enumRecommendations = recommendations - .map((e) => getRecommendationFromString(e)) - .whereType() - .toList(); - - final availablePaymentMethods = json['availablePaymentMethods'] as List? ?? []; - double minLimit = 0.0; - double maxLimit = double.infinity; - - for (var paymentMethod in availablePaymentMethods) { - if (paymentMethod is Map) { - final details = paymentMethod['details'] as Map?; - - if (details != null) { - final limits = details['limits'] as Map?; - - if (limits != null && limits.isNotEmpty) { - final firstLimitEntry = limits.values.first as Map?; - if (firstLimitEntry != null) { - minLimit = _toDouble(firstLimitEntry['min'])?.roundToDouble() ?? 0.0; - maxLimit = _toDouble(firstLimitEntry['max'])?.roundToDouble() ?? double.infinity; - break; - } - } - } - } - } - - return Quote( - rate: rate, - feeAmount: feeAmount, - networkFee: networkFee, - transactionFee: transactionFee, - payout: json['payout'] as double? ?? 0.0, - rampId: rampId, - rampName: rampName, - rampIconPath: rampIconPath, - paymentType: paymentType, - customPaymentMethodType: customPaymentMethodType, - quoteId: json['quoteId'] as String? ?? '', - recommendations: enumRecommendations, - provider: ProvidersHelper.getProviderByType(ProviderType.onramper), - isBuyAction: isBuyAction, - limits: Limits(min: minLimit, max: maxLimit), - ); - } - factory Quote.fromMoonPayJson( Map json, bool isBuyAction, PaymentType paymentType) { final rate = isBuyAction @@ -233,92 +157,6 @@ class Quote extends SelectableOption { ); } - factory Quote.fromDFXJson( - Map json, - bool isBuyAction, - PaymentType paymentType, - ) { - final rate = _toDouble(json['exchangeRate']) ?? 0.0; - final fees = json['fees'] as Map; - - final minVolume = _toDouble(json['minVolume']) ?? 0.0; - final maxVolume = _toDouble(json['maxVolume']) ?? double.infinity; - - return Quote( - rate: isBuyAction ? rate : 1 / rate, - feeAmount: _toDouble(json['feeAmount']) ?? 0.0, - networkFee: _toDouble(fees['network']) ?? 0.0, - transactionFee: _toDouble(fees['rate']) ?? 0.0, - payout: _toDouble(json['payout']) ?? 0.0, - paymentType: paymentType, - recommendations: [ProviderRecommendation.lowKyc], - provider: ProvidersHelper.getProviderByType(ProviderType.dfx), - isBuyAction: isBuyAction, - limits: Limits(min: minVolume, max: maxVolume), - ); - } - - factory Quote.fromRobinhoodJson( - Map json, bool isBuyAction, PaymentType paymentType) { - final networkFee = json['networkFee'] as Map; - final processingFee = json['processingFee'] as Map; - final networkFeeAmount = _toDouble(networkFee['fiatAmount']) ?? 0.0; - final transactionFeeAmount = _toDouble(processingFee['fiatAmount']) ?? 0.0; - final feeAmount = double.parse((networkFeeAmount + transactionFeeAmount).toStringAsFixed(2)); - - return Quote( - rate: _toDouble(json['price']) ?? 0.0, - feeAmount: feeAmount, - networkFee: _toDouble(networkFee['fiatAmount']) ?? 0.0, - transactionFee: _toDouble(processingFee['fiatAmount']) ?? 0.0, - payout: _toDouble(json['cryptoAmount']) ?? 0.0, - paymentType: paymentType, - recommendations: [], - provider: ProvidersHelper.getProviderByType(ProviderType.robinhood), - isBuyAction: isBuyAction, - limits: Limits(min: 0.0, max: double.infinity), - ); - } - - factory Quote.fromMeldJson(Map json, bool isBuyAction, PaymentType paymentType) { - final quotes = json['quotes'][0] as Map; - return Quote( - rate: quotes['exchangeRate'] as double? ?? 0.0, - feeAmount: quotes['totalFee'] as double? ?? 0.0, - networkFee: quotes['networkFee'] as double? ?? 0.0, - transactionFee: quotes['transactionFee'] as double? ?? 0.0, - payout: quotes['payout'] as double? ?? 0.0, - paymentType: paymentType, - recommendations: [], - provider: ProvidersHelper.getProviderByType(ProviderType.meld), - isBuyAction: isBuyAction, - limits: Limits(min: 0.0, max: double.infinity), - ); - } - - factory Quote.fromKryptonimJson( - Map json, bool isBuyAction, PaymentType paymentType) { - final fees = json['fees'] as Map; - // final rate = _toDouble(json['rate']) ?? 0.0; - final limits = json['limits'] as Map; - final minLimit = _toDouble(limits['min_amount']) ?? 0.0; - final maxLimit = _toDouble(limits['max_amount']) ?? double.infinity; - final convertedAmount = _toDouble(json['converted_amount']) ?? 0.0; - final amount = _toDouble(json['amount']) ?? 0.0; - final calculatedRate = amount / convertedAmount; - return Quote( - rate: calculatedRate, - feeAmount: _toDouble(fees['totalFee']) ?? 0.0, - networkFee: _toDouble(fees['network_fee']) ?? 0.0, - transactionFee: _toDouble(fees['operation_fee']) ?? 0.0, - payout: _toDouble(json['amount']) ?? 0.0, - paymentType: paymentType, - recommendations: [], - provider: ProvidersHelper.getProviderByType(ProviderType.kriptonim), - isBuyAction: isBuyAction, - limits: Limits(min: minLimit, max: maxLimit)); - } - static double? _toDouble(dynamic value) { if (value is int) { return value.toDouble(); diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart deleted file mode 100644 index 7b0ee8ae1..000000000 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ /dev/null @@ -1,421 +0,0 @@ -import 'dart:convert'; -import 'dart:developer'; - -import 'package:hash_wallet/buy/buy_provider.dart'; -import 'package:hash_wallet/buy/buy_quote.dart'; -import 'package:hash_wallet/buy/pairs_utils.dart'; -import 'package:hash_wallet/buy/payment_method.dart'; -import 'package:hash_wallet/entities/fiat_currency.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/routes.dart'; -import 'package:hash_wallet/src/screens/connect_device/connect_device_page.dart'; -import 'package:hash_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:hash_wallet/utils/show_pop_up.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class DFXBuyProvider extends BuyProvider { - DFXBuyProvider({ - required WalletBase wallet, - bool isTestEnvironment = false, - HardwareWalletViewModel? hardwareWalletVM, - }) : super( - wallet: wallet, - isTestEnvironment: isTestEnvironment, - hardwareWalletVM: hardwareWalletVM, - supportedCryptoList: supportedCryptoToFiatPairs( - notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat), - supportedFiatList: supportedFiatToCryptoPairs( - notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto), - ); - - static const _baseUrl = 'api.dfx.swiss'; - - // static const _signMessagePath = '/v1/auth/signMessage'; - static const _authPath = '/v1/auth'; - static const walletName = 'CakeWallet'; - - static final List _supportedCrypto = [ - CryptoCurrency.xmr, - CryptoCurrency.btc, - CryptoCurrency.eth, - CryptoCurrency.maticpoly, - ]; - static final List _notSupportedCrypto = CryptoCurrency.all - .where((crypto) => !_supportedCrypto.contains(crypto) || ["ETH", "POL"].contains(crypto.tag)) - .toList(); - - static final List _supportedFiat = [FiatCurrency.chf, FiatCurrency.eur]; - static final List _notSupportedFiat = - FiatCurrency.all.where((fiat) => !_supportedFiat.contains(fiat)).toList(); - - @override - String get title => 'DFX.swiss'; - - @override - String get providerDescription => S.current.dfx_option_description; - - @override - String get lightIcon => 'assets/images/dfx_light.png'; - - @override - String get darkIcon => 'assets/images/dfx_dark.png'; - - @override - bool get isAggregator => false; - - String get blockchain { - switch (wallet.type) { - case WalletType.bitcoin: - return 'Bitcoin'; - default: - return walletTypeToString(wallet.type); - } - } - - - Future getSignMessage(String walletAddress) async => - "By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress"; - - // Lets keep this just in case, but we can avoid this API Call - // Future getSignMessage() async { - // final uri = Uri.https(_baseUrl, _signMessagePath, {'address': walletAddress}); - // - // final response = await http.get(uri, headers: {'accept': 'application/json'}); - // - // if (response.statusCode == 200) { - // final responseBody = jsonDecode(response.body); - // return responseBody['message'] as String; - // } else { - // throw Exception( - // 'Failed to get sign message. Status: ${response.statusCode} ${response.body}'); - // } - // } - - Future auth(String walletAddress) async { - final signMessage = await getSignature(await getSignMessage(walletAddress), walletAddress); - - final requestBody = jsonEncode({ - 'wallet': walletName, - 'address': walletAddress, - 'signature': signMessage, - }); - - final uri = Uri.https(_baseUrl, _authPath); - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: {'Content-Type': 'application/json'}, - body: requestBody, - ); - - if (response.statusCode == 201) { - final responseBody = jsonDecode(response.body); - return responseBody['accessToken'] as String; - } else if (response.statusCode == 403) { - final responseBody = jsonDecode(response.body); - final message = responseBody['message'] ?? 'Service unavailable in your country'; - throw Exception(message); - } else { - throw Exception('Failed to sign up. ${_getErrorMessage(response.statusCode, response.body)}'); - } - } - - Future getSignature(String message, String walletAddress) async { - switch (wallet.type) { - case WalletType.ethereum: - case WalletType.polygon: - case WalletType.base: - case WalletType.arbitrum: - case WalletType.bsc: - return wallet.signMessage(message); - case WalletType.monero: - case WalletType.litecoin: - case WalletType.bitcoin: - case WalletType.bitcoinCash: - return wallet.signMessage(message, address: walletAddress); - default: - throw Exception("WalletType is not available for DFX ${wallet.type}"); - } - } - - Future> fetchFiatCredentials(String fiatCurrency) async { - final url = Uri.https(_baseUrl, '/v1/fiat'); - - try { - final response = await ProxyWrapper().get( - clearnetUri: url, - headers: {'accept': 'application/json'}); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body) as List; - for (final item in data) { - if (item['name'] == fiatCurrency) return item as Map; - } - log('DFX does not support fiat: $fiatCurrency'); - return {}; - } else { - log('DFX Failed to fetch fiat currencies: ${response.statusCode}'); - return {}; - } - } catch (e) { - printV('DFX Error fetching fiat currencies: $e'); - return {}; - } - } - - Future> fetchAssetCredential(String assetsName) async { - final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain}); - - try { - final response = await ProxyWrapper().get(clearnetUri: url, headers: {'accept': 'application/json'}); - - if (response.statusCode == 200) { - final responseData = jsonDecode(response.body); - - if (responseData is List && responseData.isNotEmpty) { - for (final i in responseData) { - if (assetsName.toLowerCase() == i["dexName"].toString().toLowerCase()) { - return i as Map; - } - } - return responseData.first as Map; - } else if (responseData is Map) { - return responseData; - } else { - log('DFX: Does not support this asset name : ${blockchain}'); - } - } else { - log('DFX: Failed to fetch assets: ${response.statusCode}'); - } - } catch (e) { - log('DFX: Error fetching assets: $e'); - } - return {}; - } - - Future> getAvailablePaymentTypes( - String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { - final List paymentMethods = []; - - if (isBuyAction) { - final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency); - if (fiatBuyCredentials.isNotEmpty) { - fiatBuyCredentials.forEach((key, value) { - if (key == 'limits') { - final limits = value as Map; - limits.forEach((paymentMethodKey, paymentMethodValue) { - final min = _toDouble(paymentMethodValue['minVolume']); - final max = _toDouble(paymentMethodValue['maxVolume']); - if (min != null && max != null && min > 0 && max > 0) { - final paymentMethod = PaymentMethod.fromDFX( - paymentMethodKey, _getPaymentTypeByString(paymentMethodKey)); - paymentMethods.add(paymentMethod); - } - }); - } - }); - } - } else { - final assetCredentials = await fetchAssetCredential(cryptoCurrency.title); - if (assetCredentials.isNotEmpty) { - if (assetCredentials['sellable'] == true) { - final availablePaymentTypes = [ - PaymentType.bankTransfer, - PaymentType.creditCard, - PaymentType.sepa - ]; - availablePaymentTypes.forEach((element) { - final paymentMethod = PaymentMethod.fromDFX(normalizePaymentMethod(element)!, element); - paymentMethods.add(paymentMethod); - }); - } - } - } - - return paymentMethods; - } - - @override - Future?> fetchQuote( - {required CryptoCurrency cryptoCurrency, - required FiatCurrency fiatCurrency, - required double amount, - required bool isBuyAction, - required String walletAddress, - PaymentType? paymentType, - String? customPaymentMethodType, - String? countryCode}) async { - /// if buying with any currency other than eur or chf then DFX is not supported - - if (isBuyAction && (fiatCurrency != FiatCurrency.eur && fiatCurrency != FiatCurrency.chf)) { - return null; - } - - String? paymentMethod; - if (paymentType != null && paymentType != PaymentType.all) { - paymentMethod = normalizePaymentMethod(paymentType); - if (paymentMethod == null) paymentMethod = paymentType.name; - } else { - paymentMethod = 'Bank'; - } - - final action = isBuyAction ? 'buy' : 'sell'; - - final fiatCredentials = await fetchFiatCredentials(fiatCurrency.name.toString()); - if (fiatCredentials['id'] == null) return null; - - final assetCredentials = await fetchAssetCredential(cryptoCurrency.title.toString()); - if (assetCredentials['id'] == null) return null; - - log('DFX: Fetching $action quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod'); - - final url = Uri.https(_baseUrl, '/v1/$action/quote'); - final headers = {'accept': 'application/json', 'Content-Type': 'application/json'}; - final body = jsonEncode({ - 'currency': {'id': fiatCredentials['id'] as int}, - 'asset': {'id': assetCredentials['id']}, - 'amount': amount, - 'targetAmount': 0, - 'paymentMethod': paymentMethod, - 'discountCode': '' - }); - - try { - final response = await ProxyWrapper().put( - clearnetUri: url, - headers: headers, - body: body, - ); - - final responseData = jsonDecode(response.body); - - if (response.statusCode == 200) { - if (responseData is Map) { - final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?); - final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType); - quote.setFiatCurrency = fiatCurrency; - quote.setCryptoCurrency = cryptoCurrency; - return [quote]; - } else { - printV('DFX: Unexpected data type: ${responseData.runtimeType}'); - return null; - } - } else { - if (responseData is Map && responseData.containsKey('message')) { - printV('DFX Error: ${responseData['message']}'); - } else { - printV('DFX Failed to fetch buy quote: ${response.statusCode}'); - } - return null; - } - } catch (e) { - printV('DFX Error fetching buy quote: $e'); - return null; - } - } - - Future? launchProvider( - {required BuildContext context, - required Quote quote, - required double amount, - required bool isBuyAction, - required String cryptoCurrencyAddress, - String? countryCode}) async { - if (wallet.isHardwareWallet) { - if (!hardwareWalletVM!.isConnected) { - await Navigator.of(context).pushNamed(Routes.connectDevices, - arguments: ConnectDevicePageParams( - walletType: wallet.walletInfo.type, - hardwareWalletType: wallet.walletInfo.hardwareWalletType!, - onConnectDevice: (context, hwwVM) { - hwwVM.initWallet(wallet); - Navigator.of(context).pop(); - })); - } else { - hardwareWalletVM!.initWallet(wallet); - } - } - - try { - final actionType = isBuyAction ? '/buy' : '/sell'; - - final accessToken = await auth(cryptoCurrencyAddress); - - final uri = Uri.https('app.dfx.swiss', actionType, { - 'session': accessToken, - 'lang': 'en', - 'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(), - 'blockchain': blockchain, - 'asset-in': isBuyAction ? quote.fiatCurrency.toString() : quote.cryptoCurrency.toString(), - 'amount-in': amount.toString() - }); - - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } else { - throw Exception('Could not launch URL'); - } - } catch (e) { - await showPopUp( - context: context, - builder: (context) => AlertWithOneAction( - alertTitle: "DFX.swiss", - alertContent: '${S.of(context).buy_provider_unavailable}: $e', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()), - ); - } - } - - String? normalizePaymentMethod(PaymentType paymentMethod) { - switch (paymentMethod) { - case PaymentType.bankTransfer: - return 'Bank'; - case PaymentType.creditCard: - return 'Card'; - case PaymentType.sepa: - return 'Instant'; - default: - return null; - } - } - - PaymentType _getPaymentTypeByString(String? paymentMethod) { - switch (paymentMethod) { - case 'Bank': - return PaymentType.bankTransfer; - case 'Card': - return PaymentType.creditCard; - case 'Instant': - return PaymentType.sepa; - default: - return PaymentType.unknown; - } - } - - double? _toDouble(dynamic value) { - if (value is int) { - return value.toDouble(); - } else if (value is double) { - return value; - } - return null; - } - - String _getErrorMessage(int statusCode, String body) { - final responseBody = jsonDecode(body) as Map; - final message = responseBody['message']?.toString() ?? ''; - - if (message.contains("address must match") || message.contains("signature must match")) { - return "The wallet type must match the selected currency"; - } - - return message.isNotEmpty ? message : "Unknown error: ${statusCode}"; - } -} diff --git a/lib/buy/kryptonim/kryptonim.dart b/lib/buy/kryptonim/kryptonim.dart deleted file mode 100644 index c0411805d..000000000 --- a/lib/buy/kryptonim/kryptonim.dart +++ /dev/null @@ -1,243 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/buy/buy_provider.dart'; -import 'package:hash_wallet/buy/buy_quote.dart'; -import 'package:hash_wallet/buy/pairs_utils.dart'; -import 'package:hash_wallet/buy/payment_method.dart'; -import 'package:hash_wallet/entities/fiat_currency.dart'; -import 'package:hash_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:hash_wallet/utils/show_pop_up.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:flutter/material.dart'; -import 'dart:developer'; -import 'package:url_launcher/url_launcher.dart'; - -class KryptonimBuyProvider extends BuyProvider { - KryptonimBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) - : super( - wallet: wallet, - isTestEnvironment: isTestEnvironment, - hardwareWalletVM: null, - supportedCryptoList: supportedCryptoToFiatPairs( - notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat), - supportedFiatList: supportedFiatToCryptoPairs( - notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto)); - - static const _isProduction = true; - - static const _baseUrl = _isProduction ? 'app.kryptonim.com' : 'intg-api.kryptonim.com'; - static const _baseWidgetUrl = _isProduction ? 'buy.kryptonim.com' : 'intg.kryptonim.com'; - static const _quotePath = '/v2/ramp/buy/quotes'; - static const _merchantId = 'a70fe053'; - - static String get _kryptonimApiKey => secrets.kryptonimApiKey; - - static const List _notSupportedCrypto = []; - static const List _notSupportedFiat = []; - - @override - String get title => 'Kryptonim'; - - @override - String get providerDescription => 'Kryptonim Buy Provider'; - - @override - String get lightIcon => 'assets/images/kryptonim_light.png'; - - @override - String get darkIcon => 'assets/images/kryptonim_dark.png'; - - @override - bool get isAggregator => false; - - Future> getExchangeRates( - {required CryptoCurrency cryptoCurrency, - required String fiatCurrency, - required double amount}) async { - final url = Uri.https(_baseUrl, _quotePath, {'m': _merchantId}); - - final headers = { - 'accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': _kryptonimApiKey, - }; - - try { - final body = jsonEncode({ - 'amount': amount, - 'currency': fiatCurrency, - 'converted_currency': cryptoCurrency.title, - 'blockchain': _normalizeBlockChain(cryptoCurrency), - 'quote_currency': fiatCurrency, - }); - - final response = await ProxyWrapper().post( - clearnetUri: url, - headers: headers, - body: body, - ); - - if (response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 401) { - - return jsonDecode(response.body) as Map; - } else { - return {}; - } - } catch (e) { - return {}; - } - } - - @override - Future> getAvailablePaymentTypes( - String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { - - final data = await getExchangeRates( - cryptoCurrency: cryptoCurrency, - fiatCurrency: fiatCurrency, - amount: 100.0, - ); - - if (data.isEmpty || !data.containsKey('data')) return []; - - final paymentMethods = (data['data'] as List) - .map((e) => PaymentMethod.fromKryptonimJson(e as Map)) - .toList(); - - return paymentMethods; - } - - @override - Future?> fetchQuote({ - required CryptoCurrency cryptoCurrency, - required FiatCurrency fiatCurrency, - required double amount, - required bool isBuyAction, - required String walletAddress, - PaymentType? paymentType, - String? customPaymentMethodType, - String? countryCode, - }) async { - log('Kryptonim: Fetching quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount'); - - final data = await getExchangeRates( - cryptoCurrency: cryptoCurrency, - fiatCurrency: fiatCurrency.toString(), - amount: amount, - ); - - if (!data.containsKey('data') || (data['data'] as List).isEmpty) { - return null; - } - - final quotesList = data['data'] as List; - - Map? selectedPaymentMethod; - - if (paymentType == PaymentType.all || paymentType == null) { - selectedPaymentMethod = quotesList.first as Map; - } else { - for (var quote in quotesList) { - final quotePaymentType = PaymentMethod.getPaymentTypeId(quote['payment_method'] as String?); - if (quotePaymentType == paymentType) { - selectedPaymentMethod = quote as Map; - break; - } - } - } - - if (selectedPaymentMethod == null) { - return null; - } - - final selectedPaymentType = - PaymentMethod.getPaymentTypeId(selectedPaymentMethod['payment_method'] as String?); - final quote = Quote.fromKryptonimJson(selectedPaymentMethod, isBuyAction, selectedPaymentType ?? PaymentType.unknown); - - quote.setFiatCurrency = fiatCurrency; - quote.setCryptoCurrency = cryptoCurrency; - - return [quote]; - } - - @override - Future? launchProvider( - {required BuildContext context, - required Quote quote, - required double amount, - required bool isBuyAction, - required String cryptoCurrencyAddress, - String? countryCode}) async { - final params = { - 'amount': amount.toInt().toString(), - 'currency': quote.fiatCurrency.name, - 'convertedCurrency': quote.cryptoCurrency.title, - 'blockchain': _normalizeBlockChain(quote.cryptoCurrency), - 'address': cryptoCurrencyAddress, - 'paymentMethod': normalizePaymentMethod(quote.paymentType), - }; - - final uri = Uri.https(_baseWidgetUrl, '/redirect-form', params); - - try { - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } else { - throw Exception('Could not launch URL'); - } - } catch (e) { - await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: "Kryptonim", - alertContent: "Payment provider is unavailable: $e", - buttonText: "OK", - buttonAction: () => Navigator.of(context).pop(), - ); - }, - ); - } - } - - String normalizePaymentMethod(PaymentType paymentType) { - switch (paymentType) { - case PaymentType.bankTransfer: - return 'bank'; - case PaymentType.creditCard: - case PaymentType.debitCard: - return 'card'; - default: - return paymentType.name.toLowerCase(); - } - } - - String _normalizeBlockChain(CryptoCurrency cur) { - String? blockchain = switch (cur.tag) { - 'ETH' => 'Ethereum', - 'BASE' => 'Base', - 'ARB' => 'Arbitrum', - 'POL' => 'Polygon', - 'AVAXC' => 'Avalanche', - 'SOL' => 'Solana', - _ => null, - }; - - if (blockchain == null) { - blockchain = switch (cur) { - CryptoCurrency.btc => 'Bitcoin', - CryptoCurrency.ltc => 'Litecoin', - CryptoCurrency.eth => 'Ethereum', - CryptoCurrency.baseEth => 'Base', - CryptoCurrency.arbEth => 'Arbitrum', - CryptoCurrency.maticpoly => 'Matic', - _ => null, - }; - } - - return blockchain ?? cur.fullName ?? ''; - } -} diff --git a/lib/buy/meld/meld_buy_provider.dart b/lib/buy/meld/meld_buy_provider.dart deleted file mode 100644 index a77295a41..000000000 --- a/lib/buy/meld/meld_buy_provider.dart +++ /dev/null @@ -1,281 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/buy/buy_provider.dart'; -import 'package:hash_wallet/buy/buy_quote.dart'; -import 'package:hash_wallet/buy/pairs_utils.dart'; -import 'package:hash_wallet/buy/payment_method.dart'; -import 'package:hash_wallet/entities/fiat_currency.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:hash_wallet/utils/show_pop_up.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:flutter/material.dart'; -import 'dart:developer'; -import 'package:url_launcher/url_launcher.dart'; - -class MeldBuyProvider extends BuyProvider { - MeldBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) - : super( - wallet: wallet, - isTestEnvironment: isTestEnvironment, - hardwareWalletVM: null, - supportedCryptoList: supportedCryptoToFiatPairs( - notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat), - supportedFiatList: supportedFiatToCryptoPairs( - notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto)); - - static const _isProduction = false; - - static const _baseUrl = _isProduction ? 'api.meld.io' : 'api-sb.meld.io'; - static const _providersProperties = '/service-providers/properties'; - static const _paymentMethodsPath = '/payment-methods'; - static const _quotePath = '/payments/crypto/quote'; - - static const String sandboxUrl = 'sb.fluidmoney.xyz'; - static const String productionUrl = 'fluidmoney.xyz'; - - static const String _baseWidgetUrl = _isProduction ? productionUrl : sandboxUrl; - - static String get _testApiKey => secrets.meldTestApiKey; - - static const List _notSupportedCrypto = []; - static const List _notSupportedFiat = []; - - static String get _testPublicKey => '' ; //secrets.meldTestPublicKey; - - @override - String get title => 'Meld'; - - @override - String get providerDescription => 'Meld Buy Provider'; - - @override - String get lightIcon => 'assets/images/meld_logo.svg'; - - @override - String get darkIcon => 'assets/images/meld_logo.svg'; - - @override - bool get isAggregator => true; - - @override - Future> getAvailablePaymentTypes( - String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { - final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'}; - - final path = '$_providersProperties$_paymentMethodsPath'; - final url = Uri.https(_baseUrl, path, params); - - try { - final response = await ProxyWrapper().get( - clearnetUri: url, - headers: { - 'Authorization': _isProduction ? '' : _testApiKey, - 'Meld-Version': '2023-12-19', - 'accept': 'application/json', - 'content-type': 'application/json', - }, - ); - - - if (response.statusCode == 200) { - final data = jsonDecode(response.body) as List; - final paymentMethods = - data.map((e) => PaymentMethod.fromMeldJson(e as Map)).toList(); - return paymentMethods; - } else { - printV('Meld: Failed to fetch payment types'); - return List.empty(); - } - } catch (e) { - printV('Meld: Failed to fetch payment types: $e'); - return List.empty(); - } - } - - @override - Future?> fetchQuote( - {required CryptoCurrency cryptoCurrency, - required FiatCurrency fiatCurrency, - required double amount, - required bool isBuyAction, - required String walletAddress, - PaymentType? paymentType, - String? customPaymentMethodType, - String? countryCode}) async { - String? paymentMethod; - if (paymentType != null && paymentType != PaymentType.all) { - paymentMethod = normalizePaymentMethod(paymentType); - if (paymentMethod == null) paymentMethod = paymentType.name; - } - - log('Meld: Fetching buy quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount'); - - final url = Uri.https(_baseUrl, _quotePath); - final headers = { - 'Authorization': _testApiKey, - 'Meld-Version': '2023-12-19', - 'accept': 'application/json', - 'content-type': 'application/json', - }; - final body = jsonEncode({ - 'countryCode': countryCode, - 'destinationCurrencyCode': isBuyAction ? fiatCurrency.name : cryptoCurrency.title, - 'sourceAmount': amount, - 'sourceCurrencyCode': isBuyAction ? cryptoCurrency.title : fiatCurrency.name, - if (paymentMethod != null) 'paymentMethod': paymentMethod, - }); - - try { - final response = await ProxyWrapper().post( - clearnetUri: url, - headers: headers, - body: body, - ); - - - if (response.statusCode == 200) { - final data = jsonDecode(response.body) as Map; - final paymentType = _getPaymentTypeByString(data['paymentMethodType'] as String?); - final quote = Quote.fromMeldJson(data, isBuyAction, paymentType); - - quote.setFiatCurrency = fiatCurrency; - quote.setCryptoCurrency = cryptoCurrency; - - return [quote]; - } else { - return null; - } - } catch (e) { - printV('Error fetching buy quote: $e'); - return null; - } - } - - Future? launchProvider( - {required BuildContext context, - required Quote quote, - required double amount, - required bool isBuyAction, - required String cryptoCurrencyAddress, - String? countryCode}) async { - final actionType = isBuyAction ? 'BUY' : 'SELL'; - - final params = { - 'publicKey': _isProduction ? '' : _testPublicKey, - 'countryCode': countryCode, - //'paymentMethodType': normalizePaymentMethod(paymentMethod.paymentMethodType), - 'sourceAmount': amount.toString(), - 'sourceCurrencyCode': quote.fiatCurrency, - 'destinationCurrencyCode': quote.cryptoCurrency, - 'walletAddress': cryptoCurrencyAddress, - 'transactionType': actionType - }; - - final uri = Uri.https(_baseWidgetUrl, '', params); - - try { - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } else { - throw Exception('Could not launch URL'); - } - } catch (e) { - await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: "Meld", - alertContent: S.of(context).buy_provider_unavailable + ': $e', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } - } - - String? normalizePaymentMethod(PaymentType paymentType) { - switch (paymentType) { - case PaymentType.creditCard: - return 'CREDIT_DEBIT_CARD'; - case PaymentType.applePay: - return 'APPLE_PAY'; - case PaymentType.googlePay: - return 'GOOGLE_PAY'; - case PaymentType.neteller: - return 'NETELLER'; - case PaymentType.skrill: - return 'SKRILL'; - case PaymentType.sepa: - return 'SEPA'; - case PaymentType.sepaInstant: - return 'SEPA_INSTANT'; - case PaymentType.ach: - return 'ACH'; - case PaymentType.achInstant: - return 'INSTANT_ACH'; - case PaymentType.Khipu: - return 'KHIPU'; - case PaymentType.ovo: - return 'OVO'; - case PaymentType.zaloPay: - return 'ZALOPAY'; - case PaymentType.zaloBankTransfer: - return 'ZA_BANK_TRANSFER'; - case PaymentType.gcash: - return 'GCASH'; - case PaymentType.imps: - return 'IMPS'; - case PaymentType.dana: - return 'DANA'; - case PaymentType.ideal: - return 'IDEAL'; - default: - return null; - } - } - - PaymentType _getPaymentTypeByString(String? paymentMethod) { - switch (paymentMethod?.toUpperCase()) { - case 'CREDIT_DEBIT_CARD': - return PaymentType.creditCard; - case 'APPLE_PAY': - return PaymentType.applePay; - case 'GOOGLE_PAY': - return PaymentType.googlePay; - case 'NETELLER': - return PaymentType.neteller; - case 'SKRILL': - return PaymentType.skrill; - case 'SEPA': - return PaymentType.sepa; - case 'SEPA_INSTANT': - return PaymentType.sepaInstant; - case 'ACH': - return PaymentType.ach; - case 'INSTANT_ACH': - return PaymentType.achInstant; - case 'KHIPU': - return PaymentType.Khipu; - case 'OVO': - return PaymentType.ovo; - case 'ZALOPAY': - return PaymentType.zaloPay; - case 'ZA_BANK_TRANSFER': - return PaymentType.zaloBankTransfer; - case 'GCASH': - return PaymentType.gcash; - case 'IMPS': - return PaymentType.imps; - case 'DANA': - return PaymentType.dana; - case 'IDEAL': - return PaymentType.ideal; - default: - return PaymentType.all; - } - } -} diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart deleted file mode 100644 index 0ad38d574..000000000 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ /dev/null @@ -1,453 +0,0 @@ -import 'dart:convert'; -import 'dart:developer'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/buy/buy_provider.dart'; -import 'package:hash_wallet/buy/buy_quote.dart'; -import 'package:hash_wallet/buy/pairs_utils.dart'; -import 'package:hash_wallet/buy/payment_method.dart'; -import 'package:hash_wallet/entities/fiat_currency.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class OnRamperBuyProvider extends BuyProvider { - OnRamperBuyProvider( - {required WalletBase wallet, bool isTestEnvironment = false}) - : super(wallet: wallet, - isTestEnvironment: isTestEnvironment, - hardwareWalletVM: null, - supportedCryptoList: supportedCryptoToFiatPairs( - notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat), - supportedFiatList: supportedFiatToCryptoPairs( - notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto)); - - static const _baseUrl = 'buy.onramper.com'; - static const _baseApiUrl = 'api.onramper.com'; - static const _cIdBaseUrl = 'exchange-helper.such.software'; - static const quotes = '/quotes'; - static const paymentTypes = '/payment-types'; - static const supported = '/supported'; - static const defaultsAll = '/defaults/all'; - - static const List _notSupportedCrypto = []; - static const List _notSupportedFiat = []; - static Map _onrampMetadata = {}; - - String? recommendedPaymentType; - - String get _apiKey => secrets.onramperApiKey; - - String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey; - - @override - String get title => 'Onramper'; - - @override - String get providerDescription => S.current.onramper_option_description; - - @override - String get lightIcon => 'assets/images/onramper_light.png'; - - @override - String get darkIcon => 'assets/images/onramper_dark.png'; - - @override - bool get isAggregator => true; - - Future getOnramperSignature(String query) async { - final uri = Uri.https(_cIdBaseUrl, "/api/onramper"); - - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey}, - body: json.encode({'query': query}), - ); - - - if (response.statusCode == 200) { - return (jsonDecode(response.body) as Map)['signature'] as String; - } else { - throw Exception( - 'Provider currently unavailable. Status: ${response.statusCode} ${response.body}'); - } - } - - Future getRecommendedPaymentType(bool isBuyAction) async { - - final params = {'type': isBuyAction ? 'buy' : 'sell'}; - - final url = Uri.https(_baseApiUrl, '$supported$defaultsAll', params); - - try { - final response = await ProxyWrapper().get( - clearnetUri: url, - headers: {'Authorization': _apiKey, 'accept': 'application/json'}, - ); - - if (response.statusCode == 200) { - final Map data = jsonDecode(response.body) as Map; - final recommended = data['message']['recommended'] as Map; - - final recommendedPaymentType = recommended['paymentMethod'] as String?; - - return recommendedPaymentType ; - } else { - final responseBody = - jsonDecode(response.body) as Map; - printV('Failed to fetch available payment types: ${responseBody['message']}'); - } - } catch (e) { - printV('Failed to fetch available payment types: $e'); - } - return null; - } - - Future> getAvailablePaymentTypes( - String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { - - final normalizedCryptoCurrency = - cryptoCurrency.title + _getNormalizeNetwork(cryptoCurrency); - - final sourceCurrency = (isBuyAction ? fiatCurrency : normalizedCryptoCurrency).toLowerCase(); - final destinationCurrency = (isBuyAction ? normalizedCryptoCurrency : fiatCurrency).toLowerCase(); - - final params = {'type': isBuyAction ? 'buy' : 'sell', 'destination' : destinationCurrency}; - - final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$sourceCurrency', params); - - try { - final response = - await ProxyWrapper().get(clearnetUri: url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); - - if (response.statusCode == 200) { - final Map data = jsonDecode(response.body) as Map; - final List message = data['message'] as List; - - final allAvailablePaymentMethods = message - .map((item) => PaymentMethod.fromOnramperJson(item as Map)) - .toList(); - - recommendedPaymentType = await getRecommendedPaymentType(isBuyAction); - - return allAvailablePaymentMethods; - } else { - final responseBody = - jsonDecode(response.body) as Map; - printV('Failed to fetch available payment types: ${responseBody['message']}'); - return []; - } - } catch (e) { - printV('Failed to fetch available payment types: $e'); - return []; - } - } - - Future> getOnrampMetadata() async { - final url = Uri.https(_baseApiUrl, '$supported/onramps/all'); - - try { - final response = - await ProxyWrapper().get(clearnetUri: url, headers: {'Authorization': _apiKey, 'accept': 'application/json'}); - - - if (response.statusCode == 200) { - final Map data = jsonDecode(response.body) as Map; - - final List onramps = data['message'] as List; - - final Map result = { - for (var onramp in onramps) - (onramp['id'] as String): { - 'displayName': onramp['displayName'] as String, - 'svg': onramp['icons']['svg'] as String - } - }; - - return result; - } else { - printV('Failed to fetch onramp metadata'); - return {}; - } - } catch (e) { - printV('Error occurred: $e'); - return {}; - } - } - - @override - Future?> fetchQuote( - {required CryptoCurrency cryptoCurrency, - required FiatCurrency fiatCurrency, - required double amount, - required bool isBuyAction, - required String walletAddress, - PaymentType? paymentType, - String? customPaymentMethodType, - String? countryCode}) async { - String? paymentMethod; - - if (paymentType == PaymentType.all ) paymentMethod = null; - else if (paymentType == PaymentType.unknown) paymentMethod = customPaymentMethodType; - else if (paymentType != null) paymentMethod = normalizePaymentMethod(paymentType); - - final actionType = isBuyAction ? 'buy' : 'sell'; - - final normalizedCryptoCurrency = - cryptoCurrency.title + _getNormalizeNetwork(cryptoCurrency).toUpperCase(); - - final params = { - 'amount': amount.toString(), - if (paymentMethod != null) 'paymentMethod': paymentMethod, - 'clientName': 'CakeWallet', - if (actionType == 'sell') 'type': actionType, - }; - - log('Onramper: Fetching $actionType quote: ${isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name} -> ${isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod'); - - final sourceCurrency = isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency; - final destinationCurrency = isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name; - - final url = Uri.https(_baseApiUrl, '$quotes/${sourceCurrency}/${destinationCurrency}', params); - final headers = {'Authorization': _apiKey, 'accept': 'application/json'}; - - try { - final response = await ProxyWrapper().get(clearnetUri: url, headers: headers); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body) as List; - if (data.isEmpty) return null; - - List validQuotes = []; - - if (_onrampMetadata.isEmpty) _onrampMetadata = await getOnrampMetadata(); - - for (var item in data) { - - if (item['errors'] != null) continue; - - final paymentMethod = (item as Map)['paymentMethod'] as String; - - final rampId = item['ramp'] as String?; - final rampMetaData = _onrampMetadata[rampId] as Map?; - - if (rampMetaData == null) continue; - - final quote = Quote.fromOnramperJson( - item, isBuyAction, _onrampMetadata, _getPaymentTypeByString(paymentMethod), customPaymentMethodType); - quote.setFiatCurrency = fiatCurrency; - quote.setCryptoCurrency = cryptoCurrency; - validQuotes.add(quote); - } - - if (validQuotes.isEmpty) return null; - - return validQuotes; - } else { - printV('Onramper: Failed to fetch rate'); - return null; - } - } catch (e) { - printV('Onramper: Failed to fetch rate $e'); - return null; - } - } - - Future? launchProvider( - {required BuildContext context, - required Quote quote, - required double amount, - required bool isBuyAction, - required String cryptoCurrencyAddress, - String? countryCode}) async { - final actionType = isBuyAction ? 'buy' : 'sell'; - - final primaryColor = getColorStr(Theme.of(context).colorScheme.primary,); - final secondaryColor = getColorStr(Theme.of(context).colorScheme.surface); - final primaryTextColor = getColorStr(Theme.of(context).colorScheme.onSurface); - final secondaryTextColor = - getColorStr(Theme.of(context).colorScheme.onSurfaceVariant); - final containerColor = getColorStr(Theme.of(context).colorScheme.surface); - var cardColor = getColorStr(Theme.of(context).colorScheme.surfaceContainer); - - final defaultCrypto = - quote.cryptoCurrency.title + _getNormalizeNetwork(quote.cryptoCurrency).toLowerCase(); - - final paymentMethod = quote.paymentType == PaymentType.unknown ? quote.customPaymentMethodType : normalizePaymentMethod(quote.paymentType); - - final networkWallets = - '${_tagToNetwork(quote.cryptoCurrency.tag ?? quote.cryptoCurrency.title).toLowerCase()}:$cryptoCurrencyAddress'; - - final signature = await getOnramperSignature("networkWallets=$networkWallets"); - - final uri = Uri.https(_baseUrl, '', { - 'apiKey': _apiKey, - 'txnType': actionType, - 'txnFiat': quote.fiatCurrency.name, - 'txnCrypto': defaultCrypto, - 'txnAmount': amount.toString(), - 'skipTransactionScreen': "true", - if (paymentMethod != null) 'txnPaymentMethod': paymentMethod, - 'txnOnramp': quote.rampId, - 'networkWallets': networkWallets, - 'supportSwap': "false", - 'primaryColor': primaryColor, - 'secondaryColor': secondaryColor, - 'containerColor': containerColor, - 'primaryTextColor': primaryTextColor, - 'secondaryTextColor': secondaryTextColor, - 'cardColor': cardColor, - 'signature': signature, - }); - - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } else { - throw Exception('Could not launch URL ${uri.toString()}'); - } - } - - List mainCurrency = [ - CryptoCurrency.btc, - CryptoCurrency.eth, - CryptoCurrency.sol, - ]; - - String _tagToNetwork(String tag) { - switch (tag) { - case 'POL': - return 'POLYGON'; - case 'ARB': - return 'ARBITRUM'; - case 'ETH': - return 'ETHEREUM'; - case 'TRX': - return 'TRON'; - case 'SOL': - return 'SOLANA'; - case 'ZEC': - return 'ZCASH'; - default: - return tag; - } - } - - String _getNormalizeNetwork(CryptoCurrency currency) { - if (mainCurrency.contains(currency)) return ''; - - if (currency == CryptoCurrency.eos) return '_EOSIO'; - - if (currency.tag != null) return '_' + _tagToNetwork(currency.tag!); - - return '_' + (currency.fullName?.replaceAll(' ', '') ?? currency.title); - } - - String? normalizePaymentMethod(PaymentType paymentType) { - switch (paymentType) { - case PaymentType.bankTransfer: - return 'banktransfer'; - case PaymentType.creditCard: - return 'creditcard'; - case PaymentType.debitCard: - return 'debitcard'; - case PaymentType.applePay: - return 'applepay'; - case PaymentType.googlePay: - return 'googlepay'; - case PaymentType.revolutPay: - return 'revolutpay'; - case PaymentType.neteller: - return 'neteller'; - case PaymentType.skrill: - return 'skrill'; - case PaymentType.sepa: - return 'sepabanktransfer'; - case PaymentType.sepaInstant: - return 'sepainstant'; - case PaymentType.ach: - return 'ach'; - case PaymentType.achInstant: - return 'iach'; - case PaymentType.Khipu: - return 'khipu'; - case PaymentType.palomaBanktTansfer: - return 'palomabanktransfer'; - case PaymentType.ovo: - return 'ovo'; - case PaymentType.zaloPay: - return 'zalopay'; - case PaymentType.zaloBankTransfer: - return 'zalobanktransfer'; - case PaymentType.gcash: - return 'gcash'; - case PaymentType.imps: - return 'imps'; - case PaymentType.dana: - return 'dana'; - case PaymentType.ideal: - return 'ideal'; - case PaymentType.pixPay: - return 'pix'; - default: - return null; - } - } - - PaymentType _getPaymentTypeByString(String paymentMethod) { - switch (paymentMethod.toLowerCase()) { - case 'banktransfer': - return PaymentType.bankTransfer; - case 'creditcard': - return PaymentType.creditCard; - case 'debitcard': - return PaymentType.debitCard; - case 'applepay': - return PaymentType.applePay; - case 'googlepay': - return PaymentType.googlePay; - case 'revolutpay': - return PaymentType.revolutPay; - case 'neteller': - return PaymentType.neteller; - case 'skrill': - return PaymentType.skrill; - case 'sepabanktransfer': - return PaymentType.sepa; - case 'sepainstant': - return PaymentType.sepaInstant; - case 'ach': - return PaymentType.ach; - case 'iach': - return PaymentType.achInstant; - case 'khipu': - return PaymentType.Khipu; - case 'palomabanktransfer': - return PaymentType.palomaBanktTansfer; - case 'ovo': - return PaymentType.ovo; - case 'zalopay': - return PaymentType.zaloPay; - case 'zalobanktransfer': - return PaymentType.zaloBankTransfer; - case 'gcash': - return PaymentType.gcash; - case 'imps': - return PaymentType.imps; - case 'dana': - return PaymentType.dana; - case 'ideal': - return PaymentType.ideal; - case 'pix': - return PaymentType.pixPay; - default: - return PaymentType.unknown; - } - } - - String getColorStr(Color color) => - color.toARGB32().toRadixString(16).replaceAll(RegExp(r'^ff'), ""); -} diff --git a/lib/buy/payment_method.dart b/lib/buy/payment_method.dart index 87ee9a0a2..3c1327bbb 100644 --- a/lib/buy/payment_method.dart +++ b/lib/buy/payment_method.dart @@ -193,23 +193,6 @@ class PaymentMethod extends SelectableOption { customIconPath: 'assets/images/dollar_coin.svg'); } - factory PaymentMethod.fromOnramperJson(Map json) { - final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?); - return PaymentMethod( - paymentMethodType: type ?? PaymentType.unknown, - customPaymentMethodType: json['paymentTypeId'] as String?, - customTitle: json['name'] as String? ?? 'Unknown', - customIconPath: json['icon'] as String? ?? 'assets/images/card.png', - customDescription: json['description'] as String?); - } - - factory PaymentMethod.fromDFX(String paymentMethod, PaymentType paymentType) { - return PaymentMethod( - paymentMethodType: paymentType, - customTitle: paymentMethod, - customIconPath: 'assets/images/card.png'); - } - factory PaymentMethod.fromMoonPayJson(Map json, PaymentType paymentType) { return PaymentMethod( paymentMethodType: paymentType, @@ -217,25 +200,6 @@ class PaymentMethod extends SelectableOption { customIconPath: 'assets/images/card.png'); } - factory PaymentMethod.fromMeldJson(Map json) { - final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?); - final logos = json['logos'] as Map; - return PaymentMethod( - paymentMethodType: type ?? PaymentType.unknown, - customTitle: json['name'] as String? ?? 'Unknown', - customIconPath: logos['dark'] as String? ?? 'assets/images/card.png', - customDescription: json['description'] as String?); - } - - factory PaymentMethod.fromKryptonimJson(Map json) { - final type = PaymentMethod.getPaymentTypeId(json['payment_method'] as String?); - return PaymentMethod( - paymentMethodType: type ?? PaymentType.unknown, - customTitle: json['payment_method'] as String? ?? 'Unknown', - customIconPath: 'assets/images/card.png', - ); - } - static PaymentType? getPaymentTypeId(String? type) { switch (type?.toLowerCase()) { case 'banktransfer': diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart deleted file mode 100644 index afd6d2157..000000000 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ /dev/null @@ -1,287 +0,0 @@ -import 'dart:convert'; -import 'dart:developer'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/buy/buy_provider.dart'; -import 'package:hash_wallet/buy/buy_quote.dart'; -import 'package:hash_wallet/buy/pairs_utils.dart'; -import 'package:hash_wallet/buy/payment_method.dart'; -import 'package:hash_wallet/entities/fiat_currency.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/routes.dart'; -import 'package:hash_wallet/src/screens/connect_device/connect_device_page.dart'; -import 'package:hash_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:hash_wallet/utils/show_pop_up.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class RobinhoodBuyProvider extends BuyProvider { - RobinhoodBuyProvider({ - required WalletBase wallet, - bool isTestEnvironment = false, - HardwareWalletViewModel? hardwareWalletVM, - }) : super( - wallet: wallet, - isTestEnvironment: isTestEnvironment, - hardwareWalletVM: hardwareWalletVM, - supportedCryptoList: supportedCryptoToFiatPairs( - notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat), - supportedFiatList: supportedFiatToCryptoPairs( - notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto), - ); - - static const _baseUrl = 'applink.robinhood.com'; - static const _cIdBaseUrl = 'exchange-helper.such.software'; - static const _apiBaseUrl = 'api.robinhood.com'; - static const _assetsPath = '/catpay/v1/supported_currencies/'; - - static List _supportedCrypto = []; - static final List _supportedFiat = [FiatCurrency.usd]; - - static final List _notSupportedCrypto = []; - - static final List _notSupportedFiat = - FiatCurrency.all.where((fiat) => !_supportedFiat.contains(fiat)).toList(); - - @override - String get title => 'Robinhood Connect'; - - @override - String get providerDescription => S.current.robinhood_option_description; - - @override - String get lightIcon => 'assets/images/robinhood_light.png'; - - @override - String get darkIcon => 'assets/images/robinhood_dark.png'; - - @override - bool get isAggregator => false; - - String get _applicationId => secrets.robinhoodApplicationId; - - String get _apiSecret => secrets.exchangeHelperApiKey; - - Future> getSupportedAssets() async { - final uri = Uri.https(_apiBaseUrl, '$_assetsPath', {'applicationId': _applicationId}); - - try { - final response = - await ProxyWrapper().get(clearnetUri: uri, headers: {'accept': 'application/json'}); - - if (response.statusCode == 200) { - final responseData = jsonDecode(response.body) as Map; - final pairs = responseData['cryptoCurrencyPairs'] as List; - - final supportedAssets = []; - - for (final item in pairs) { - String code = item['assetCurrency']['code'] as String; - if (code == 'AVAX') code = 'AVAXC'; - try { - final currency = CryptoCurrency.fromString(code); - supportedAssets.add(currency); - } catch (e) { - log('Robinhood: Unknown asset code "$code" - skipped'); - } - } - return supportedAssets; - } else { - return []; - } - } catch (e) { - log('Robinhood: Failed to fetch supported assets: $e'); - return []; - } - } - - Future getSignature(String message) async { - switch (wallet.type) { - case WalletType.ethereum: - case WalletType.polygon: - case WalletType.base: - case WalletType.arbitrum: - case WalletType.bsc: - case WalletType.dogecoin: - return wallet.signMessage(message); - case WalletType.litecoin: - case WalletType.bitcoin: - case WalletType.bitcoinCash: - return wallet.signMessage(message, address: wallet.walletAddresses.address); - case WalletType.monero: - case WalletType.none: - case WalletType.nano: - case WalletType.banano: - case WalletType.wownero: - throw Exception("Wallet Type ${wallet.type.name} is not available for Robinhood"); - } - } - - Future getConnectId() async { - final walletAddress = wallet.walletAddresses.address; - final valid_until = (DateTime.now().millisecondsSinceEpoch / 1000).round() + 10; - final message = "$_apiSecret:${valid_until}"; - - final signature = await getSignature(message); - - final uri = Uri.https(_cIdBaseUrl, "/api/robinhood"); - - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: {'Content-Type': 'application/json'}, - body: json - .encode({'valid_until': valid_until, 'wallet': walletAddress, 'signature': signature}), - ); - - if (response.statusCode == 200) { - return (jsonDecode(response.body) as Map)['connectId'] as String; - } else { - throw Exception('Provider currently unavailable. Status: ${response.statusCode}'); - } - } - - Future requestProviderUrl() async { - final connectId = await getConnectId(); - final networkName = wallet.currency.fullName?.toUpperCase().replaceAll(" ", "_"); - - return Uri.https(_baseUrl, '/u/connect', { - 'applicationId': _applicationId, - 'connectId': connectId, - 'walletAddress': wallet.walletAddresses.address, - 'userIdentifier': wallet.walletAddresses.address, - 'supportedNetworks': networkName - }); - } - - Future? launchProvider( - {required BuildContext context, - required Quote quote, - required double amount, - required bool isBuyAction, - required String cryptoCurrencyAddress, - String? countryCode}) async { - if (wallet.isHardwareWallet) { - if (!hardwareWalletVM!.isConnected) { - await Navigator.of(context).pushNamed(Routes.connectDevices, - arguments: ConnectDevicePageParams( - walletType: wallet.walletInfo.type, - hardwareWalletType: wallet.walletInfo.hardwareWalletType!, - onConnectDevice: (context, hwwVM) { - hwwVM.initWallet(wallet); - Navigator.of(context).pop(); - })); - } else { - hardwareWalletVM!.initWallet(wallet); - } - } - - try { - final uri = await requestProviderUrl(); - await launchUrl(uri, mode: LaunchMode.externalApplication); - } catch (e) { - await showPopUp( - context: context, - builder: (context) => AlertWithOneAction( - alertTitle: "Robinhood Connect", - alertContent: '${S.of(context).buy_provider_unavailable}: $e', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()), - ); - } - } - - @override - Future?> fetchQuote( - {required CryptoCurrency cryptoCurrency, - required FiatCurrency fiatCurrency, - required double amount, - required bool isBuyAction, - required String walletAddress, - PaymentType? paymentType, - String? customPaymentMethodType, - String? countryCode}) async { - String? paymentMethod; - - if (_supportedCrypto.isEmpty) _supportedCrypto = await getSupportedAssets(); - if (_supportedCrypto.isNotEmpty && !(_supportedCrypto.contains(cryptoCurrency))) return null; - - if (paymentType != null && paymentType != PaymentType.all) { - paymentMethod = normalizePaymentMethod(paymentType); - if (paymentMethod == null) return null; - } - - final action = isBuyAction ? 'buy' : 'sell'; - log('Robinhood: Fetching $action quote: ${isBuyAction ? cryptoCurrency.title : fiatCurrency.name.toUpperCase()} -> ${isBuyAction ? fiatCurrency.name.toUpperCase() : cryptoCurrency.title}, amount: $amount paymentMethod: $paymentMethod'); - - final queryParams = { - 'applicationId': _applicationId, - 'fiatCode': fiatCurrency.name, - 'assetCode': cryptoCurrency.title, - 'fiatAmount': amount.toString(), - if (paymentMethod != null) 'paymentMethod': paymentMethod, - }; - - final uri = - Uri.https('api.robinhood.com', '/catpay/v1/${cryptoCurrency.title}/quote/', queryParams); - - try { - final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'accept': 'application/json'}); - - final responseData = jsonDecode(response.body) as Map; - - if (response.statusCode == 200) { - final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?); - final quote = Quote.fromRobinhoodJson(responseData, isBuyAction, paymentType); - quote.setFiatCurrency = fiatCurrency; - quote.setCryptoCurrency = cryptoCurrency; - return [quote]; - } else { - if (responseData.containsKey('message')) { - log('Robinhood Error: ${responseData['message']}'); - } else { - printV('Robinhood Failed to fetch $action quote: ${response.statusCode}'); - } - return null; - } - } catch (e) { - log('Robinhood: Failed to fetch $action quote: $e'); - return null; - } - - // Supported payment methods: - // ● buying_power - // ● crypto_balance - // ● debit_card - // ● bank_transfer - } - - String? normalizePaymentMethod(PaymentType paymentMethod) { - switch (paymentMethod) { - case PaymentType.creditCard: - return 'debit_card'; - case PaymentType.debitCard: - return 'debit_card'; - case PaymentType.bankTransfer: - return 'bank_transfer'; - default: - return null; - } - } - - PaymentType _getPaymentTypeByString(String? paymentMethod) { - switch (paymentMethod) { - case 'debit_card': - return PaymentType.debitCard; - case 'bank_transfer': - return PaymentType.bankTransfer; - default: - return PaymentType.unknown; - } - } -} diff --git a/lib/cake_pay/cake_pay.dart b/lib/cake_pay/cake_pay.dart deleted file mode 100644 index c24878fad..000000000 --- a/lib/cake_pay/cake_pay.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'src/auth/cake_pay_welcome_page.dart'; -export 'src/auth/cake_pay_verify_otp_page.dart'; -export 'src/auth/cake_pay_account_page.dart'; -export 'src/cards/cake_pay_cards_page.dart'; -export 'src/cards/cake_pay_buy_card_page.dart'; diff --git a/lib/cake_pay/src/auth/cake_pay_account_page.dart b/lib/cake_pay/src/auth/cake_pay_account_page.dart deleted file mode 100644 index d27ff973c..000000000 --- a/lib/cake_pay/src/auth/cake_pay_account_page.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:hash_wallet/routes.dart'; -import 'package:hash_wallet/src/screens/base_page.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/cake_pay_tile.dart'; -import 'package:hash_wallet/src/widgets/primary_button.dart'; -import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_account_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; - -class CakePayAccountPage extends BasePage { - CakePayAccountPage(this.cakePayAccountViewModel); - - final CakePayAccountViewModel cakePayAccountViewModel; - - @override - Widget leading(BuildContext context) { - return MergeSemantics( - child: SizedBox( - height: 37, - width: 37, - child: ButtonTheme( - minWidth: double.minPositive, - child: Semantics( - label: S.of(context).seed_alert_back, - child: TextButton( - style: ButtonStyle( - overlayColor: WidgetStateColor.resolveWith( - (states) => Colors.transparent), - ), - onPressed: () => Navigator.pop(context), - child: backButton(context), - ), - ), - ), - ), - ); - } - - @override - Widget middle(BuildContext context) { - return Text( - S.current.account, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.w600, - ), - ); - } - - @override - Widget body(BuildContext context) { - return ScrollableWithBottomSection( - contentPadding: EdgeInsets.all(24), - content: Column( - children: [ - SizedBox(height: 20), - Observer( - builder: (_) => Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1.0, - color: Theme.of(context).colorScheme.outlineVariant, - ), - ), - ), - child: CakePayTile( - title: S.of(context).email_address, - subTitle: cakePayAccountViewModel.email - ), - ), - ), - ], - ), - bottomSectionPadding: EdgeInsets.all(30), - bottomSection: Column( - children: [ - PrimaryButton( - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - text: S.of(context).logout, - onPressed: () { - cakePayAccountViewModel.logout(); - Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false); - }, - ), - ], - ), - ); - } -} diff --git a/lib/cake_pay/src/auth/cake_pay_verify_otp_page.dart b/lib/cake_pay/src/auth/cake_pay_verify_otp_page.dart deleted file mode 100644 index cdff88526..000000000 --- a/lib/cake_pay/src/auth/cake_pay_verify_otp_page.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/cake_pay/src/cake_pay_states.dart'; -import 'package:hash_wallet/src/screens/base_page.dart'; -import 'package:hash_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:hash_wallet/src/widgets/base_text_form_field.dart'; -import 'package:hash_wallet/src/widgets/keyboard_done_button.dart'; -import 'package:hash_wallet/src/widgets/primary_button.dart'; -import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart'; -import 'package:hash_wallet/typography.dart'; -import 'package:hash_wallet/utils/show_pop_up.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:keyboard_actions/keyboard_actions.dart'; -import 'package:mobx/mobx.dart'; - -class CakePayVerifyOtpPage extends BasePage { - CakePayVerifyOtpPage(this._authViewModel, this._email, this.isSignIn) - : _codeController = TextEditingController(), - _codeFocus = FocusNode() { - _codeController.addListener(() { - final otp = _codeController.text; - _authViewModel.otp = otp; - if (otp.length > 5) { - _authViewModel.otpState = CakePayOtpSendEnabled(); - } else { - _authViewModel.otpState = CakePayOtpSendDisabled(); - } - }); - } - - final CakePayAuthViewModel _authViewModel; - final bool isSignIn; - - final String _email; - - @override - Widget middle(BuildContext context) { - return Text( - S.current.verification, - style: textMediumSemiBold( - color: Theme.of(context).colorScheme.onSurface, - ), - ); - } - - final TextEditingController _codeController; - final FocusNode _codeFocus; - - @override - Widget body(BuildContext context) { - reaction((_) => _authViewModel.otpState, (CakePayOtpState state) { - if (state is CakePayOtpFailure) { - _onOtpFailure(context, state.error); - } - if (state is CakePayOtpSuccess) { - _onOtpSuccessful(context); - } - }); - return KeyboardActions( - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).colorScheme.surface, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _codeFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ), - ]), - child: Container( - height: 0, - color: Theme.of(context).colorScheme.surface, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.all(24), - content: Column( - children: [ - BaseTextFormField( - hintText: S.of(context).enter_code, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - focusNode: _codeFocus, - controller: _codeController, - onSubmit: (_) => _verify(), - ), - SizedBox(height: 14), - Text( - S.of(context).fill_code, - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - SizedBox(height: 34), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(S.of(context).didnt_get_code), - SizedBox(width: 20), - InkWell( - onTap: () => _authViewModel.logIn(_email), - child: Text( - S.of(context).resend_code, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ], - ), - ], - ), - bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), - bottomSection: Column( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Observer( - builder: (_) => LoadingPrimaryButton( - text: S.of(context).continue_text, - onPressed: _verify, - isDisabled: _authViewModel.otpState is CakePayOtpSendDisabled, - isLoading: _authViewModel.otpState is CakePayOtpValidating, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - ), - ), - SizedBox(height: 20), - ], - ), - ], - ), - ), - ), - ); - } - - void _onOtpFailure(BuildContext context, String error) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.current.verification, - alertContent: error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } - - - void _onOtpSuccessful(BuildContext context) => - Navigator.pop(context, true); - - void _verify() async => await _authViewModel.verifyEmail(_codeController.text); -} diff --git a/lib/cake_pay/src/auth/cake_pay_welcome_page.dart b/lib/cake_pay/src/auth/cake_pay_welcome_page.dart deleted file mode 100644 index 3aace2449..000000000 --- a/lib/cake_pay/src/auth/cake_pay_welcome_page.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:hash_wallet/core/email_validator.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/cake_pay/src/cake_pay_states.dart'; -import 'package:hash_wallet/routes.dart'; -import 'package:hash_wallet/src/screens/base_page.dart'; -import 'package:hash_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:hash_wallet/src/widgets/base_text_form_field.dart'; -import 'package:hash_wallet/src/widgets/primary_button.dart'; -import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart'; -import 'package:hash_wallet/typography.dart'; -import 'package:hash_wallet/utils/show_pop_up.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; - -class CakePayWelcomePage extends BasePage { - CakePayWelcomePage(this._authViewModel) - : _formKey = GlobalKey(), - _emailController = TextEditingController() { - _emailController.text = _authViewModel.email; - _emailController.addListener(() => _authViewModel.email = _emailController.text); - } - - final GlobalKey _formKey; - - final CakePayAuthViewModel _authViewModel; - - final TextEditingController _emailController; - - @override - Widget middle(BuildContext context) { - return Text( - S.current.welcome_to_cakepay, - style: textMediumSemiBold( - color: Theme.of(context).colorScheme.onSurface, - ), - ); - } - - @override - Widget body(BuildContext context) { - reaction((_) => _authViewModel.userVerificationState, (CakePayUserVerificationState state) { - if (state is CakePayUserVerificationStateFailure) { - _onLoginUserFailure(context, state.error); - } - if (state is CakePayUserVerificationStateSuccess) { - _onLoginSuccessful(context, _authViewModel); - } - }); - return ScrollableWithBottomSection( - contentPadding: EdgeInsets.all(24), - content: Column( - children: [ - SizedBox(height: 90), - Form( - key: _formKey, - child: BaseTextFormField( - hintText: S.of(context).email_address, - keyboardType: TextInputType.emailAddress, - validator: EmailValidator(), - controller: _emailController, - onSubmit: (text) => _login(), - ), - ), - SizedBox(height: 20), - Text( - S.of(context).about_cake_pay, - style: textLarge( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - SizedBox(height: 20), - Text(S.of(context).cake_pay_account_note, - style: textLarge(color: Theme.of(context).colorScheme.onSurface)), - ], - ), - bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), - bottomSection: Column( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Observer( - builder: (_) => LoadingPrimaryButton( - text: S.of(context).login, - onPressed: _login, - isLoading: - _authViewModel.userVerificationState is CakePayUserVerificationStateLoading, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - ), - ), - SizedBox( - height: 20, - ), - ], - ), - ], - ), - ); - } - - void _onLoginUserFailure(BuildContext context, String error) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.current.login, - alertContent: error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } - - Future _onLoginSuccessful(BuildContext context, CakePayAuthViewModel authViewModel) async { - final verified = await Navigator.pushNamed(context, Routes.cakePayVerifyOtpPage, - arguments: [authViewModel.email, true]); - - if (verified == true && Navigator.of(context).canPop()) { - Navigator.pop(context, true); - } - } - - void _login() async { - if (_formKey.currentState != null && !_formKey.currentState!.validate()) { - return; - } - await _authViewModel.logIn(_emailController.text); - } -} diff --git a/lib/cake_pay/src/cake_pay_states.dart b/lib/cake_pay/src/cake_pay_states.dart deleted file mode 100644 index 93b9afc79..000000000 --- a/lib/cake_pay/src/cake_pay_states.dart +++ /dev/null @@ -1,68 +0,0 @@ -abstract class CakePayUserVerificationState {} - -class CakePayUserVerificationStateInitial extends CakePayUserVerificationState {} - -class CakePayUserVerificationStateSuccess extends CakePayUserVerificationState {} - -class CakePayUserVerificationStatePending extends CakePayUserVerificationState {} - -class CakePayUserVerificationStateLoading extends CakePayUserVerificationState {} - -class CakePayUserVerificationStateFailure extends CakePayUserVerificationState { - CakePayUserVerificationStateFailure({required this.error}); - - final String error; -} - -abstract class CakePayOtpState {} - -class CakePayOtpValidating extends CakePayOtpState {} - -class CakePayOtpSuccess extends CakePayOtpState {} - -class CakePayOtpSendDisabled extends CakePayOtpState {} - -class CakePayOtpSendEnabled extends CakePayOtpState {} - -class CakePayOtpFailure extends CakePayOtpState { - CakePayOtpFailure({required this.error}); - - final String error; -} - -class CakePayCreateCardState {} - -class CakePayCreateCardStateSuccess extends CakePayCreateCardState {} - -class CakePayCreateCardStateLoading extends CakePayCreateCardState {} - -class CakePayCreateCardStateFailure extends CakePayCreateCardState { - CakePayCreateCardStateFailure({required this.error}); - - final String error; -} - -class UserCakePayCardsState {} - -class UserCakePayCardsStateInitial extends UserCakePayCardsState {} - -class UserCakePayCardsStateNoCards extends UserCakePayCardsState {} - -class UserCakePayCardsStateFetching extends UserCakePayCardsState {} - -class UserCakePayCardsStateFailure extends UserCakePayCardsState { - UserCakePayCardsStateFailure({required this.error}); - - final String error; -} - -class UserCakePayCardsStateSuccess extends UserCakePayCardsState {} - - -abstract class CakePayVendorState {} - -class InitialCakePayVendorLoadingState extends CakePayVendorState {} - -class CakePayVendorLoadingState extends CakePayVendorState {} - -class CakePayVendorLoadedState extends CakePayVendorState {} diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart deleted file mode 100644 index b5a32fda4..000000000 --- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart +++ /dev/null @@ -1,800 +0,0 @@ -import 'dart:io'; - -import 'package:hash_wallet/bitcoin/bitcoin.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_card.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/denominations_amount_widget.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/enter_amount_widget.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/image_placeholder.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/link_extractor.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/text_icon_button.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart'; -import 'package:hash_wallet/core/execution_state.dart'; -import 'package:hash_wallet/entities/parsed_address.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; -import 'package:hash_wallet/routes.dart'; -import 'package:hash_wallet/src/screens/base_page.dart'; -import 'package:hash_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:hash_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; -import 'package:hash_wallet/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart'; -import 'package:hash_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; -import 'package:hash_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; -import 'package:hash_wallet/src/widgets/keyboard_done_button.dart'; -import 'package:hash_wallet/src/widgets/primary_button.dart'; -import 'package:hash_wallet/src/widgets/standard_checkbox.dart'; -import 'package:hash_wallet/typography.dart'; -import 'package:hash_wallet/utils/feature_flag.dart'; -import 'package:hash_wallet/utils/show_pop_up.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart'; -import 'package:hash_wallet/view_model/send/output.dart'; -import 'package:hash_wallet/view_model/send/send_view_model.dart'; -import 'package:hash_wallet/view_model/send/send_view_model_state.dart'; -import 'package:cw_core/unspent_coin_type.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:keyboard_actions/keyboard_actions.dart'; -import 'package:mobx/mobx.dart'; - -class CakePayBuyCardPage extends BasePage { - CakePayBuyCardPage(this.cakePayBuyCardViewModel) - : _sendViewModel = cakePayBuyCardViewModel.sendViewModel, - _amountFieldFocus = FocusNode(), - _amountController = TextEditingController(), - _quantityFieldFocus = FocusNode(), - _quantityController = - TextEditingController(text: cakePayBuyCardViewModel.quantity.toString()) { - _amountController.addListener(() { - cakePayBuyCardViewModel.onAmountChanged(_amountController.text); - }); - } - - final CakePayBuyCardViewModel cakePayBuyCardViewModel; - final SendViewModel _sendViewModel; - - bool _effectsInstalled = false; - late final BuildContext _overlayCtx; - - @override - String get title => cakePayBuyCardViewModel.card.name; - - @override - bool get extendBodyBehindAppBar => true; - - @override - bool get gradientAll => true; - - @override - bool get resizeToAvoidBottomInset => false; - - @override - AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent; - - @override - Widget? trailing(BuildContext context) { - return const SizedBox( - width: 54, - height: 0, - ); - } - - @override - Widget? middle(BuildContext context) { - return Text( - title, - textAlign: TextAlign.center, - maxLines: 2, - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - fontFamily: 'Lato', - color: titleColor(context)), - ); - } - - final TextEditingController _amountController; - final FocusNode _amountFieldFocus; - final TextEditingController _quantityController; - final FocusNode _quantityFieldFocus; - - @override - Widget body(BuildContext context) { - _setEffects(context); - - final card = cakePayBuyCardViewModel.card; - final vendor = cakePayBuyCardViewModel.vendor; - - return Container( - color: Colors.transparent, - child: KeyboardHideOverlay( - child: Column( - children: [ - RoundedOverlayCards( - topCardChild: Column( - children: [ - Expanded(flex: 4, child: const SizedBox()), - Expanded( - flex: 7, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(150), - blurRadius: 8, - offset: const Offset(0, 2)) - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.network( - card.cardImageUrl ?? '', - fit: BoxFit.cover, - loadingBuilder: (BuildContext context, Widget child, - ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) return child; - return Center(child: CircularProgressIndicator()); - }, - errorBuilder: (context, error, stackTrace) => - CakePayCardImagePlaceholder(), - ), - ), - ), - ), - Expanded(child: const SizedBox()), - ], - ), - bottomCardChild: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: card.denominationItems.isNotEmpty - ? DenominationsAmountWidget( - fiatCurrency: card.fiatCurrency.title, - denominations: card.denominationItems, - amountFieldFocus: _amountFieldFocus, - amountController: _amountController, - quantityFieldFocus: _quantityFieldFocus, - quantityController: _quantityController, - onAmountChanged: cakePayBuyCardViewModel.onAmountChanged, - onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged, - cakePayBuyCardViewModel: cakePayBuyCardViewModel) - : EnterAmountWidget( - minValue: card.minValue ?? '-', - maxValue: card.maxValue ?? '-', - fiatCurrency: card.fiatCurrency.title, - amountFieldFocus: _amountFieldFocus, - amountController: _amountController, - onAmountChanged: cakePayBuyCardViewModel.onAmountChanged))), - Expanded( - flex: 2, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (vendor.cakeWarnings != null) - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Center( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.white.withAlpha(50)), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - vendor.cakeWarnings!, - textAlign: TextAlign.center, - style: textSmallSemiBold(color: Colors.white), - ), - ), - ), - ), - ), - Expanded( - child: SingleChildScrollView( - primary: false, - padding: EdgeInsets.zero, - child: ClickableLinksText( - text: card.description ?? '', - textStyle: TextStyle( - color: Theme.of(context).textTheme.titleLarge!.color!, - fontSize: 14, - fontWeight: FontWeight.w400, - ), - ), - ), - ), - ], - ), - ), - ), - Expanded( - child: SingleChildScrollView( - primary: false, - child: Padding( - padding: const EdgeInsets.only(left: 24, right: 24), - child: Column( - children: [ - if (card.expiryAndValidity != null && card.expiryAndValidity!.isNotEmpty) - Row( - children: [ - Text(S.of(context).expiry_and_validity + ':', - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge!.color!, - fontSize: 16, - fontWeight: FontWeight.w900)), - Expanded( - child: Text(card.expiryAndValidity!, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.labelMedium)), - ], - ), - SizedBox(height: 8), - TextIconButton( - label: S.of(context).how_to_use_card, - onTap: () => _showHowToUseCard(context, card)), - SizedBox(height: 8), - TextIconButton( - label: S.of(context).settings_terms_and_conditions, - onTap: () => _showTermsAndCondition(context, card.termsAndConditions)), - ], - ), - ), - ), - ), - SizedBox(height: 8), - Observer(builder: (_) { - Widget _buildPaymentMethodWidget( - List methods, CakePayPaymentMethod selected) { - return Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 24, right: 8), - child: Text( - 'Payment Method', - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge!.color!, - fontSize: 16, - fontWeight: FontWeight.w900, - ), - ), - ), - Expanded(child: const SizedBox()), - if (methods.length > 1) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6), - child: ToggleButtons( - isSelected: methods.map((m) => m == selected).toList(), - borderRadius: BorderRadius.circular(8), - onPressed: (index) => - cakePayBuyCardViewModel.chooseMethod(methods[index]), - children: methods - .map((m) => Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Text(m.label), - )) - .toList(), - ), - ), - ], - ); - } - - final methods = cakePayBuyCardViewModel.availableMethods; - final selected = cakePayBuyCardViewModel.selectedPaymentMethod ?? - (methods.isNotEmpty ? methods.first : null); - - return Column( - children: [ - methods.length <= 1 || selected == null - ? const SizedBox.shrink() - : _buildPaymentMethodWidget(methods, selected), - if (_sendViewModel.walletType == WalletType.litecoin && - _sendViewModel.isMwebEnabled) - Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20), - child: GestureDetector( - key: ValueKey('cake_pay_buy_page_unspent_coin_button_key'), - onTap: () { - bool value = _sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any; - _sendViewModel.setAllowMwebCoins(!value); - }, - child: Container( - color: Colors.transparent, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - StandardCheckbox( - caption: S.of(context).litecoin_mweb_allow_coins, - captionColor: Theme.of(context).colorScheme.onSurfaceVariant, - borderColor: Theme.of(context).colorScheme.primary, - iconColor: Theme.of(context).colorScheme.primary, - value: _sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any, - onChanged: (bool? value) { - _sendViewModel.setAllowMwebCoins(value ?? false); - }, - ), - ], - ), - ), - ), - ), - ), - if (FeatureFlag.hasDevOptions && FeatureFlag.isCakePayPurchaseSimulationEnabled) - Padding( - padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20), - child: LoadingPrimaryButton( - onPressed: () { - //Request dummy node to get the focus out of the text fields - FocusScope.of(context).requestFocus(FocusNode()); - - cakePayBuyCardViewModel.isSimulatingFlow = true; - isIOSUnavailable(card) - ? alertIOSAvailability(context, card) - : confirmPurchaseFirst(context); - }, - text: '(Dev) Simulate Purchasing Gift Card', - isDisabled: !cakePayBuyCardViewModel.isAmountSufficient || - cakePayBuyCardViewModel.isPurchasing, - isLoading: _sendViewModel.state is IsExecutingState || - cakePayBuyCardViewModel.isPurchasing, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - ), - ), - Padding( - padding: EdgeInsets.only(top: 10, bottom: 34, right: 20, left: 20), - child: LoadingPrimaryButton( - onPressed: () { - //Request dummy node to get the focus out of the text fields - FocusScope.of(context).requestFocus(FocusNode()); - - isIOSUnavailable(card) - ? alertIOSAvailability(context, card) - : confirmPurchaseFirst(context); - }, - text: S.of(context).purchase_gift_card, - isDisabled: !cakePayBuyCardViewModel.isAmountSufficient || - cakePayBuyCardViewModel.isPurchasing || _sendViewModel.state is ExecutedSuccessfullyState, - isLoading: _sendViewModel.state is IsExecutingState || - cakePayBuyCardViewModel.isPurchasing, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary, - ), - ), - ], - ); - }), - ], - ), - ), - ); - } - - bool isWordInCardsName(CakePayCard card, String word) { - return card.name.toLowerCase().contains(word.toLowerCase()); - } - - bool isIOSUnavailable(CakePayCard card) { - if (!Platform.isIOS && !Platform.isMacOS) { - return false; - } - - final isDigitalGameStores = isWordInCardsName(card, 'playstation') || - isWordInCardsName(card, 'xbox') || - isWordInCardsName(card, 'steam') || - isWordInCardsName(card, 'meta quest') || - isWordInCardsName(card, 'kigso') || - isWordInCardsName(card, 'game world') || - isWordInCardsName(card, 'google') || - isWordInCardsName(card, 'nintendo'); - final isGCodes = isWordInCardsName(card, 'gcodes'); - final isApple = isWordInCardsName(card, 'itunes') || isWordInCardsName(card, 'apple'); - final isTidal = isWordInCardsName(card, 'tidal'); - final isVPNServices = isWordInCardsName(card, 'nordvpn') || - isWordInCardsName(card, 'expressvpn') || - isWordInCardsName(card, 'surfshark') || - isWordInCardsName(card, 'proton'); - final isStreamingServices = isWordInCardsName(card, 'netflix') || - isWordInCardsName(card, 'spotify') || - isWordInCardsName(card, 'hulu') || - isWordInCardsName(card, 'hbo') || - isWordInCardsName(card, 'soundcloud') || - isWordInCardsName(card, 'twitch'); - final isDatingServices = isWordInCardsName(card, 'tinder'); - - return isDigitalGameStores || - isGCodes || - isApple || - isTidal || - isVPNServices || - isStreamingServices || - isDatingServices; - } - - Future alertIOSAvailability(BuildContext context, CakePayCard card) async { - cakePayBuyCardViewModel.isSimulatingFlow = false; - return await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: S.of(context).cakepay_ios_not_available, - buttonText: S.of(context).ok, - buttonAction: () { - // _walletHardwareRestoreVM.error = null; - Navigator.of(context).pop(); - }); - }); - } - - Future confirmPurchaseFirst(BuildContext context) async { - bool isLogged = await cakePayBuyCardViewModel.isUserLogged; - if (!isLogged) { - cakePayBuyCardViewModel.isSimulatingFlow = false; - Navigator.of(context).pushNamed(Routes.cakePayWelcomePage); - } else { - cakePayBuyCardViewModel.isPurchasing = true; - await _showconfirmPurchaseFirstAlert(context); - } - } - - Future _showconfirmPurchaseFirstAlert(BuildContext context) async { - if (!cakePayBuyCardViewModel.confirmsNoVpn || - !cakePayBuyCardViewModel.confirmsVoidedRefund || - !cakePayBuyCardViewModel.confirmsTermsAgreed) { - await showPopUp( - context: context, - builder: (BuildContext context) => ThreeCheckboxAlert( - alertTitle: S.of(context).cakepay_confirm_purchase, - leftButtonText: S.of(context).cancel, - rightButtonText: S.of(context).confirm, - actionLeftButton: () { - cakePayBuyCardViewModel.isPurchasing = false; - cakePayBuyCardViewModel.isSimulatingFlow = false; - Navigator.of(context).pop(); - }, - actionRightButton: (confirmsNoVpn, confirmsVoidedRefund, confirmsTermsAgreed) { - cakePayBuyCardViewModel.confirmsNoVpn = confirmsNoVpn; - cakePayBuyCardViewModel.confirmsVoidedRefund = confirmsVoidedRefund; - cakePayBuyCardViewModel.confirmsTermsAgreed = confirmsTermsAgreed; - - Navigator.of(context).pop(); - }, - ), - ); - } - - if (cakePayBuyCardViewModel.confirmsNoVpn && - cakePayBuyCardViewModel.confirmsVoidedRefund && - cakePayBuyCardViewModel.confirmsTermsAgreed) { - await purchaseCard(context); - } else { - cakePayBuyCardViewModel.isPurchasing = false; - cakePayBuyCardViewModel.isSimulatingFlow = false; - } - } - - Future purchaseCard(BuildContext context) async { - bool isLogged = await cakePayBuyCardViewModel.isUserLogged; - if (!isLogged) { - cakePayBuyCardViewModel.isSimulatingFlow = false; - Navigator.of(context).pushNamed(Routes.cakePayWelcomePage); - } else { - try { - await cakePayBuyCardViewModel.createOrder(); - } catch (_) { - cakePayBuyCardViewModel.isSimulatingFlow = false; - await cakePayBuyCardViewModel.logout(); - } - } - cakePayBuyCardViewModel.isPurchasing = false; - } - - BuildContext? dialogContext; - BuildContext? loadingBottomSheetContext; - BuildContext? confirmBottomSheetContext; - - void _setEffects(BuildContext context) { - if (_effectsInstalled) { - return; - } - - _overlayCtx = Navigator.of(context).context; - - if (_sendViewModel.isElectrumWallet) { - bitcoin!.updateFeeRates(_sendViewModel.wallet); - } - - reaction((_) => _sendViewModel.state, (ExecutionState state) async { - if (dialogContext != null && dialogContext!.mounted) Navigator.of(dialogContext!).pop(); - - if (confirmBottomSheetContext != null && confirmBottomSheetContext!.mounted) { - Navigator.of(confirmBottomSheetContext!).pop(); - } - - if (state is! IsExecutingState && - loadingBottomSheetContext != null && - loadingBottomSheetContext!.mounted) { - Navigator.of(loadingBottomSheetContext!).pop(); - } - - if (state is FailureState) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - key: ValueKey('cake_pay_buy_page_send_failure_dialog_key'), - buttonKey: ValueKey('cake_pay_buy_page_send_failure_dialog_button_key'), - alertTitle: S.of(context).error, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - }); - } - - if (state is IsExecutingState) { - // wait a bit to avoid showing the loading dialog if transaction is failed - await Future.delayed(const Duration(milliseconds: 300)); - final currentState = _sendViewModel.state; - if (currentState is ExecutedSuccessfullyState || currentState is FailureState) { - return; - } - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { - showModalBottomSheet( - context: context, - isDismissible: false, - builder: (BuildContext context) { - loadingBottomSheetContext = context; - return LoadingBottomSheet( - titleText: S.of(context).generating_transaction, - ); - }, - ); - } - }); - } - - if (state is ExecutedSuccessfullyState) { - if (cakePayBuyCardViewModel.order == null) return; - - ReactionDisposer? disposer; - - disposer = reaction((_) => cakePayBuyCardViewModel.isOrderExpired, (bool isExpired) { - if (isExpired) { - _sendViewModel.state = FailureState('Order expired'); - disposer?.call(); - } - }); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (context.mounted) { - final order = cakePayBuyCardViewModel.order; - - final displayingOutputs = _sendViewModel.outputs - .map((o) => o.OutputCopyWithParsedAddress( - parsedAddress: ParsedAddress( - addresses: [o.address], - name: 'Cake Pay', - profileName: order?.cards.first.cardName ?? 'Cake Pay', - profileImageUrl: order?.cards.first.cardImagePath ?? '', - ), - fiatAmount: '${order?.totalReceiveAmount}', - )) - .toList(); - - final result = await showModalBottomSheet( - context: context, - isDismissible: false, - isScrollControlled: true, - builder: (BuildContext bottomSheetContext) { - confirmBottomSheetContext = bottomSheetContext; - return ConfirmSendingBottomSheet( - key: ValueKey('cake_pay_buy_page_confirm_sending_dialog_key'), - titleText: S.of(bottomSheetContext).confirm_transaction, - cakePayBuyCardViewModel: cakePayBuyCardViewModel, - paymentId: S.of(bottomSheetContext).payment_id, - paymentIdValue: cakePayBuyCardViewModel.order?.orderId, - expirationTime: cakePayBuyCardViewModel.formattedRemainingTime, - walletType: _sendViewModel.walletType, - titleIconPath: _sendViewModel.selectedCryptoCurrency.iconPath, - currency: _sendViewModel.selectedCryptoCurrency, - amount: S.of(bottomSheetContext).send_amount, - amountValue: _sendViewModel.amountParsingProxy.getDisplayCryptoAmount( - _sendViewModel.pendingTransaction!.amountFormatted, - _sendViewModel.selectedCryptoCurrency), - quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}', - fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted, - fee: S.of(bottomSheetContext).send_fee, - feeValue: "${_sendViewModel.amountParsingProxy.getDisplayCryptoAmount( - _sendViewModel.pendingTransaction!.feeFormattedValue, - _sendViewModel.selectedCryptoCurrency)} ${_sendViewModel.amountParsingProxy - .getCryptoSymbol(_sendViewModel.wallet.currency)}", - feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted, - outputs: displayingOutputs, - footerType: FooterType.slideActionButton, - isSlideActionEnabled: _sendViewModel.isReadyForSend, - slideActionButtonText: - cakePayBuyCardViewModel.isSimulating ? 'Swipe to simulate' : 'Swipe to send', - accessibleNavigationModeSlideActionButtonText: - cakePayBuyCardViewModel.isSimulating ? 'Simulate' : S.of(context).send, - onSlideActionComplete: () async { - Navigator.of(bottomSheetContext).pop(true); - cakePayBuyCardViewModel.isSimulating - ? cakePayBuyCardViewModel.simulatePayment() - : _sendViewModel.commitTransaction(context); - }, - change: _sendViewModel.pendingTransaction!.change, - isOpenCryptoPay: _sendViewModel.ocpRequest != null, - amountParsingProxy: _sendViewModel.amountParsingProxy, - ); - }, - ); - - confirmBottomSheetContext = null; - cakePayBuyCardViewModel.isSimulatingFlow = false; - _handleDispose(disposer); - if (result == null) _sendViewModel.dismissTransaction(); - } - }); - } - - if (state is TransactionCommitted) { - final order = cakePayBuyCardViewModel.order; - final outputsCopy = List.from(_sendViewModel.outputs); - - final displayingOutputs = outputsCopy - .map((o) => o.OutputCopyWithParsedAddress( - parsedAddress: ParsedAddress( - addresses: [o.address], - name: 'Cake Pay', - profileName: order?.cards.first.cardName ?? 'Cake Pay', - profileImageUrl: order?.cards.first.cardImagePath ?? '', - ), - fiatAmount: '${order?.totalReceiveAmount}', - )) - .toList(); - - _sendViewModel.clearOutputs(); - - final bool usePageContextLater = context.mounted; - - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (displayingOutputs.isEmpty) { - if (context.mounted) Navigator.of(context).pop(); - return; - } - - final BuildContext sheetParentCtx = usePageContextLater ? context : _overlayCtx; - - if (!sheetParentCtx.mounted) { - return; - } - - await showModalBottomSheet( - context: sheetParentCtx, - useRootNavigator: !usePageContextLater, - isScrollControlled: true, - isDismissible: true, - backgroundColor: Colors.transparent, - builder: (sheetCtx) { - return CakePayTransactionSentBottomSheet( - key: const ValueKey('cake_pay_buy_page_transaction_sent_bottom_sheet_key'), - titleText: S.of(sheetCtx).transaction_sent, - titleIconWidget: const CircleAvatar( - radius: 10, - backgroundColor: Colors.green, - child: Icon(Icons.check, size: 16, color: Colors.white), - ), - output: displayingOutputs.first, - currency: _sendViewModel.selectedCryptoCurrency, - amount: S.of(sheetCtx).send_amount, - amountValue: _sendViewModel.pendingTransaction!.amountFormatted, - quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}', - fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted, - fee: S.of(sheetCtx).send_fee, - feeValue: _sendViewModel.pendingTransaction!.feeFormatted, - feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted, - paymentId: 'Order ID', - paymentIdValue: order?.orderId ?? '', - onClose: () { - Navigator.of(sheetCtx).pop(); - }, - ); - }, - ); - if (context.mounted) { - Navigator.of(context).pop(); - } - }); - } - - if (state is IsAwaitingDeviceResponseState) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!context.mounted) return; - - showModalBottomSheet( - context: context, - isDismissible: false, - builder: (BuildContext bottomSheetContext) => InfoBottomSheet( - footerType: FooterType.singleActionButton, - titleText: S.of(bottomSheetContext).proceed_on_device, - contentImage: 'assets/images/hardware_wallet/ledger_nano_x.png', - contentImageColor: Theme.of(context).textTheme.titleLarge!.color!, - content: S.of(bottomSheetContext).proceed_on_device_description, - singleActionButtonText: S.of(context).cancel, - onSingleActionButtonPressed: () { - _sendViewModel.state = InitialExecutionState(); - Navigator.of(bottomSheetContext).pop(); - }, - ), - ); - }); - } - }); - - _effectsInstalled = true; - } - - void _handleDispose(ReactionDisposer? disposer) { - cakePayBuyCardViewModel.dispose(); - if (disposer != null) { - disposer(); - } - } -} - -void _showHowToUseCard(BuildContext context, CakePayCard card) { - showPopUp( - context: context, - builder: (BuildContext context) { - return CakePayAlertModal( - title: S.of(context).how_to_use_card, - dismissible: true, - content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: EdgeInsets.all(10), - child: Text(card.name, style: Theme.of(context).textTheme.headlineSmall)), - ClickableLinksText( - text: card.howToUse ?? '', - textStyle: Theme.of(context).textTheme.bodyMedium!, - linkStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic)) - ]), - actionTitle: S.current.got_it, - showCloseButton: false, - ); - }); -} - -void _showTermsAndCondition(BuildContext context, String? termsAndConditions) { - showPopUp( - context: context, - builder: (BuildContext context) { - return CakePayAlertModal( - title: S.of(context).settings_terms_and_conditions, - dismissible: true, - content: Align( - alignment: Alignment.bottomLeft, - child: ClickableLinksText( - text: termsAndConditions ?? '', - textStyle: Theme.of(context).textTheme.bodyMedium!, - linkStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic))), - actionTitle: S.of(context).agree, - showCloseButton: false, - ); - }); -} diff --git a/lib/cake_pay/src/cards/cake_pay_cards_page.dart b/lib/cake_pay/src/cards/cake_pay_cards_page.dart deleted file mode 100644 index 5de9ef92d..000000000 --- a/lib/cake_pay/src/cards/cake_pay_cards_page.dart +++ /dev/null @@ -1,510 +0,0 @@ -import 'package:hash_wallet/cake_pay/src/cake_pay_states.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_card.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/cake_pay_search_bar_widget.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/user_card_item.dart'; -import 'package:hash_wallet/entities/country.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; -import 'package:hash_wallet/routes.dart'; -import 'package:hash_wallet/src/screens/base_page.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/card_item.dart'; -import 'package:hash_wallet/src/screens/dashboard/widgets/filter_widget.dart'; -import 'package:hash_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart'; -import 'package:hash_wallet/src/widgets/bottom_sheet/cake_pay_card_info_bottom_sheet_widget.dart'; -import 'package:hash_wallet/src/widgets/gradient_background.dart'; -import 'package:hash_wallet/src/widgets/picker.dart'; -import 'package:hash_wallet/src/widgets/tab_view_wrapper_widget.dart'; -import 'package:hash_wallet/typography.dart'; -import 'package:hash_wallet/utils/feature_flag.dart'; -import 'package:hash_wallet/utils/responsive_layout_util.dart'; -import 'package:hash_wallet/utils/show_pop_up.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; - -class CakePayCardsPage extends BasePage { - CakePayCardsPage(this._cardsListViewModel); - - final CakePayCardsListViewModel _cardsListViewModel; - - @override - bool get gradientBackground => true; - - @override - Widget Function(BuildContext, Widget) get rootWrapper => - (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold); - - @override - bool get resizeToAvoidBottomInset => false; - - @override - Widget middle(BuildContext context) { - return Text( - 'Cake Pay', - style: textMediumSemiBold( - color: titleColor(context), - ), - ); - } - - @override - Widget trailing(BuildContext context) { - return Observer(builder: (_) { - final loggedIn = _cardsListViewModel.isUserAuthenticated == true; - - Future _handleOnPressed() async { - if (loggedIn) { - Navigator.pushNamed(context, Routes.cakePayAccountPage); - return; - } - final success = await Navigator.pushNamed(context, Routes.cakePayWelcomePage); - - if (success == true) await _cardsListViewModel.checkAuth(); - } - - if (!loggedIn || _cardsListViewModel.username == null) { - return _TrailingIcon( - asset: 'assets/images/profile.png', - iconColor: pageIconColor(context) ?? Colors.white, - onPressed: () async => await _handleOnPressed(), - ); - } - final letter = _cardsListViewModel.username!.trim()[0].toUpperCase(); - return IconButton( - padding: EdgeInsets.zero, - iconSize: 25, - onPressed: _handleOnPressed, - icon: CircleAvatar( - radius: 12, - backgroundColor: Theme.of(context).colorScheme.primary, - child: Text( - letter, - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, - fontWeight: FontWeight.w600, - ), - ), - ), - ); - }); - } - - @override - Widget body(BuildContext context) { - return KeyboardHideOverlay(child:CakePayCardsPageBody(cardsListViewModel: _cardsListViewModel, titleColor: titleColor)); - } -} - -class CakePayCardsPageBody extends StatefulWidget { - const CakePayCardsPageBody({ - super.key, - required CakePayCardsListViewModel cardsListViewModel, - required this.titleColor, - }) : _cardsListViewModel = cardsListViewModel; - - final CakePayCardsListViewModel _cardsListViewModel; - final Color? Function(BuildContext) titleColor; - - @override - State createState() => _CakePayCardsPageBodyState(); -} - -class _CakePayCardsPageBodyState extends State { - ReactionDisposer? _countryPickerDisposer; - - @override - void initState() { - super.initState(); - final viewModel = widget._cardsListViewModel; - - _countryPickerDisposer = when( - (_) => viewModel.shouldShowCountryPicker, - () async { - viewModel.storeInitialFilterStates(); - - WidgetsBinding.instance.addPostFrameCallback( - (_) async { - await showCountryPicker(context, viewModel); - if (viewModel.hasFiltersChanged) { - viewModel.resetLoadingNextPageState(); - viewModel.getVendors(); - } - viewModel.settingsStore.selectedCakePayCountry = viewModel.selectedCountry; - }, - ); - }, - ); - } - - @override - void dispose() { - _countryPickerDisposer?.call(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Observer(builder: (_) { - final isUserAuthenticated = widget._cardsListViewModel.isUserAuthenticated; - - if (isUserAuthenticated == null) return const _Loading(); - - if (isUserAuthenticated == false || !FeatureFlag.isCakePayRedemptionFlowEnabled) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: _ShopTab(cardsListViewModel: widget._cardsListViewModel), - ); - } - - final titleColor = widget.titleColor(context); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: Column(children: [ - Expanded( - child: TabViewWrapper( - labelStyle: TextStyle( - color: titleColor, - fontFamily: 'Lato', - fontSize: 20, - fontWeight: FontWeight.w600), - unselectedLabelStyle: TextStyle( - color: titleColor?.withAlpha(150) ?? Colors.white70, - fontFamily: 'Lato', - fontSize: 20, - fontWeight: FontWeight.w400), - indicatorColor: titleColor, - tabs: const [ - Tab(text: 'My Cards'), - Tab(text: 'Shop') - ], - views: [ - _MyCardsTab( - cardsListViewModel: widget._cardsListViewModel, - ), - _ShopTab(cardsListViewModel: widget._cardsListViewModel) - ]), - ) - ])); - }); - } -} - -Future showFilterWidget( - BuildContext context, CakePayCardsListViewModel cardsListViewModel) async { - return showPopUp( - context: context, - builder: (BuildContext context) { - return FilterWidget(filterItems: cardsListViewModel.createFilterItems); - }, - ); -} - -Future showCountryPicker( - BuildContext context, CakePayCardsListViewModel cardsListViewModel) async { - await showPopUp( - context: context, - builder: (_) => Picker( - title: S.of(context).select_your_country, - items: cardsListViewModel.availableCountries, - images: cardsListViewModel.availableCountries - .map((e) => Image.asset( - e.iconPath, - errorBuilder: (context, error, stackTrace) => Container( - width: 58, - height: 58, - ), - )) - .toList(), - selectedAtIndex: - cardsListViewModel.availableCountries.indexOf(cardsListViewModel.selectedCountry), - onItemSelected: (Country country) => cardsListViewModel.setSelectedCountry(country), - isSeparated: false, - hintText: S.of(context).search, - matchingCriteria: (Country country, String searchText) => - country.fullName.toLowerCase().contains(searchText.toLowerCase()))); -} - -class _TrailingIcon extends StatelessWidget { - const _TrailingIcon({required this.asset, this.onPressed, required this.iconColor}); - - final String asset; - final VoidCallback? onPressed; - final Color iconColor; - - @override - Widget build(BuildContext context) { - return Semantics( - label: S.of(context).profile, - child: Material( - color: Colors.transparent, - child: IconButton( - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - highlightColor: Colors.transparent, - onPressed: onPressed, - icon: ImageIcon(AssetImage(asset), size: 25, color: iconColor), - ), - )); - } -} - -class _MyCardsTab extends StatefulWidget { - const _MyCardsTab({required this.cardsListViewModel}); - - final CakePayCardsListViewModel cardsListViewModel; - - @override - State<_MyCardsTab> createState() => _MyCardsTabState(); -} - -class _MyCardsTabState extends State<_MyCardsTab> { - late final TextEditingController _searchController; - - @override - void initState() { - super.initState(); - - _searchController = TextEditingController( - text: widget.cardsListViewModel.searchMyCardsString, - ); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final viewModel = widget.cardsListViewModel; - return Observer(builder: (_) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(2, 6, 0, 6), - child: CakePaySearchBar( - initialQuery: viewModel.searchMyCardsString, - controller: _searchController, - onSearch: viewModel.setMyCardsQuery, - onFilter: () async {}, // TODO: implement filter - )), - Expanded( - child: Observer(builder: (_) { - final cards = viewModel.filteredUserCards; - if (viewModel.userCardState is UserCakePayCardsStateFetching) return const _Loading(); - if (viewModel.userCardState is UserCakePayCardsStateNoCards) - return Center(child: Text(S.of(context).no_cards_found)); - - final showThumb = cards.length > 6; - final userCardsList = Stack( - children: [ - GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 1.25, - crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 3 : 2, - crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5, - mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5), - padding: EdgeInsets.only(left: 2, right: showThumb ? 10 : 22), - itemCount: cards.length, - itemBuilder: (_, i) { - final card = cards[i]; - return UserCardItem( - logoUrl: card.cardImageUrl, - title: card.name, - subTitle: '\$100', - onTap: () => _showCardInfoBottomSheet(context, card), - ); - }, - ), - ], - ); - return showThumb - ? Scrollbar( - key: ValueKey('cake_pay_my_cards_tab_scrollbar_key'), - thumbVisibility: true, - trackVisibility: true, - child: userCardsList, - ) - : userCardsList; - }), - ), - ], - ); - }); - } -} - -Future _showCardInfoBottomSheet(BuildContext context, CakePayCard card) async { - bool isReloadable = false; // TODO: replace with real logic - if (card.name.toLowerCase().contains('prepaid')) { - isReloadable = true; - } - await showModalBottomSheet( - context: context, - isDismissible: false, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext bottomSheetContext) { - return isReloadable - ? CakePayCardInfoBottomSheet( - isReloadable: isReloadable, - titleText: 'Reloadable Card', - balance: '100 USD', - howToUse: card.howToUse, - footerType: FooterType.doubleActionButton, - applyBoxShadow: true, - contentImage: card.cardImageUrl, - leftActionButtonKey: const Key('cake_pay_cards_page_reload_card_left_button_key'), - doubleActionLeftButtonText: 'Archive', - onLeftActionButtonPressed: () {}, - rightActionButtonKey: const Key('cake_pay_cards_page_reload_card_right_button_key'), - doubleActionRightButtonText: 'Top Up', - onRightActionButtonPressed: () {}, - onUpdateBalancePressed: () {}) - : CakePayCardInfoBottomSheet( - isReloadable: isReloadable, - titleText: card.name, - balance: '500 USD', - howToUse: card.howToUse, - footerType: FooterType.singleActionButton, - applyBoxShadow: true, - contentImage: card.cardImageUrl, - singleActionButtonKey: const Key('cake_pay_cards_page_card_info_bottom_sheet_key'), - singleActionButtonText: 'Mark As Used', - onSingleActionButtonPressed: () {}, - onUpdateBalancePressed: () {}); - }, - ); -} - -class _ShopTab extends StatefulWidget { - const _ShopTab({required this.cardsListViewModel}); - - final CakePayCardsListViewModel cardsListViewModel; - - @override - State<_ShopTab> createState() => _ShopTabState(); -} - -class _ShopTabState extends State<_ShopTab> { - late final ScrollController _scroll; - - @override - void initState() { - super.initState(); - _scroll = ScrollController() - ..addListener(() { - if (!_scroll.hasClients) return; - final max = _scroll.position.maxScrollExtent; - - final threshold = 200.0; - if (_scroll.offset >= max - threshold && !_scroll.position.outOfRange) { - widget.cardsListViewModel.fetchNextPage(); - } - }); - } - - @override - void dispose() { - _scroll.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final viewModel = widget.cardsListViewModel; - - return Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(2, 6, 0, 6), - child: CakePaySearchBar( - initialQuery: viewModel.searchString, - onSearch: (String searchText) { - if (searchText != viewModel.searchString) { - viewModel.searchString = searchText; - viewModel.resetLoadingNextPageState(); - viewModel.getVendors(text: searchText); - } - }, - onFilter: () async { - viewModel.storeInitialFilterStates(); - await showFilterWidget(context, viewModel); - if (viewModel.hasFiltersChanged) { - viewModel.resetLoadingNextPageState(); - viewModel.getVendors(text: viewModel.searchString); - } - }, - onCountryPick: () async { - viewModel.storeInitialFilterStates(); - await showCountryPicker(context, viewModel); - if (viewModel.hasFiltersChanged) { - viewModel.resetLoadingNextPageState(); - viewModel.getVendors(text: viewModel.searchString); - } - }, - selectedCountry: viewModel.selectedCountry, - ), - ), - Expanded( - child: Observer(builder: (_) { - final vendors = viewModel.cakePayVendors; - - if (viewModel.vendorsState is! CakePayVendorLoadedState) { - return const _Loading(); - } - - if (vendors.isEmpty) return Center(child: Text(S.of(context).no_cards_found)); - - final loadingMore = viewModel.isLoadingNextPage; - final showThumb = vendors.length > 3; - final cardsList = Stack( - children: [ - GridView.builder( - controller: _scroll, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 2 : 1, - childAspectRatio: 5, - crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5, - mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5), - padding: EdgeInsets.only(left: 2, right: showThumb ? 10 : 22), - itemCount: vendors.length + (loadingMore ? 1 : 0), - itemBuilder: (_, i) { - if (i >= vendors.length) return const _Loading(); - final vendor = vendors[i]; - return CardItem( - logoUrl: vendor.card?.cardImageUrl, - title: vendor.name, - subTitle: vendor.card?.description ?? '', - onTap: () => Navigator.pushNamed(context, Routes.cakePayBuyCardPage, - arguments: [vendor])); - }, - ), - ], - ); - return showThumb - ? Scrollbar( - key: ValueKey('cake_pay_shop_tab_scrollbar_key'), - thumbVisibility: true, - trackVisibility: true, - controller: _scroll, - child: cardsList, - ) - : cardsList; - }), - ), - ], - ); - } -} - -class _Loading extends StatelessWidget { - const _Loading(); - - @override - Widget build(BuildContext context) => Center( - child: CircularProgressIndicator(), - ); -} diff --git a/lib/cake_pay/src/models/cake_pay_card.dart b/lib/cake_pay/src/models/cake_pay_card.dart deleted file mode 100644 index 4fdea0ae9..000000000 --- a/lib/cake_pay/src/models/cake_pay_card.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:hash_wallet/entities/fiat_currency.dart'; - -class CakePayCard { - final int id; - final String name; - final String? description; - final String? termsAndConditions; - final String? howToUse; - final String? expiryAndValidity; - final String? cardImageUrl; - final String? country; - final FiatCurrency fiatCurrency; - final String? minValueUsd; - final String? maxValueUsd; - final String? minValue; - final String? maxValue; - final List denominationItems; - - CakePayCard({ - required this.id, - required this.name, - this.description, - this.termsAndConditions, - this.howToUse, - this.expiryAndValidity, - this.cardImageUrl, - this.country, - required this.fiatCurrency, - this.minValueUsd, - this.maxValueUsd, - this.minValue, - this.maxValue, - List? denominationItems, - }) : denominationItems = denominationItems ?? const []; - - factory CakePayCard.fromJson(Map json) { - final name = stripHtmlIfNeeded(json['name'] as String? ?? ''); - final description = stripHtmlIfNeeded(json['description'] as String? ?? ''); - final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? ''); - final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? ''); - final fiatCurrency = FiatCurrency.deserialize(raw: json['currency_code'] as String? ?? ''); - - String? minValue = json['min_value'] as String?; - - final parsedMinValueLocal = _toDouble(json['min_value']); - final parsedMinValueUsd = _toDouble(json['min_value_usd']); - - if (parsedMinValueLocal != null && parsedMinValueLocal > 0 && parsedMinValueUsd != null && parsedMinValueUsd > 0 && parsedMinValueUsd < 10.0) { - final rate = parsedMinValueLocal / parsedMinValueUsd; - final minLocalValueLimit = 10.0 * rate; - minValue = minLocalValueLimit.toStringAsFixed(2); - } - - final raw = (json['denominations'] as List?) ?? const []; - final denominations = []; - for (final item in raw) { - if (item is Map) { - denominations.add(Denomination.fromJson(Map.from(item))); - } - } - - return CakePayCard( - id: json['id'] as int? ?? 0, - name: name, - description: description, - termsAndConditions: termsAndConditions, - howToUse: howToUse, - expiryAndValidity: json['expiry_and_validity'] as String?, - cardImageUrl: json['card_image_url'] as String?, - country: json['country'] as String?, - fiatCurrency: fiatCurrency, - minValueUsd: json['min_value_usd'] as String?, - maxValueUsd: json['max_value_usd'] as String?, - minValue: minValue, - maxValue: json['max_value'] as String?, - denominationItems: denominations, - ); - } - - static String stripHtmlIfNeeded(String text) { - return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' '); - } - static double? _toDouble(dynamic v) { - if (v == null) return null; - if (v is num) return v.toDouble(); - return double.tryParse(v.toString()); - } -} - -class Denomination { - final double value; - final int? cardId; - final double? usdValue; - - Denomination({ - required this.value, - required this.cardId, - this.usdValue, - }); - - static double? _toDouble(dynamic v) { - if (v == null) return null; - if (v is num) return v.toDouble(); - return double.tryParse(v.toString()); - } - - factory Denomination.fromJson(Map json) { - return Denomination( - value: _toDouble(json['value']) ?? 0, - cardId: json['card_id'] is int ? json['card_id'] as int : int.tryParse('${json['card_id']}'), - usdValue: _toDouble(json['usd_value']), - ); - } -} diff --git a/lib/cake_pay/src/models/cake_pay_order.dart b/lib/cake_pay/src/models/cake_pay_order.dart deleted file mode 100644 index 7d6b945bb..000000000 --- a/lib/cake_pay/src/models/cake_pay_order.dart +++ /dev/null @@ -1,214 +0,0 @@ -enum CakePayPaymentMethod { BTC, BTC_LN, XMR, LTC, LTC_MWEB, ZEC } - -extension CakePayPaymentMethodLabel on CakePayPaymentMethod { - String get label => switch (this) { - CakePayPaymentMethod.BTC => 'Bitcoin', - CakePayPaymentMethod.BTC_LN => 'Lightning', - CakePayPaymentMethod.XMR => 'Monero', - CakePayPaymentMethod.LTC => 'Litecoin', - CakePayPaymentMethod.LTC_MWEB => 'MWEB', - CakePayPaymentMethod.ZEC => 'Zcash', - }; -} - -class CakePayOrder { - final String orderId; - final List cards; - final String? externalId; - final double amountUsd; - final String totalReceiveAmount; - final int quantity; - final String status; - final String? vouchers; - final String fiatCurrencyCode; - final PaymentData paymentData; - - CakePayOrder({ - required this.orderId, - required this.cards, - required this.externalId, - required this.amountUsd, - required this.totalReceiveAmount, - required this.quantity, - required this.status, - required this.vouchers, - required this.fiatCurrencyCode, - required this.paymentData, - }); - - factory CakePayOrder.fromMap(Map map) { - - final cards = map['cards'] as List; - final firstCard = cards.isNotEmpty ? cards.first as Map : {}; - - return CakePayOrder( - orderId: map['order_id'] as String, - cards: cards - .map((x) => OrderCard.fromMap(x as Map)) - .toList(), - externalId: map['external_id'] as String?, - amountUsd: map['amount_usd'] as double, - totalReceiveAmount: firstCard['subtotal'] as String? ?? '', - quantity: firstCard['quantity'] as int, - status: map['status'] as String, - vouchers: map['vouchers'] as String?, - fiatCurrencyCode: firstCard['currency_code'] as String? ?? '', - paymentData: PaymentData.fromMap(map['payment_data'] as Map)); - } - - static CryptoPaymentData? getPaymentDataFor({CakePayPaymentMethod? method,CakePayOrder? order}) { - if (order == null || method == null) return null; - - final data = switch (method) { - CakePayPaymentMethod.BTC => order.paymentData.btc, - CakePayPaymentMethod.XMR => order.paymentData.xmr, - CakePayPaymentMethod.LTC => order.paymentData.ltc, - CakePayPaymentMethod.LTC_MWEB => order.paymentData.ltc_mweb, - CakePayPaymentMethod.ZEC => order.paymentData.zec, - CakePayPaymentMethod.BTC_LN => order.paymentData.btc_ln, - }; - - if (data == null) return null; - - final bip21 = data.paymentUrls?.bip21; - if (bip21 != null && bip21.isNotEmpty) { - final uri = Uri.parse(bip21); - final addr = uri.path; - final price = uri.queryParameters['amount'] ?? data.price; - - return CryptoPaymentData(price: price, address: addr, amount: data.amount, paymentUrls: data.paymentUrls); - } - - return data; - } - - static String getCurrencyCodeFromPaymentMethod(CakePayPaymentMethod method) { - return switch (method) { - CakePayPaymentMethod.BTC => 'BTC', - CakePayPaymentMethod.BTC_LN => 'BTC', - CakePayPaymentMethod.XMR => 'XMR', - CakePayPaymentMethod.LTC => 'LTC', - CakePayPaymentMethod.LTC_MWEB => 'LTC', - CakePayPaymentMethod.ZEC => 'ZEC', - }; - } -} - -class OrderCard { - final int cardId; - final int? externalId; - final String price; - final int quantity; - final String currencyCode; - final String? cardName; - final String? cardImagePath; - - OrderCard({ - required this.cardId, - required this.externalId, - required this.price, - required this.quantity, - required this.currencyCode, - required this.cardName, - required this.cardImagePath, - }); - - factory OrderCard.fromMap(Map map) { - return OrderCard( - cardId: map['card_id'] as int, - externalId: map['external_id'] as int?, - price: map['price'] as String, - quantity: map['quantity'] as int, - currencyCode: map['currency_code'] as String, - cardName: map['name'] as String?, - cardImagePath: map['card_image_url'] as String?, - ); - } -} - -class PaymentData { - final CryptoPaymentData? btc; - final CryptoPaymentData? btc_ln; - final CryptoPaymentData? xmr; - final CryptoPaymentData? ltc; - final CryptoPaymentData? ltc_mweb; - final CryptoPaymentData? zec; - final DateTime invoiceTime; - final DateTime expirationTime; - final int? commission; - - PaymentData({ - required this.btc, - required this.btc_ln, - required this.xmr, - required this.ltc, - required this.ltc_mweb, - required this.zec, - required this.invoiceTime, - required this.expirationTime, - required this.commission, - }); - - factory PaymentData.fromMap(Map map) { - return PaymentData( - btc: map['BTC'] != null ? CryptoPaymentData.fromMap(map['BTC'] as Map) : null, - btc_ln: map['BTC_LN'] != null ? CryptoPaymentData.fromMap(map['BTC_LN'] as Map) : null, - xmr: map['XMR'] != null ? CryptoPaymentData.fromMap(map['XMR'] as Map) : null, - ltc: map['LTC'] != null ? CryptoPaymentData.fromMap(map['LTC'] as Map) : null, - ltc_mweb: map['LTC_MWEB'] != null ? CryptoPaymentData.fromMap(map['LTC_MWEB'] as Map) : null, - zec: map['ZEC_CHAIN'] != null ? CryptoPaymentData.fromMap(map['ZEC_CHAIN'] as Map) : null, - invoiceTime: DateTime.fromMillisecondsSinceEpoch(map['invoice_time'] as int), - expirationTime: DateTime.fromMillisecondsSinceEpoch(map['expiration_time'] as int), - commission: map['commission'] as int?, - ); - } -} - -class CryptoPaymentData { - final String price; - final String amount; - final PaymentUrl? paymentUrls; - final String address; - - CryptoPaymentData({ - required this.price, - required this.amount, - this.paymentUrls, - required this.address, - }); - - factory CryptoPaymentData.fromMap(Map map) { - return CryptoPaymentData( - price: map['price'] as String, - amount: (map['amount_from'] as double?)?.toString() ?? '', - paymentUrls: PaymentUrl.fromMap(map['paymentUrls'] as Map?), - address: map['address'] as String, - ); - } -} - -class PaymentUrl { - final String? bip21; - final String? bip72; - final String? bip72b; - final String? bip73; - final String? bolt11; - - PaymentUrl({ - this.bip21, - this.bip72, - this.bip72b, - this.bip73, - this.bolt11, - }); - - factory PaymentUrl.fromMap(Map? map) { - return PaymentUrl( - bip21: map?['BIP21'] as String?, - bip72: map?['BIP72'] as String?, - bip72b: map?['BIP72b'] as String?, - bip73: map?['BIP73'] as String?, - bolt11: map?['BOLT11'] as String?, - ); - } -} diff --git a/lib/cake_pay/src/models/cake_pay_user_credentials.dart b/lib/cake_pay/src/models/cake_pay_user_credentials.dart deleted file mode 100644 index 5899b31ad..000000000 --- a/lib/cake_pay/src/models/cake_pay_user_credentials.dart +++ /dev/null @@ -1,6 +0,0 @@ -class CakePayUserCredentials { - const CakePayUserCredentials(this.username, this.token); - - final String username; - final String token; -} \ No newline at end of file diff --git a/lib/cake_pay/src/models/cake_pay_vendor.dart b/lib/cake_pay/src/models/cake_pay_vendor.dart deleted file mode 100644 index 5aeba77fb..000000000 --- a/lib/cake_pay/src/models/cake_pay_vendor.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:hash_wallet/entities/country.dart'; -import 'package:cw_core/utils/print_verbose.dart'; - -import 'cake_pay_card.dart'; - -class CakePayVendor { - final int id; - final String name; - final bool available; - final String? cakeWarnings; - final String country; - final CakePayCard? card; - - CakePayVendor({ - required this.id, - required this.name, - required this.available, - this.cakeWarnings, - required this.country, - this.card, - }); - - factory CakePayVendor.fromJson(Map json) { - final name = stripHtmlIfNeeded(json['name'] as String); - - final parsedCountry = json['country'] as String; - final country = Country.normalizeName(parsedCountry); - - var cardsJson = json['cards'] as List?; - CakePayCard? cardForVendor; - - if (cardsJson != null && cardsJson.isNotEmpty) { - try { - cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) { - return Country.normalizeName(card['country'] as String) == country; - }) as Map); - } catch (e) { - printV('Error parsing card for vendor: $e'); - } - } - - return CakePayVendor( - id: json['id'] as int, - name: name, - available: json['available'] as bool? ?? false, - cakeWarnings: json['cake_warnings'] as String?, - country: country, - card: cardForVendor, - ); - } - - static String stripHtmlIfNeeded(String text) { - return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' '); - } -} diff --git a/lib/cake_pay/src/services/cake_pay_api.dart b/lib/cake_pay/src/services/cake_pay_api.dart deleted file mode 100644 index 5a6bcf08e..000000000 --- a/lib/cake_pay/src/services/cake_pay_api.dart +++ /dev/null @@ -1,289 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_user_credentials.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_vendor.dart'; -import 'package:hash_wallet/utils/feature_flag.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/utils/print_verbose.dart'; - -class CakePayApi { - static const testBaseUri = FeatureFlag.hasDevOptions; - - static const baseTestCakePayUri = 'api-stg.cakepay.com'; - static const baseProdCakePayUri = 'api-prod.cakepay.com'; - - static const baseCakePayUri = testBaseUri ? baseTestCakePayUri : baseProdCakePayUri; - - static const vendorsPath = '/api/marketplace/vendors'; - static const authPath = '/api/accounts/auth'; - static final verifyEmailPath = '/api/accounts/auth/verify'; - static final logoutPath = '/api/accounts/logout'; - static final orderPath = '/api/orders/order'; - static final simulatePaymentPath = '/api/orders/simulate-payment'; - - /// AuthenticateUser - Future authenticateUser({required String email, required String apiKey}) async { - try { - final uri = Uri.https(baseCakePayUri, authPath); - final headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Api-Key $apiKey', - }; - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode({'email': email}), - ); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final bodyJson = json.decode(response.body) as Map; - - if (bodyJson.containsKey('user') && bodyJson['user']['email'] != null) { - return bodyJson['user']['email'] as String; - } - - throw Exception('Failed to authenticate user with error: $bodyJson'); - } catch (e) { - throw Exception('Failed to authenticate user with error: $e'); - } - } - - /// Verify email - Future verifyEmail({ - required String email, - required String code, - required String apiKey, - }) async { - final uri = Uri.https(baseCakePayUri, verifyEmailPath); - final headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Api-Key $apiKey', - }; - final query = {'email': email, 'otp': code}; - - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode(query), - ); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final bodyJson = json.decode(response.body) as Map; - - if (bodyJson.containsKey('error')) { - throw Exception(bodyJson['error'] as String); - } - - if (bodyJson.containsKey('token')) { - final token = bodyJson['token'] as String; - final userEmail = bodyJson['user']['email'] as String; - return CakePayUserCredentials(userEmail, token); - } else { - throw Exception('E-mail verification failed.'); - } - } - - /// createOrder - Future createOrder({ - required String apiKey, - required int cardId, - required String price, - required int quantity, - required String userEmail, - required String token, - required bool confirmsNoVpn, - required bool confirmsVoidedRefund, - required bool confirmsTermsAgreed, - }) async { - final uri = Uri.https(baseCakePayUri, orderPath); - - final headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Token $token', - }; - - final body = json.encode({ - 'card_id': cardId, - 'price': price, - 'quantity': quantity, - 'user_email': userEmail, - 'send_email': true, - 'confirms_no_vpn': confirmsNoVpn, - 'confirms_voided_refund': confirmsVoidedRefund, - 'confirms_terms_agreed': confirmsTermsAgreed, - }); - - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: body, - ); - - if (response.statusCode == 201) { - final data = json.decode(response.body) as Map; - return CakePayOrder.fromMap(data); - } - - String message = 'Server error ${response.statusCode}'; - - final isJson = response.headers['content-type']?.contains('application/json') == true || - response.body.trim().startsWith(RegExp(r'[\{\[]')); - - if (isJson) { - try { - final decoded = json.decode(response.body); - if (decoded is List && decoded.isNotEmpty) { - message = decoded.first.toString(); - } else if (decoded is Map && decoded['detail'] != null) { - message = decoded['detail'].toString(); - } else { - message = decoded.toString(); - } - } on FormatException {} - } - - throw Exception(message); - } - - /// Get Order by ID - Future getOrderById({required String orderId, required String token}) async { - final uri = Uri.https(baseCakePayUri, '$orderPath/$orderId'); - final headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Token $token', - }; - final response = await ProxyWrapper().get( - clearnetUri: uri, - headers: headers, - ); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final bodyJson = json.decode(response.body) as Map; - - return CakePayOrder.fromMap(bodyJson); - } - - ///Simulate Payment - Future simulatePayment( - {required String CSRFToken, - required String authorization, - required String orderId, - required String token}) async { - final uri = Uri.https(baseCakePayUri, simulatePaymentPath); - - final headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Token $token', - }; - - final body = json.encode({ - 'order_id': orderId, - }); - - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: body, - ); - - printV('Response: ${response.statusCode}'); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final bodyJson = json.decode(response.body) as Map; - - return 'You just SIMULATED a buying of a gift card with ID: ${bodyJson['order_id']}'; - } - - /// Logout - Future logoutUser({required String email, required String apiKey}) async { - final uri = Uri.https(baseCakePayUri, logoutPath); - final headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Api-Key $apiKey', - }; - - try { - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode({'email': email}), - ); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - } catch (e) { - printV('Caught exception: $e'); - } - } - - /// Get Vendors - Future> getVendors({ - required String apiKey, - required int page, - required String countryCode, - String? country, - String? search, - List? vendorIds, - bool? giftCards = true, - bool? prepaidCards = true, - bool? onDemand, - bool? custom, - }) async { - var queryParams = { - 'page': page.toString(), - 'country_code': countryCode, - if (search != null && search.isNotEmpty) 'search': search, - if (vendorIds != null && vendorIds.isNotEmpty) 'vendor_ids': vendorIds.join(','), - }; - - if (giftCards == false || prepaidCards == false) { - queryParams['gift_cards'] = giftCards.toString(); - queryParams['prepaid_cards'] = prepaidCards.toString(); - } - - final uri = Uri.https(baseCakePayUri, vendorsPath, queryParams); - - var headers = { - 'accept': 'application/json; charset=UTF-8', - 'Content-Type': 'application/json; charset=UTF-8', - 'Authorization': 'Api-Key $apiKey', - }; - - var response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); - - if (response.statusCode != 200) { - throw Exception( - 'Failed to fetch vendors: statusCode - ${response.statusCode}, queryParams -$queryParams, response - ${response.body}'); - } - - final bodyJson = json.decode(response.body); - - if (bodyJson is List && bodyJson.isEmpty) { - return []; - } - - return (bodyJson['results'] as List) - .map((e) => CakePayVendor.fromJson(e as Map)) - .toList(); - } -} diff --git a/lib/cake_pay/src/services/cake_pay_service.dart b/lib/cake_pay/src/services/cake_pay_service.dart deleted file mode 100644 index 01f751efb..000000000 --- a/lib/cake_pay/src/services/cake_pay_service.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/cake_pay/src/services/cake_pay_api.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_vendor.dart'; -import 'package:hash_wallet/core/secure_storage.dart'; - -class CakePayService { - CakePayService(this.secureStorage, this.cakePayApi); - - /// `_v2` was added to log out users so they can login using the new backend - static const cakePayEmailStorageKey = 'cake_pay_email_v2'; - static const cakePayUsernameStorageKey = 'cake_pay_username_v2'; - static const cakePayUserTokenKey = 'cake_pay_user_token_v2'; - - static String get testCakePayApiKey => secrets.testCakePayApiKey; - - static String get cakePayApiKey => secrets.cakePayApiKey; - - static String get CSRFToken => secrets.CSRFToken; - - static String get authorization => secrets.authorization; - - final SecureStorage secureStorage; - final CakePayApi cakePayApi; - - /// Get Vendors - Future> getVendors({ - required int page, - required String countryCode, - String? country, - String? search, - List? vendorIds, - bool? giftCards, - bool? prepaidCards, - bool? onDemand, - bool? custom, - }) async { - final result = await cakePayApi.getVendors( - apiKey: cakePayApiKey, - page: page, - country: country, - countryCode: countryCode, - search: search, - vendorIds: vendorIds, - giftCards: giftCards, - prepaidCards: prepaidCards, - onDemand: onDemand, - custom: custom); - return result; - } - - /// LogIn - Future logIn(String email) async { - final userName = await cakePayApi.authenticateUser(email: email, apiKey: cakePayApiKey); - await secureStorage.write(key: cakePayEmailStorageKey, value: userName); - await secureStorage.write(key: cakePayUsernameStorageKey, value: userName); - } - - /// Verify email - Future verifyEmail(String code) async { - final email = (await secureStorage.read(key: cakePayEmailStorageKey))!; - final credentials = - await cakePayApi.verifyEmail(email: email, code: code, apiKey: cakePayApiKey); - await secureStorage.write(key: cakePayUserTokenKey, value: credentials.token); - await secureStorage.write(key: cakePayUsernameStorageKey, value: credentials.username); - } - - Future getUserEmail() async { - return (await secureStorage.read(key: cakePayEmailStorageKey)); - } - - /// Check is user logged - Future isLogged() async { - final username = await secureStorage.read(key: cakePayUsernameStorageKey) ?? ''; - final password = await secureStorage.read(key: cakePayUserTokenKey) ?? ''; - return username.isNotEmpty && password.isNotEmpty; - } - - /// Logout - Future logout([String? email]) async { - await secureStorage.delete(key: cakePayUsernameStorageKey); - await secureStorage.delete(key: cakePayUserTokenKey); - if (email != null) { - await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey); - } - } - - /// Purchase Gift Card - Future createOrder({ - required int cardId, - required String price, - required int quantity, - required bool confirmsNoVpn, - required bool confirmsVoidedRefund, - required bool confirmsTermsAgreed, - }) async { - final userEmail = (await secureStorage.read(key: cakePayEmailStorageKey))!; - final token = (await secureStorage.read(key: cakePayUserTokenKey))!; - return await cakePayApi.createOrder( - apiKey: cakePayApiKey, - cardId: cardId, - price: price, - quantity: quantity, - token: token, - userEmail: userEmail, - confirmsNoVpn: confirmsNoVpn, - confirmsVoidedRefund: confirmsVoidedRefund, - confirmsTermsAgreed: confirmsTermsAgreed, - ); - } - - Future findOrderById({required String orderId}) async { - final token = (await secureStorage.read(key: cakePayUserTokenKey))!; - return await cakePayApi.getOrderById(orderId: orderId, token: token); - } - - ///Simulate Purchase Gift Card - Future simulatePayment({required String orderId}) async { - final token = (await secureStorage.read(key: cakePayUserTokenKey))!; - return await cakePayApi.simulatePayment( - CSRFToken: CSRFToken, authorization: authorization, token: token, orderId: orderId); - } -} diff --git a/lib/cake_pay/src/widgets/cake_pay_alert_modal.dart b/lib/cake_pay/src/widgets/cake_pay_alert_modal.dart deleted file mode 100644 index 61ed0cce7..000000000 --- a/lib/cake_pay/src/widgets/cake_pay_alert_modal.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:hash_wallet/src/widgets/alert_background.dart'; -import 'package:hash_wallet/src/widgets/primary_button.dart'; -import 'package:flutter/material.dart'; - -class CakePayAlertModal extends StatelessWidget { - const CakePayAlertModal({ - super.key, - required this.title, - required this.content, - required this.actionTitle, - this.showCloseButton = true, - this.dismissible = false, - }); - - final String title; - final Widget content; - final String actionTitle; - final bool showCloseButton; - final bool dismissible; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final maxHeight = MediaQuery.of(context).size.height * 0.8; - - return AlertBackground( - dismissible: dismissible, - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: maxHeight), - child: Material( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Container( - decoration: BoxDecoration( - color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(30)), - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (title.isNotEmpty) ...[ - Text(title, - style: theme.textTheme.titleLarge?.copyWith( - color: theme.colorScheme.onSurface, fontWeight: FontWeight.bold), - textAlign: TextAlign.center), - const SizedBox(height: 12), - ], - Flexible(child: SingleChildScrollView(child: content)), - const SizedBox(height: 24), - PrimaryButton( - onPressed: () => Navigator.pop(context), - text: actionTitle, - color: theme.colorScheme.surfaceContainer, - textColor: theme.colorScheme.primary), - if (showCloseButton) ...[ - const SizedBox(height: 16), - InkWell( - onTap: () => Navigator.pop(context), - child: CircleAvatar( - backgroundColor: theme.colorScheme.surfaceContainer, - child: Icon(Icons.close, color: theme.colorScheme.onSurface), - ), - ), - ], - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/cake_pay/src/widgets/cake_pay_search_bar_widget.dart b/lib/cake_pay/src/widgets/cake_pay_search_bar_widget.dart deleted file mode 100644 index cc3aeb3b1..000000000 --- a/lib/cake_pay/src/widgets/cake_pay_search_bar_widget.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:hash_wallet/entities/country.dart'; -import 'package:hash_wallet/src/widgets/search_bar_widget.dart'; -import 'package:hash_wallet/utils/debounce.dart'; -import 'package:flutter/material.dart'; - -class CakePaySearchBar extends StatefulWidget { - const CakePaySearchBar( - {required this.initialQuery, - required this.onSearch, - required this.onFilter, - this.onCountryPick, - this.controller, - this.selectedCountry}); - - final String initialQuery; - final ValueChanged onSearch; - final VoidCallback onFilter; - final VoidCallback? onCountryPick; - final Country? selectedCountry; - final TextEditingController? controller; - - @override - State createState() => _CakePaySearchBarState(); -} - -class _CakePaySearchBarState extends State { - late final TextEditingController _searchController = - widget.controller ?? TextEditingController(text: widget.initialQuery); - final _searchFocusNode = FocusNode(); - final _debounce = Debounce(const Duration(milliseconds: 500)); - - @override - void initState() { - super.initState(); - _searchController - .addListener(() => _debounce.run(() => widget.onSearch(_searchController.text))); - } - - @override - void dispose() { - _searchController.dispose(); - _searchFocusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 32, - child: Row( - children: [ - Expanded(child: SearchBarWidget(searchController: _searchController)), - const SizedBox(width: 5), - _CakePayFilterButton(onFilter: widget.onFilter), - if (widget.selectedCountry != null) - _CountryPickerWidget( - onTap: widget.onCountryPick, - selectedCountry: widget.selectedCountry!, - ), - ], - ), - ); - } -} - -class _CakePayFilterButton extends StatelessWidget { - const _CakePayFilterButton({required this.onFilter}); - - final VoidCallback onFilter; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onFilter, - child: Container( - width: 32, - padding: const EdgeInsets.symmetric(vertical: 7), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(10)), - child: Image.asset('assets/images/filter_icon.png', - color: Theme.of(context).colorScheme.onSurface)), - ); - } -} - -class _CountryPickerWidget extends StatelessWidget { - const _CountryPickerWidget({required this.selectedCountry, this.onTap}); - - final Country selectedCountry; - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 5), - child: GestureDetector( - onTap: onTap, - child: Container( - height: 32, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(10)), - child: Row( - children: [ - Image.asset(selectedCountry.iconPath, - width: 24, - height: 24, - errorBuilder: (_, __, ___) => const SizedBox(width: 24, height: 24)), - const SizedBox(width: 6), - Text( - selectedCountry.countryCode, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/cake_pay/src/widgets/cake_pay_tile.dart b/lib/cake_pay/src/widgets/cake_pay_tile.dart deleted file mode 100644 index 31f6fda70..000000000 --- a/lib/cake_pay/src/widgets/cake_pay_tile.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; - -class CakePayTile extends StatelessWidget { - const CakePayTile({ - Key? key, - required this.title, - required this.subTitle, - this.onTap, - }) : super(key: key); - - final VoidCallback? onTap; - final String title; - final String subTitle; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return GestureDetector( - onTap: onTap, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - SizedBox(height: 8), - Text( - subTitle, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: colorScheme.onSurface, - fontWeight: FontWeight.w600, - ), - ), - ], - ) - ], - ), - ); - } -} diff --git a/lib/cake_pay/src/widgets/card_item.dart b/lib/cake_pay/src/widgets/card_item.dart deleted file mode 100644 index 9b4659070..000000000 --- a/lib/cake_pay/src/widgets/card_item.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import 'image_placeholder.dart'; - -class CardItem extends StatelessWidget { - CardItem({ - required this.title, - required this.subTitle, - this.isAmount = false, - this.onTap, - this.logoUrl, - }); - - final VoidCallback? onTap; - final String title; - final String subTitle; - final String? logoUrl; - final bool isAmount; - - @override - Widget build(BuildContext context) { - return Theme( - data: ThemeData( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - child: InkWell( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(10), - ), - child: Row( - children: [ - if (logoUrl != null) - AspectRatio( - aspectRatio: 1.8, - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(10)), - child: Image.network( - logoUrl!, - fit: BoxFit.cover, - loadingBuilder: - (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) return child; - return Center(child: CircularProgressIndicator()); - }, - errorBuilder: (context, error, stackTrace) => CakePayCardImagePlaceholder(), - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(fontSize: 18, fontWeight: FontWeight.w700)), - Text(subTitle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(fontSize: 10, fontWeight: FontWeight.w700)), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/cake_pay/src/widgets/denominations_amount_widget.dart b/lib/cake_pay/src/widgets/denominations_amount_widget.dart deleted file mode 100644 index ac6cba108..000000000 --- a/lib/cake_pay/src/widgets/denominations_amount_widget.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:hash_wallet/cake_pay/src/models/cake_pay_card.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/src/widgets/number_text_fild_widget.dart'; -import 'package:hash_wallet/typography.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart'; -import 'package:hash_wallet/view_model/dashboard/dropdown_filter_item_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; - -class DenominationsAmountWidget extends StatefulWidget { - const DenominationsAmountWidget({ - required this.fiatCurrency, - required this.denominations, - required this.amountFieldFocus, - required this.amountController, - required this.quantityFieldFocus, - required this.quantityController, - required this.cakePayBuyCardViewModel, - required this.onAmountChanged, - required this.onQuantityChanged, - }); - - final String fiatCurrency; - final List denominations; - final FocusNode amountFieldFocus; - final TextEditingController amountController; - final FocusNode quantityFieldFocus; - final TextEditingController quantityController; - final CakePayBuyCardViewModel cakePayBuyCardViewModel; - final Function(String) onAmountChanged; - final Function(int?) onQuantityChanged; - - @override - State createState() => _DenominationsAmountWidgetState(); -} - -class _DenominationsAmountWidgetState extends State { - late (String, int?) _selected; - - @override - void initState() { - super.initState(); - - final first = widget.denominations.first; - final amount = widget.amountController.text.isNotEmpty - ? widget.amountController.text - : first.value.toString(); - _selected = (amount, first.cardId); - widget.cakePayBuyCardViewModel.selectedDenomination = _selected; - - widget.amountController.text = _selected.$1; - widget.onAmountChanged(_selected.$1); - } - - @override - Widget build(BuildContext context) { - return Container( - height: MediaQuery.of(context).size.height * 0.1, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - flex: 8, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - DropdownFilterList( - items: widget.denominations - .map((e) => e.value.toString()) - .toList(), - itemPrefix: widget.fiatCurrency, - selectedItem: _selected.$1, - onItemSelected: (value) { - setState(() => _selected = (value, widget.denominations - .firstWhere((e) => e.value.toString() == value) - .cardId)); - widget.amountController.text = value; - widget.onAmountChanged(value); - widget.cakePayBuyCardViewModel.selectedDenomination = (_selected.$1, _selected.$2); - }, - ), - const SizedBox(height: 4), - Container( - width: double.infinity, - decoration: BoxDecoration( - border: Border( - top: BorderSide( - width: 1.0, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - child: Text( - S.of(context).value, - maxLines: 2, - style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - ), - ], - ), - ), - Spacer(), - Expanded( - flex: 5, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - NumberTextField( - controller: widget.quantityController, - focusNode: widget.quantityFieldFocus, - min: 1, - max: 99, - onChanged: (value) => widget.onQuantityChanged(value), - ), - const SizedBox(height: 4), - Container( - width: double.infinity, - decoration: BoxDecoration( - border: Border( - top: BorderSide( - width: 1.0, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - child: Text( - S.of(context).quantity, - maxLines: 1, - style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - ), - ], - ), - ), - Spacer(), - Expanded( - flex: 8, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Observer( - builder: (_) => Text( - '${widget.fiatCurrency} ${widget.cakePayBuyCardViewModel.totalAmount}', - maxLines: 1, - style: Theme.of(context).textTheme.titleMedium!, - ), - ), - const SizedBox(height: 4), - Container( - width: double.infinity, - decoration: BoxDecoration( - border: Border( - top: BorderSide( - width: 1.0, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - child: Text( - S.of(context).total, - maxLines: 1, - style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/cake_pay/src/widgets/enter_amount_widget.dart b/lib/cake_pay/src/widgets/enter_amount_widget.dart deleted file mode 100644 index b70ae3d03..000000000 --- a/lib/cake_pay/src/widgets/enter_amount_widget.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/src/widgets/base_text_form_field.dart'; -import 'package:hash_wallet/typography.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class EnterAmountWidget extends StatelessWidget { - const EnterAmountWidget( - {required this.minValue, - required this.maxValue, - required this.fiatCurrency, - required this.amountFieldFocus, - required this.amountController, - required this.onAmountChanged}); - - final String minValue; - final String maxValue; - final String fiatCurrency; - final FocusNode amountFieldFocus; - final TextEditingController amountController; - final Function(String) onAmountChanged; - - @override - Widget build(BuildContext context) { - return Container( - height: MediaQuery.of(context).size.height * 0.1, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Spacer(flex: 1), - Text( - S.of(context).enter_amount, - style: Theme.of(context).textTheme.titleLarge!, - ), - Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant))), - child: BaseTextFormField( - isDense: true, - contentPadding: EdgeInsets.zero, - controller: amountController, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - hintText: '0.00', - maxLines: 1, - prefixIcon: Padding( - padding: const EdgeInsets.only(top: 2.0), - child: Text( - '$fiatCurrency: ', - style: Theme.of(context).textTheme.titleMedium!, - ), - ), - prefixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0), - suffixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0), - suffixIcon: Padding( - padding: const EdgeInsets.only(top: 2, right: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('QTY ', style: Theme.of(context).textTheme.titleMedium!), - Text( - '1', - style: Theme.of(context).textTheme - .titleMedium! - .copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant) - ), - ], - ), - ), - textStyle: Theme.of(context).textTheme.titleMedium!, - placeholderTextStyle: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')), - FilteringTextInputFormatter.allow( - RegExp(r'^\d+(\.|\,)?\d{0,2}'), - ), - ], - ), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(S.of(context).min_amount(minValue) + ' $fiatCurrency', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - )), - Text(S.of(context).max_amount(maxValue) + ' $fiatCurrency', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - )), - ], - ), - Spacer(flex: 1), - ], - ), - ); - } -} diff --git a/lib/cake_pay/src/widgets/flip_card_widget.dart b/lib/cake_pay/src/widgets/flip_card_widget.dart deleted file mode 100644 index 83f65e83e..000000000 --- a/lib/cake_pay/src/widgets/flip_card_widget.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:math' as math; -import 'package:flutter/material.dart'; - -class FlipCard extends StatefulWidget { - const FlipCard({ - super.key, - required this.front, - required this.back, - this.duration = const Duration(milliseconds: 400), - this.flipOnTouch = true, - }); - - final Widget front; - final Widget back; - final Duration duration; - final bool flipOnTouch; - - @override - FlipCardState createState() => FlipCardState(); -} - -class FlipCardState extends State with SingleTickerProviderStateMixin { - late final AnimationController _ctrl = - AnimationController(vsync: this, duration: widget.duration); - bool _isFront = true; - - void toggleCard() { - if (_isFront) { - _ctrl.forward(); - } else { - _ctrl.reverse(); - } - _isFront = !_isFront; - } - - @override - Widget build(BuildContext context) { - final content = AnimatedBuilder( - animation: _ctrl, - builder: (_, __) { - final angle = _ctrl.value * math.pi; - final isFront = angle < math.pi / 2; - return Transform( - alignment: Alignment.center, - transform: Matrix4.identity() - ..setEntry(3, 2, 0.001) - ..rotateY(angle), - child: isFront ? widget.front - : Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(math.pi), - child: widget.back, - ), - ); - }, - ); - - return widget.flipOnTouch - ? GestureDetector(onTap: toggleCard, child: content) - : content; - } -} diff --git a/lib/cake_pay/src/widgets/image_placeholder.dart b/lib/cake_pay/src/widgets/image_placeholder.dart deleted file mode 100644 index 3643a2e86..000000000 --- a/lib/cake_pay/src/widgets/image_placeholder.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; - -class CakePayCardImagePlaceholder extends StatelessWidget { - const CakePayCardImagePlaceholder({this.text}); - - final String? text; - - @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: 1.8, - child: Container( - child: Center( - child: Text( - text ?? 'Image not found!', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.w900, - ), - ), - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - ), - ), - ); - } -} diff --git a/lib/cake_pay/src/widgets/link_extractor.dart b/lib/cake_pay/src/widgets/link_extractor.dart deleted file mode 100644 index d81dc492c..000000000 --- a/lib/cake_pay/src/widgets/link_extractor.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class ClickableLinksText extends StatelessWidget { - const ClickableLinksText({ - required this.text, - required this.textStyle, - this.linkStyle, - }); - - final String text; - final TextStyle textStyle; - final TextStyle? linkStyle; - - @override - Widget build(BuildContext context) { - List spans = []; - RegExp linkRegExp = RegExp(r'(https?://[^\s]+)'); - Iterable matches = linkRegExp.allMatches(text); - - int previousEnd = 0; - matches.forEach((match) { - if (match.start > previousEnd) { - spans.add(TextSpan(text: text.substring(previousEnd, match.start), style: textStyle)); - } - String url = text.substring(match.start, match.end); - if (url.toLowerCase().endsWith('.md')) { - spans.add( - TextSpan( - text: url, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontSize: 18, - ), - recognizer: TapGestureRecognizer() - ..onTap = () async { - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); - } - }, - ), - ); - } else { - spans.add( - TextSpan( - text: url, - style: linkStyle, - recognizer: TapGestureRecognizer() - ..onTap = () { - launchUrl(Uri.parse(url)); - }, - ), - ); - } - previousEnd = match.end; - }); - - if (previousEnd < text.length) { - spans.add(TextSpan(text: text.substring(previousEnd), style: textStyle)); - } - - return RichText(text: TextSpan(children: spans)); - } -} diff --git a/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart b/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart deleted file mode 100644 index 6a9889774..000000000 --- a/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; - -class RoundedOverlayCards extends StatelessWidget { - const RoundedOverlayCards({ - this.topCardChild = const SizedBox(), - this.bottomCardChild = const SizedBox(), - }); - - final Widget topCardChild; - final Widget bottomCardChild; - - @override - Widget build(BuildContext context) { - final screenHeight = MediaQuery.of(context).size.height; - return ClipRRect( - borderRadius: - BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)), - child: Container( - height: screenHeight * 0.50, - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24), - ), - color: Theme.of(context).colorScheme.surfaceContainer - ), - child: Column( - children: [ - ClipRRect( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), - color: Theme.of(context).colorScheme.surfaceContainerLow, - ), - height: screenHeight * 0.38, - width: double.infinity, - child: topCardChild)), - bottomCardChild, - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/cake_pay/src/widgets/text_icon_button.dart b/lib/cake_pay/src/widgets/text_icon_button.dart deleted file mode 100644 index 6f912d6c7..000000000 --- a/lib/cake_pay/src/widgets/text_icon_button.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; - -class TextIconButton extends StatelessWidget { - const TextIconButton({ - Key? key, - required this.label, - this.onTap, - }) : super(key: key); - - final String label; - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge!.color, - fontSize: 16, - fontWeight: FontWeight.w900, - ), - ), - Icon( - Icons.chevron_right_rounded, - color: Theme.of(context).textTheme.titleLarge!.color, - ), - ], - ), - ); - } -} diff --git a/lib/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart b/lib/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart deleted file mode 100644 index 77444b546..000000000 --- a/lib/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart +++ /dev/null @@ -1,203 +0,0 @@ -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/src/widgets/base_alert_dialog.dart'; -import 'package:hash_wallet/src/widgets/standard_checkbox.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class ThreeCheckboxAlert extends BaseAlertDialog { - ThreeCheckboxAlert({ - required this.alertTitle, - required this.leftButtonText, - required this.rightButtonText, - required this.actionLeftButton, - required this.actionRightButton, - this.alertBarrierDismissible = true, - Key? key, - }); - - final String alertTitle; - final String leftButtonText; - final String rightButtonText; - final VoidCallback actionLeftButton; - final Function(bool, bool, bool) actionRightButton; - final bool alertBarrierDismissible; - - bool checkbox1 = false; - void toggleCheckbox1() => checkbox1 = !checkbox1; - bool checkbox2 = false; - void toggleCheckbox2() => checkbox2 = !checkbox2; - bool checkbox3 = false; - void toggleCheckbox3() => checkbox3 = !checkbox3; - - bool showValidationMessage = true; - - @override - String get titleText => alertTitle; - - @override - bool get isDividerExists => true; - - @override - String get leftActionButtonText => leftButtonText; - - @override - String get rightActionButtonText => rightButtonText; - - @override - VoidCallback get actionLeft => actionLeftButton; - - @override - VoidCallback get actionRight => () { - actionRightButton(checkbox1, checkbox2, checkbox3); - }; - - @override - bool get barrierDismissible => alertBarrierDismissible; - - @override - Widget content(BuildContext context) { - return ThreeCheckboxAlertContent( - checkbox1: checkbox1, - toggleCheckbox1: toggleCheckbox1, - checkbox2: checkbox2, - toggleCheckbox2: toggleCheckbox2, - checkbox3: checkbox3, - toggleCheckbox3: toggleCheckbox3, - ); - } -} - -class ThreeCheckboxAlertContent extends StatefulWidget { - ThreeCheckboxAlertContent({ - required this.checkbox1, - required this.toggleCheckbox1, - required this.checkbox2, - required this.toggleCheckbox2, - required this.checkbox3, - required this.toggleCheckbox3, - Key? key, - }) : super(key: key); - - bool checkbox1; - void Function() toggleCheckbox1; - bool checkbox2; - void Function() toggleCheckbox2; - bool checkbox3; - void Function() toggleCheckbox3; - - @override - _ThreeCheckboxAlertContentState createState() => _ThreeCheckboxAlertContentState( - checkbox1: checkbox1, - toggleCheckbox1: toggleCheckbox1, - checkbox2: checkbox2, - toggleCheckbox2: toggleCheckbox2, - checkbox3: checkbox3, - toggleCheckbox3: toggleCheckbox3, - ); - - static _ThreeCheckboxAlertContentState? of(BuildContext context) { - return context.findAncestorStateOfType<_ThreeCheckboxAlertContentState>(); - } -} - -class _ThreeCheckboxAlertContentState extends State { - _ThreeCheckboxAlertContentState({ - required this.checkbox1, - required this.toggleCheckbox1, - required this.checkbox2, - required this.toggleCheckbox2, - required this.checkbox3, - required this.toggleCheckbox3, - }); - - bool checkbox1; - void Function() toggleCheckbox1; - bool checkbox2; - void Function() toggleCheckbox2; - bool checkbox3; - void Function() toggleCheckbox3; - - bool showValidationMessage = true; - - bool get areAllCheckboxesChecked => checkbox1 && checkbox2 && checkbox3; - - @override - Widget build(BuildContext context) { - return Form( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - StandardCheckbox( - value: checkbox1, - caption: S.of(context).cakepay_confirm_no_vpn, - onChanged: (bool? value) { - setState(() { - checkbox1 = value ?? false; - toggleCheckbox1(); - showValidationMessage = !areAllCheckboxesChecked; - }); - }, - ), - StandardCheckbox( - value: checkbox2, - caption: S.of(context).cakepay_confirm_voided_refund, - onChanged: (bool? value) { - setState(() { - checkbox2 = value ?? false; - toggleCheckbox2(); - showValidationMessage = !areAllCheckboxesChecked; - }); - }, - ), - StandardCheckbox( - value: checkbox3, - caption: S.of(context).cakepay_confirm_terms_agreed, - onChanged: (bool? value) { - setState(() { - checkbox3 = value ?? false; - toggleCheckbox3(); - showValidationMessage = !areAllCheckboxesChecked; - }); - }, - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse("https://cakepay.com/cakepay-web-terms.txt"), - mode: LaunchMode.externalApplication, - ), - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - S.of(context).settings_terms_and_conditions, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Colors.blueAccent, - decoration: TextDecoration.none, - height: 1, - ), - softWrap: true, - ), - ), - ), - if (showValidationMessage) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - 'Please confirm all checkboxes', - style: TextStyle( - color: Colors.red, - fontSize: 14, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - decoration: TextDecoration.none, - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/cake_pay/src/widgets/user_card_item.dart b/lib/cake_pay/src/widgets/user_card_item.dart deleted file mode 100644 index 70ae447fe..000000000 --- a/lib/cake_pay/src/widgets/user_card_item.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:flutter/material.dart'; -import 'image_placeholder.dart'; - -class UserCardItem extends StatelessWidget { - UserCardItem({ - required this.title, - required this.subTitle, - this.onTap, - this.logoUrl - }); - - final VoidCallback? onTap; - final String title; - final String subTitle; - final String? logoUrl; - - @override - Widget build(BuildContext context) { - return Theme( - data: ThemeData( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - child: InkWell( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(15), - ), - child: Column( - children: [ - if (logoUrl != null) - Padding( - padding: const EdgeInsets.all(2.0), - child: AspectRatio( - aspectRatio: 1.65, - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(13)), - child: Image.network( - logoUrl!, - fit: BoxFit.cover, - loadingBuilder: - (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) return child; - return Center(child: CircularProgressIndicator()); - }, - errorBuilder: (context, error, stackTrace) => CakePayCardImagePlaceholder(), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - child: Row( - children: [ - Expanded( - child: Text(title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(fontSize: 16, fontWeight: FontWeight.w700)), - ), - SizedBox(width: 8), - Text(subTitle, - maxLines: 1, - overflow: TextOverflow.clip, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(fontSize: 16, fontWeight: FontWeight.w700)), - ], - ), - ), - ], - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/core/trade_monitor.dart b/lib/core/trade_monitor.dart index def54a035..674b56b29 100644 --- a/lib/core/trade_monitor.dart +++ b/lib/core/trade_monitor.dart @@ -1,23 +1,12 @@ import 'dart:async'; import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/simpleswap_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; import 'package:hash_wallet/exchange/trade.dart'; import 'package:hash_wallet/exchange/trade_state.dart'; import 'package:hash_wallet/store/dashboard/trades_store.dart'; import 'package:hash_wallet/entities/exchange_api_mode.dart'; import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/exolix_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/letsexchange_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/swaptrade_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/sideshift_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:hash_wallet/store/app_store.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -38,31 +27,13 @@ class TradeMonitor { final SharedPreferences preferences; ExchangeProvider? _getProviderByDescription(ExchangeProviderDescription description) { + // Providers whose implementations were removed (ChangeNow, SideShift, + // SimpleSwap, Exolix, ThorChain, SwapTrade, LetsExchange, StealthEx, + // Chainflip, XOSwap, SwapsXyz) fall through to null; their trades are + // skipped as "provider not supported". switch (description) { - case ExchangeProviderDescription.changeNow: - return ChangeNowExchangeProvider(settingsStore: appStore.settingsStore); - case ExchangeProviderDescription.sideShift: - return SideShiftExchangeProvider(); - case ExchangeProviderDescription.simpleSwap: - return SimpleSwapExchangeProvider(); case ExchangeProviderDescription.trocador: return TrocadorExchangeProvider(); - case ExchangeProviderDescription.exolix: - return ExolixExchangeProvider(); - case ExchangeProviderDescription.thorChain: - return ThorChainExchangeProvider(); - case ExchangeProviderDescription.swapTrade: - return SwapTradeExchangeProvider(); - case ExchangeProviderDescription.letsExchange: - return LetsExchangeExchangeProvider(); - case ExchangeProviderDescription.stealthEx: - return StealthExExchangeProvider(); - case ExchangeProviderDescription.chainflip: - return ChainflipExchangeProvider(); - case ExchangeProviderDescription.xoSwap: - return XOSwapExchangeProvider(); - case ExchangeProviderDescription.swapsXyz: - return SwapsXyzExchangeProvider(); case ExchangeProviderDescription.nearIntents: return NearIntentsExchangeProvider(); } diff --git a/lib/di.dart b/lib/di.dart index 29f32218c..c8bca35b3 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -8,14 +8,7 @@ import 'package:hash_wallet/bitcoin/bitcoin.dart'; import 'package:hash_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:hash_wallet/entities/bitcoin_amount_display_mode.dart'; import 'package:hash_wallet/evm/evm.dart'; -import 'package:hash_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:hash_wallet/buy/moonpay/moonpay_provider.dart'; -import 'package:hash_wallet/buy/onramper/onramper_buy_provider.dart'; -import 'package:hash_wallet/buy/robinhood/robinhood_buy_provider.dart'; -import 'package:hash_wallet/cake_pay/cake_pay.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_vendor.dart'; -import 'package:hash_wallet/cake_pay/src/services/cake_pay_api.dart'; -import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart'; import 'package:hash_wallet/core/auth_service.dart'; import 'package:hash_wallet/core/backup_service_v3.dart'; import 'package:hash_wallet/core/key_service.dart'; @@ -209,14 +202,9 @@ import 'package:hash_wallet/view_model/bridge/bridge_history_view_model.dart'; import 'package:hash_wallet/view_model/buy/buy_amount_view_model.dart'; import 'package:hash_wallet/view_model/buy/buy_sell_view_model.dart'; import 'package:hash_wallet/view_model/buy/buy_view_model.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_account_view_model.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart'; import 'package:hash_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:hash_wallet/view_model/contact_list/contact_view_model.dart'; import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart'; -import 'package:hash_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:hash_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:hash_wallet/view_model/dashboard/home_settings_view_model.dart'; @@ -309,8 +297,6 @@ import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:trezor_connect/trezor_connect.dart'; -import 'buy/kryptonim/kryptonim.dart'; -import 'buy/meld/meld_buy_provider.dart'; import 'dogecoin/dogecoin.dart'; import 'new-ui/viewmodels/card_customizer/card_customizer_bloc.dart'; import 'new-ui/widgets/addresses_page/address_info.dart'; @@ -376,6 +362,34 @@ Future setup({ return; } + _registerCoreSingletons(settingsStore); + + final walletList = await WalletInfo.getAll(); + + _registerCoreViewModels(settingsStore); + + _registerAuthAndSecurity(walletList, navigatorKey); + + final appStore = getIt.get(); + + _registerDashboardAndSendPages(appStore); + + _registerWalletListAndAccounts(); + + _registerSettingsAndNodes(walletList); + + _registerExchangeAndSwap(); + + _registerRestoreAndBackup(); + + _registerBuyAndSupport(); + + _registerDevToolsAndBridge(appStore); + + _isSetupFinished = true; +} + +void _registerCoreSingletons(SettingsStore settingsStore) { getIt.registerFactory>(() => _nodeSource); getIt.registerFactory>(() => _powNodeSource, instanceName: Node.boxName + "pow"); @@ -457,10 +471,9 @@ Future setup({ getIt.get(), newWalletArguments: newWalletArgs, )); +} - - final walletList = await WalletInfo.getAll(); - +void _registerCoreViewModels(SettingsStore settingsStore) { getIt.registerFactory( () => WalletManager( getIt.get(), @@ -605,7 +618,9 @@ Future setup({ getIt.registerFactory( () => LightningUsernameBloc(getIt.get().wallet!)); +} +void _registerAuthAndSecurity(List walletList, GlobalKey navigatorKey) { getIt.registerFactory( () => AuthService( secureStorage: getIt.get(), @@ -751,9 +766,9 @@ Future setup({ }); getIt.registerSingleton(BottomSheetServiceImpl()); +} - final appStore = getIt.get(); - +void _registerDashboardAndSendPages(AppStore appStore) { getIt.registerLazySingleton(() => KeyServiceImpl()); getIt.registerLazySingleton(() { @@ -934,7 +949,9 @@ Future setup({ getIt.registerFactory( () => SendTemplatePage(sendTemplateViewModel: getIt.get())); +} +void _registerWalletListAndAccounts() { if (DeviceInfo.instance.isMobile) { getIt.registerFactory( () => WalletListViewModel( @@ -1050,7 +1067,9 @@ Future setup({ (NanoAccount? account, _) => NanoAccountEditOrCreatePage( nanoAccountCreationViewModel: getIt.get(param1: account))); +} +void _registerSettingsAndNodes(List walletList) { getIt.registerFactory(() => DisplaySettingsViewModel(getIt.get(), getIt.get())); @@ -1198,39 +1217,15 @@ Future setup({ nodeCreateOrEditViewModel: getIt.get(param1: {'isPow' : true, 'editingNode': editingNode}), editingNode: editingNode, isSelected: isSelected)); +} - getIt.registerFactory(() => RobinhoodBuyProvider( - wallet: getIt.get().wallet!, - hardwareWalletVM: getIt().wallet!.isHardwareWallet - ? getIt(param1: getIt().wallet!.hardwareWalletType!) - : null, - )); - - getIt.registerFactory(() => DFXBuyProvider( - wallet: getIt().wallet!, - hardwareWalletVM: getIt().wallet!.isHardwareWallet - ? getIt(param1: getIt().wallet!.hardwareWalletType!) - : null, - )); - +void _registerExchangeAndSwap() { getIt.registerFactory(() => MoonPayProvider( appStore: getIt.get(), wallet: getIt.get().wallet!, isTestEnvironment: kDebugMode || kProfileMode, )); - getIt.registerFactory(() => OnRamperBuyProvider( - wallet: getIt.get().wallet!, - )); - - getIt.registerFactory(() => MeldBuyProvider( - wallet: getIt.get().wallet!, - )); - - getIt.registerFactory(() => KryptonimBuyProvider( - wallet: getIt.get().wallet!, - )); - getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); getIt.registerFactory( @@ -1335,7 +1330,9 @@ Future setup({ throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } }); +} +void _registerRestoreAndBackup() { getIt.registerFactoryParam( (isDuressPin, _) => SetupPinCodeViewModel( getIt.get(), @@ -1446,12 +1443,8 @@ Future setup({ getIt.registerFactoryParam( (trade, _) => TradeDetailsViewModel(tradeForDetails: trade, appStore: getIt.get())); - getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); - - getIt.registerFactory(() => CakeFeaturesPage( - dashboardViewModel: getIt.get(), - cakeFeaturesViewModel: getIt.get())); + dashboardViewModel: getIt.get())); getIt.registerFactory(() => BackupServiceV3(getIt.get(), _transactionDescriptionBox, @@ -1478,7 +1471,9 @@ Future setup({ getIt.registerFactoryParam( (Trade trade, _) => TradeDetailsPage(getIt.get(param1: trade))); +} +void _registerBuyAndSupport() { getIt.registerFactory(() => BuyAmountViewModel()); getIt.registerFactory(() => BuySellViewModel(getIt.get())); @@ -1519,12 +1514,10 @@ Future setup({ getIt.registerFactoryParam((order, _) { final wallet = getIt.get().wallet; - final cakePayService = getIt.get(); return OrderDetailsViewModel( wallet: wallet!, orderForDetails: order, - cakePayService: cakePayService, orders: _ordersSource); }); @@ -1578,7 +1571,9 @@ Future setup({ unspentCoinsDetailsViewModel: getIt.get( param1: item, param2: unspentCoinsListViewModel)); }); +} +void _registerDevToolsAndBridge(AppStore appStore) { getIt.registerFactory(() => AddressResolver( wallet: getIt.get().wallet!, settingsStore: getIt.get())); @@ -1586,48 +1581,8 @@ Future setup({ getIt.registerFactoryParam( (QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData)); - getIt.registerFactory(() => CakePayApi()); - getIt.registerFactory(() => AnyPayApi()); - getIt.registerFactory( - () => CakePayService(getIt.get(), getIt.get())); - - getIt.registerFactory( - () => CakePayCardsListViewModel(cakePayService: getIt.get(), - settingsStore: getIt.get())); - - getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get())); - - getIt.registerFactoryParam( - (CakePayVendor vendor, _) { - return CakePayBuyCardViewModel(vendor: vendor, - cakePayService: getIt.get(), - sendViewModel: getIt.get(), - orders: _ordersSource); - }); - - getIt.registerFactory(() => CakePayAccountViewModel(cakePayService: getIt.get())); - - getIt.registerFactory(() => CakePayWelcomePage(getIt.get())); - - getIt.registerFactoryParam, void>((List args, _) { - final email = args.first as String; - final isSignIn = args[1] as bool; - - return CakePayVerifyOtpPage(getIt.get(), email, isSignIn); - }); - - getIt.registerFactoryParam, void>((List args, _) { - final vendor = args.first as CakePayVendor; - - return CakePayBuyCardPage(getIt.get(param1: vendor)); - }); - - getIt.registerFactory(() => CakePayCardsPage(getIt.get())); - - getIt.registerFactory(() => CakePayAccountPage(getIt.get())); - getIt.registerFactory(() => AnonPayApi( useTorOnly: getIt.get().exchangeStatus == ExchangeApiMode.torOnly, wallet: getIt.get().wallet!)); @@ -1776,6 +1731,4 @@ Future setup({ initialToken: initialToken, ), ); - - _isSetupFinished = true; } diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index 274c622aa..ea7b793af 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -1,10 +1,5 @@ import 'package:hash_wallet/buy/buy_provider.dart'; -import 'package:hash_wallet/buy/dfx/dfx_buy_provider.dart'; -import 'package:hash_wallet/buy/kryptonim/kryptonim.dart'; -import 'package:hash_wallet/buy/meld/meld_buy_provider.dart'; import 'package:hash_wallet/buy/moonpay/moonpay_provider.dart'; -import 'package:hash_wallet/buy/onramper/onramper_buy_provider.dart'; -import 'package:hash_wallet/buy/robinhood/robinhood_buy_provider.dart'; import 'package:hash_wallet/di.dart'; import 'package:hash_wallet/.secrets.g.dart' as secrets; @@ -61,18 +56,12 @@ class ProvidersHelper { static BuyProvider getProviderByType(ProviderType type) { switch (type) { - case ProviderType.robinhood: - return getIt.get(); - case ProviderType.dfx: - return getIt.get(); - case ProviderType.onramper: - return getIt.get(); case ProviderType.moonpay: return getIt.get(); - case ProviderType.meld: - return getIt.get(); - case ProviderType.kriptonim: - return getIt.get(); - } + // The remaining provider implementations (Robinhood/DFX/Onramper/Meld/ + // Kryptonim) were removed; their enum values stay for future re-enable. + default: + throw UnsupportedError('Buy provider ${type.title} is not available in this build'); + } } } diff --git a/lib/exchange/provider/chainflip_exchange_provider.dart b/lib/exchange/provider/chainflip_exchange_provider.dart deleted file mode 100644 index ca612848c..000000000 --- a/lib/exchange/provider/chainflip_exchange_provider.dart +++ /dev/null @@ -1,431 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:hash_wallet/utils/exchange_provider_logger.dart'; - -class ChainflipExchangeProvider extends ExchangeProvider { - ChainflipExchangeProvider(); - - static final List _supported = [ - CryptoCurrency.btc, - CryptoCurrency.eth, - CryptoCurrency.usdc, - CryptoCurrency.usdterc20, - CryptoCurrency.flip, - CryptoCurrency.wbtc, - CryptoCurrency.sol, - CryptoCurrency.usdcsol, - CryptoCurrency.usdtSol, - CryptoCurrency.arbEth, - CryptoCurrency.usdcArb, - CryptoCurrency.usdtArb, - ]; - - static const _baseURL = 'chainflip-broker.io'; - static const _assetsPath = '/assets'; - static const _quotePath = '/quotes-native'; - static const _swapPath = '/swap'; - static const _txInfoPath = '/status-by-deposit-channel'; - static const _affiliateBps = secrets.chainflipAffiliateFee; - static const _affiliateKey = secrets.chainflipApiKey; - - @override - String get title => 'Chainflip'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => false; - - @override - ExchangeProviderDescription get description => - ExchangeProviderDescription.chainflip; - - @override - Future checkIsAvailable() async => true; - - @override - Future fetchLimits( - {required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode}) async { - - try { - - if(!_supported.contains(from) || !_supported.contains(to)) { - throw Exception('No rates found for $from to $to'); - } - - final assetId = _normalizeCurrency(from); - - final assetsResponse = await _getAssets(); - final assets = assetsResponse['assets'] as List; - - final minAmount = assets.firstWhere( - (asset) => asset['id'] == assetId, - orElse: () => null)?['minimalAmountNative'] ?? '0'; - - if (minAmount == '0') throw Exception('No rates found for $from to $to'); - - return Limits(min: _amountFromNative(minAmount.toString(), from)); - } catch (e) { - printV(e.toString()); - throw Exception('Chainflip failed to fetch limits'); - } - } - - @override - Future fetchRate({ - required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount - }) async { - // TODO: It seems this rate is getting cached, and re-used for different amounts, can we not do this? - - try { - if (amount == 0) return 0.0; - - if(!_supported.contains(from) || !_supported.contains(to)) return 0.0; - - - final quoteParams = { - 'apiKey': _affiliateKey, - 'sourceAsset': _normalizeCurrency(from), - 'destinationAsset': _normalizeCurrency(to), - 'amount': _amountToNative(amount, from), - 'commissionBps': _affiliateBps - }; - - final quoteResponse = await _getSwapQuote(quoteParams); - - final expectedAmountOut = - quoteResponse['egressAmountNative'] as String? ?? '0'; - - final rate = _amountFromNative(expectedAmountOut, to) / amount; - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'fetchRate', - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'quoteParams': quoteParams, - }, - responseData: { - 'expectedAmountOut': expectedAmountOut, - 'rate': rate, - 'quoteResponse': quoteResponse, - }, - ); - - return rate; - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: e, - stackTrace: s, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - printV(e.toString()); - return 0.0; - } - } - - @override - Future createTrade( - {required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll}) async { - try { - final maxSlippage = 2; - - final quoteParams = { - 'apiKey': _affiliateKey, - 'sourceAsset': _normalizeCurrency(request.fromCurrency), - 'destinationAsset': _normalizeCurrency(request.toCurrency), - 'amount': _amountToNative(double.parse(request.fromAmount), request.fromCurrency), - 'commissionBps': _affiliateBps - }; - - final quoteResponse = await _getSwapQuote(quoteParams); - final estimatedPrice = quoteResponse['estimatedPrice'] as double; - final minimumPrice = estimatedPrice * (100 - maxSlippage) / 100; - - final swapParams = { - 'apiKey': _affiliateKey, - 'sourceAsset': _normalizeCurrency(request.fromCurrency), - 'destinationAsset': _normalizeCurrency(request.toCurrency), - 'destinationAddress': request.toAddress, - 'commissionBps': _affiliateBps, - 'minimumPrice': minimumPrice.toString(), - 'refundAddress': request.refundAddress, - 'boostFee': '6', - 'retryDurationInBlocks': '150' - }; - - if (quoteResponse.containsKey('numberOfChunks') && quoteResponse.containsKey('chunkIntervalBlocks')) { - swapParams.addAll({ - 'numberOfChunks': quoteResponse['numberOfChunks'].toString(), - 'chunkIntervalBlocks': quoteResponse['chunkIntervalBlocks'].toString(), - }); - } - - final swapResponse = await _openDepositChannel(swapParams); - - final id = '${swapResponse['issuedBlock']}-${swapResponse['network'].toString()}-${swapResponse['channelId']}'; - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'createTrade', - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'quoteParams': quoteParams, - 'swapParams': swapParams, - }, - responseData: { - 'id': id, - 'inputAddress': swapResponse['address'].toString(), - 'estimatedPrice': estimatedPrice, - 'minimumPrice': minimumPrice, - 'swapResponse': swapResponse, - }, - ); - - return Trade( - id: id, - from: request.fromCurrency, - to: request.toCurrency, - provider: description, - inputAddress: swapResponse['address'].toString(), - createdAt: DateTime.now(), - amount: request.fromAmount, - receiveAmount: request.toAmount, - state: TradeState.waiting, - payoutAddress: request.toAddress, - isSendAll: isSendAll, - ); - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: e, - stackTrace: s, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - }, - ); - printV(e.toString()); - rethrow; - } - } - - @override - Future findTradeById({required String id}) async { - try { - final channelParts = id.split('-'); - final network = channelParts[1]; - final normalizedNetwork = _normalizeNetworkName(network); - - final statusParams = { - 'apiKey': _affiliateKey, - 'issuedBlock': channelParts[0], - 'network': normalizedNetwork, - 'channelId': channelParts[2] - }; - - final statusResponse = await _getStatus(statusParams); - - if (statusResponse == null) - throw Exception('Trade not found for id: $id'); - - final status = statusResponse['status']; - final currentState = _determineState(status['state'].toString()); - - final depositAmount = status['deposit']?['amount']?.toString() ?? '0.0'; - final receiveAmount = status['swapEgress']?['amount']?.toString() ?? '0.0'; - final refundAmount = status['refundEgress']?['amount']?.toString() ?? '0.0'; - final isRefund = status['refundEgress'] != null; - final amount = isRefund ? refundAmount : receiveAmount; - - final from = status['sourceAsset'].toString(); - final to = status['destinationAsset'].toString(); - - final newTrade = Trade( - id: id, - from: _toCurrency(from), - to: _toCurrency(to), - provider: description, - amount: depositAmount, - receiveAmount: amount, - state: currentState, - payoutAddress: status['destinationAddress'].toString(), - outputTransaction: - status['swapEgress']?['transactionReference']?.toString(), - isRefund: isRefund, - ); - - return newTrade; - } catch (e) { - printV(e.toString()); - rethrow; - } - } - - String _normalizeCurrency(CryptoCurrency currency) { - final tag = currency.tag?.toLowerCase(); - final title = currency.title.toLowerCase(); - - // Naive assets without network tag - if (tag == null) return '$title.$title'; - - return '$title.$tag'; - } - - String _normalizeNetworkName (String name) { - final networkName = switch (name) { - 'BITCOIN' => 'Bitcoin', - 'ETHEREUM' => 'Ethereum', - 'ARBITRUM' => 'Arbitrum', - 'SOLANA' => 'Solana', - _ => name - }; - - return networkName; - } - - - CryptoCurrency? _toCurrency(String name) { - final currency = switch (name) { - 'btc.btc' => CryptoCurrency.btc, - 'eth.eth' => CryptoCurrency.eth, - 'usdc.eth' => CryptoCurrency.usdc, - 'usdt.eth' => CryptoCurrency.usdterc20, - 'flip.eth' => CryptoCurrency.flip, - 'wbtc.eth' => CryptoCurrency.wbtc, - 'sol.sol' => CryptoCurrency.sol, - 'usdc.sol' => CryptoCurrency.usdcsol, - 'eth.arb' => CryptoCurrency.arbEth, - 'usdc.arb' => CryptoCurrency.usdcArb, - 'usdt.arb' => CryptoCurrency.usdtArb, - _ => null - }; - - return currency; - } - - String _amountToNative(double amount, CryptoCurrency currency) => - (amount * pow(10, currency.decimals)).toInt().toString(); - - double _amountFromNative(String amount, CryptoCurrency currency) => - double.parse(amount) / pow(10, currency.decimals); - - Future> _getAssets() async => - _getRequest(_assetsPath, {}); - - Future> _openDepositChannel(Map params) async => - _getRequest(_swapPath, params); - - Future> _getRequest(String path, Map params) async { - final uri = Uri.https(_baseURL, path, params); - - final response = await ProxyWrapper().get(clearnetUri: uri); - - if ((response.statusCode != 200) || (response.body.contains('error'))) { - throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}'); - } - - return json.decode(response.body) as Map; - } - - Future> _getSwapQuote(Map params) async { - final uri = Uri.https(_baseURL, _quotePath, params); - - final response = await ProxyWrapper().get(clearnetUri: uri); - - if ((response.statusCode != 200) || (response.body.contains('error'))) { - throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}'); - } - - final List jsonList = json.decode(response.body) as List; - final List> quotes = - jsonList.map((e) => e as Map).toList(); - - Map highestQuote = quotes.reduce((current, next) { - double currentAmount = current['egressAmount'] as double; - double nextAmount = next['egressAmount'] as double; - - return currentAmount > nextAmount ? current : next; - }); - - return highestQuote; - } - - Future?> _getStatus(Map params) async { - final uri = Uri.https(_baseURL, _txInfoPath, params); - - final response = await ProxyWrapper().get(clearnetUri: uri); - - if (response.statusCode == 404) return null; - - if ((response.statusCode != 200) || (response.body.contains('error'))) { - throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}'); - } - - return json.decode(response.body) as Map; - } - - TradeState _determineState(String state) { - final swapState = switch (state) { - 'waiting' => TradeState.waiting, - 'receiving' => TradeState.processing, - 'swapping' => TradeState.processing, - 'sending' => TradeState.processing, - 'sent' => TradeState.processing, - 'completed' => TradeState.success, - 'failed' => TradeState.failed, - _ => TradeState.notFound - }; - - return swapState; - } -} diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart deleted file mode 100644 index 8e3898908..000000000 --- a/lib/exchange/provider/changenow_exchange_provider.dart +++ /dev/null @@ -1,366 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_not_found_exception.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:hash_wallet/store/settings_store.dart'; -import 'package:hash_wallet/utils/distribution_info.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:hash_wallet/wallet_type_utils.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:hash_wallet/utils/exchange_provider_logger.dart'; - -class ChangeNowExchangeProvider extends ExchangeProvider { - ChangeNowExchangeProvider({required SettingsStore settingsStore}) - : _settingsStore = settingsStore, - _lastUsedRateId = ''; - - static final apiKey = - isMoneroOnly ? secrets.changeNowMoneroApiKey : secrets.changeNowCakeWalletApiKey; - static const apiAuthority = 'api.changenow.io'; - static const createTradePath = '/v2/exchange'; - static const findTradeByIdPath = '/v2/exchange/by-id'; - static const estimatedAmountPath = '/v2/exchange/estimated-amount'; - static const rangePath = '/v2/exchange/range'; - static const apiHeaderKey = 'x-changenow-api-key'; - - final SettingsStore _settingsStore; - String _lastUsedRateId; - - @override - String get title => 'ChangeNOW'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => true; - - @override - ExchangeProviderDescription get description => ExchangeProviderDescription.changeNow; - - @override - Future checkIsAvailable() async => true; - - @override - Future fetchLimits( - {required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode}) async { - final headers = {apiHeaderKey: apiKey}; - final params = { - 'fromCurrency': _normalizeCurrency(from), - 'toCurrency': _normalizeCurrency(to), - 'fromNetwork': _networkFor(from), - 'toNetwork': _networkFor(to), - 'flow': _getFlow(isFixedRateMode) - }; - final uri = Uri.https(apiAuthority, rangePath, params); - final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); - - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error'] as String; - final message = responseJSON['message'] as String; - throw Exception('${error}\n$message'); - } - - if (response.statusCode != 200) - throw Exception('Unexpected http status: ${response.statusCode}'); - - final responseJSON = json.decode(response.body) as Map; - final min = double.tryParse(responseJSON['minAmount']?.toString() ?? ''); - final max = double.tryParse(responseJSON['maxAmount']?.toString() ?? ''); - if (max == 0) return null; - return Limits(min: min, max: max); - } - - @override - Future fetchRate( - {required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount}) async { - try { - if (amount == 0) return 0.0; - - final headers = {apiHeaderKey: apiKey}; - final isReverse = isReceiveAmount; - final type = isReverse ? 'reverse' : 'direct'; - final params = { - 'fromCurrency': _normalizeCurrency(from), - 'toCurrency': _normalizeCurrency(to), - 'fromNetwork': _networkFor(from), - 'toNetwork': _networkFor(to), - 'type': type, - 'flow': _getFlow(isFixedRateMode) - }; - - if (isReverse) - params['toAmount'] = amount.toString(); - else - params['fromAmount'] = amount.toString(); - - final uri = Uri.https(apiAuthority, estimatedAmountPath, params); - final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); - - final responseJSON = json.decode(response.body) as Map; - final fromAmount = double.tryParse(responseJSON['fromAmount']?.toString() ?? '') ?? 0.0; - final toAmount = double.tryParse(responseJSON['toAmount']?.toString() ?? '') ?? 0.0; - if (fromAmount <= 0 || toAmount <= 0) return 0.0; - final rateId = responseJSON['rateId'] as String? ?? ''; - - if (rateId.isNotEmpty) _lastUsedRateId = rateId; - - final rate = isReverse ? (amount / fromAmount) : (toAmount / amount); - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'fetchRate', - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'type': type, - 'flow': _getFlow(isFixedRateMode), - }, - responseData: { - 'fromAmount': fromAmount, - 'toAmount': toAmount, - 'rateId': rateId, - 'rate': rate, - }, - ); - - return rate; - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: e, - stackTrace: s, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - printV(e.toString()); - return 0.0; - } - } - - @override - Future createTrade({ - required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll, - }) async { - final distributionPath = await DistributionInfo.instance.getDistributionPath(); - final formattedAppVersion = int.tryParse(_settingsStore.appVersion.replaceAll('.', '')) ?? 0; - final payload = { - 'app': isMoneroOnly ? 'monerocom' : 'cakewallet', - 'device': Platform.operatingSystem, - 'distribution': distributionPath, - 'version': formattedAppVersion - }; - final headers = {apiHeaderKey: apiKey, 'Content-Type': 'application/json'}; - final type = isFixedRateMode ? 'reverse' : 'direct'; - final body = { - 'fromCurrency': _normalizeCurrency(request.fromCurrency), - 'toCurrency': _normalizeCurrency(request.toCurrency), - 'fromNetwork': _networkFor(request.fromCurrency), - 'toNetwork': _networkFor(request.toCurrency), - if (!isFixedRateMode) 'fromAmount': request.fromAmount, - if (isFixedRateMode) 'toAmount': request.toAmount, - 'address': request.toAddress, - 'flow': _getFlow(isFixedRateMode), - 'type': type, - 'refundAddress': request.refundAddress, - 'payload': payload, - }; - - if (isFixedRateMode) { - // since we schedule to calculate the rate every 5 seconds we need to ensure that - // we have the latest rate id with the given inputs before creating the trade - await fetchRate( - from: request.fromCurrency, - to: request.toCurrency, - amount: double.tryParse(request.toAmount) ?? 0, - isFixedRateMode: true, - isReceiveAmount: true, - ); - body['rateId'] = _lastUsedRateId; - } - - final uri = Uri.https(apiAuthority, createTradePath); - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode(body), - ); - - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error'] as String; - final message = responseJSON['message'] as String; - throw Exception('${error}\n$message'); - } - - if (response.statusCode != 200) - throw Exception('Unexpected http status: ${response.statusCode}'); - - final responseJSON = json.decode(response.body) as Map; - final id = responseJSON['id'] as String; - final inputAddress = responseJSON['payinAddress'] as String; - final refundAddress = responseJSON['refundAddress'] as String; - final extraId = responseJSON['payinExtraId'] as String?; - final payoutAddress = responseJSON['payoutAddress'] as String; - final fromAmount = responseJSON['fromAmount']?.toString(); - final toAmount = responseJSON['toAmount']?.toString(); - - return Trade( - id: id, - from: request.fromCurrency, - to: request.toCurrency, - provider: description, - inputAddress: inputAddress, - refundAddress: refundAddress, - extraId: extraId, - createdAt: DateTime.now(), - amount: fromAmount ?? request.fromAmount, - receiveAmount: toAmount ?? request.toAmount, - state: TradeState.created, - payoutAddress: payoutAddress, - isSendAll: isSendAll, - ); - } - - @override - Future findTradeById({required String id}) async { - final headers = {apiHeaderKey: apiKey}; - final params = {'id': id}; - final uri = Uri.https(apiAuthority, findTradeByIdPath, params); - final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); - - if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description); - - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['message'] as String; - - throw TradeNotFoundException(id, provider: description, description: error); - } - - if (response.statusCode != 200) - throw Exception('Unexpected http status: ${response.statusCode}'); - - final responseJSON = json.decode(response.body) as Map; - - // Parsing 'from' currency - final fromCurrency = responseJSON['fromCurrency'] as String; - final fromNetwork = responseJSON['fromNetwork'] as String?; - final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? ''); - final fromTag = fromCurrency.toUpperCase() == _normalizedFromNetwork.toUpperCase() - ? null - : _normalizedFromNetwork; - final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag); - - // Parsing 'to' currency - final toCurrency = responseJSON['toCurrency'] as String; - final toNetwork = responseJSON['toNetwork'] as String?; - final _normalizedToNetwork = _normalizeNetworkType(toNetwork ?? ''); - final toTag = toCurrency.toUpperCase() == _normalizedToNetwork.toUpperCase() - ? null - : _normalizedToNetwork; - final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag); - - final inputAddress = responseJSON['payinAddress'] as String; - final expectedSendAmount = responseJSON['expectedAmountFrom'].toString(); - final status = responseJSON['status'] as String; - final state = TradeState.deserialize(raw: status); - final extraId = responseJSON['payinExtraId'] as String?; - final outputTransaction = responseJSON['payoutHash'] as String?; - final expiredAtRaw = responseJSON['validUntil'] as String?; - final payoutAddress = responseJSON['payoutAddress'] as String; - final expiredAt = DateTime.tryParse(expiredAtRaw ?? '')?.toLocal(); - - return Trade( - id: id, - from: from, - to: to, - provider: description, - inputAddress: inputAddress, - amount: expectedSendAmount, - state: state, - extraId: extraId, - expiredAt: expiredAt, - outputTransaction: outputTransaction, - payoutAddress: payoutAddress, - ); - } - - String _getFlow(bool isFixedRate) => isFixedRate ? 'fixed-rate' : 'standard'; - - String _networkFor(CryptoCurrency currency) { - switch (currency) { - case CryptoCurrency.usdt: - return 'btc'; - case CryptoCurrency.arb: - return 'arbitrum'; - default: - return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title.toLowerCase(); - } - } - - String _normalizeCurrency(CryptoCurrency currency) { - if (currency.title == "USDC" && currency.tag == "POLY") { - throw "Only Bridged USDC (USDC.e) is allowed in ChangeNow"; - } - switch (currency) { - case CryptoCurrency.zec: - return 'zec'; - default: - return currency.title.toLowerCase(); - } - } - - String _normalizeTag(String tag) { - switch (tag) { - case 'POLY': - return 'matic'; - case 'LN': - return 'lightning'; - case 'AVAXC': - return 'cchain'; - case 'ARB': - return 'arbitrum'; - default: - return tag.toLowerCase(); - } - } - - String _normalizeNetworkType(String network) { - return switch (network.toUpperCase()) { - 'POLY' => 'MATIC', - 'AVAXC' => 'CCHAIN', - 'ARBITRUM' => 'ARB', - _ => network, - }; - } -} diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart deleted file mode 100644 index 4146b3f42..000000000 --- a/lib/exchange/provider/exolix_exchange_provider.dart +++ /dev/null @@ -1,467 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/core/lightning_invoice_service.dart'; -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_not_found_exception.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:hash_wallet/wallet_type_utils.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:hash_wallet/utils/exchange_provider_logger.dart'; - -class ExolixExchangeProvider extends ExchangeProvider { - ExolixExchangeProvider(); - - static final apiKey = isMoneroOnly ? secrets.exolixMoneroApiKey : secrets.exolixCakeWalletApiKey; - static const apiBaseUrl = 'exolix.com'; - static const transactionsPath = '/api/v2/transactions'; - static const ratePath = '/api/v2/rate'; - - @override - String get title => 'Exolix'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => true; - - @override - ExchangeProviderDescription get description => ExchangeProviderDescription.exolix; - - @override - Future checkIsAvailable() async => true; - - @override - Future fetchLimits({ - required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode, - }) async { - final params = { - 'rateType': _getRateType(isFixedRateMode), - 'amount': '1', - 'apiToken': apiKey, - }; - - if (isFixedRateMode) { - params['coinFrom'] = _normalizeCurrency(to); - params['coinTo'] = _normalizeCurrency(_overrideFromCryptoCurrency(from)); - params['networkFrom'] = _networkFor(to); - params['networkTo'] = _networkFor(from); - } else { - params['coinFrom'] = _normalizeCurrency(_overrideFromCryptoCurrency(from)); - params['coinTo'] = _normalizeCurrency(to); - params['networkFrom'] = _networkFor(from); - params['networkTo'] = _networkFor(to); - } - - // Maximum of 2 attempts to fetch limits - for (int i = 0; i < 2; i++) { - final uri = Uri.https(apiBaseUrl, ratePath, params); - final response = await ProxyWrapper().get(clearnetUri: uri); - - - if (response.statusCode == 200) { - final responseJSON = json.decode(response.body) as Map; - final minAmount = responseJSON['minAmount']; - final maxAmount = responseJSON['maxAmount']; - return Limits(min: _toDouble(minAmount), max: _toDouble(maxAmount)); - } else if (response.statusCode == 422) { - final errorResponse = json.decode(response.body) as Map; - if (errorResponse.containsKey('minAmount')) { - params['amount'] = errorResponse['minAmount'].toString(); - continue; - } - throw Exception('Error 422: ${errorResponse['message'] ?? 'Unknown error'}'); - } else { - throw Exception('Unexpected HTTP status: ${response.statusCode}'); - } - } - - throw Exception('Failed to fetch limits after retrying.'); - } - - @override - Future fetchRate({ - required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount - }) async { - try { - if (amount == 0) return 0.0; - - final params = { - 'coinFrom': _normalizeCurrency(_overrideFromCryptoCurrency(from)), - 'coinTo': _normalizeCurrency(to), - 'networkFrom': _networkFor(from), - 'networkTo': _networkFor(to), - 'rateType': _getRateType(isFixedRateMode), - 'apiToken': apiKey, - }; - - if (isReceiveAmount) - params['withdrawalAmount'] = amount.toString(); - else - params['amount'] = amount.toString(); - - final uri = Uri.https(apiBaseUrl, ratePath, params); - final response = await ProxyWrapper().get(clearnetUri: uri); - - final responseJSON = json.decode(response.body) as Map; - - if (response.statusCode != 200) { - final message = responseJSON['message'] as String?; - - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: Exception(message ?? 'Unknown error'), - stackTrace: StackTrace.current, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'params': params, - 'url': uri.toString(), - }, - ); - - throw Exception(message); - } - - final rate = double.tryParse(responseJSON['rate']?.toString() ?? '') ?? 0.0; - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'fetchRate', - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'params': params, - 'url': uri.toString(), - }, - responseData: { - 'rate': rate, - 'statusCode': response.statusCode, - 'responseJSON': responseJSON, - }, - ); - - return rate; - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: e, - stackTrace: s, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - printV(e.toString()); - printV(s.toString()); - return 0.0; - } - } - - @override - Future createTrade({ - required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll, - }) async { - final headers = {'Content-Type': 'application/json'}; - final body = { - 'coinFrom': _normalizeCurrency(_overrideFromCryptoCurrency(request.fromCurrency)), - 'coinTo': _normalizeCurrency(_overrideToCryptoCurrency(request.toCurrency, request.toAddress)), - 'networkFrom': _networkFor(request.fromCurrency), - 'networkTo': _networkFor(request.toCurrency), - 'withdrawalAddress': await _normalizeAddress(request.toAddress), - 'refundAddress': await _normalizeAddress(request.refundAddress), - 'rateType': _getRateType(isFixedRateMode), - 'apiToken': apiKey, - }; - - if (isFixedRateMode) - body['withdrawalAmount'] = request.toAmount; - else - body['amount'] = request.fromAmount; - - final uri = Uri.https(apiBaseUrl, transactionsPath); - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode(body), - ); - - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final errors = responseJSON['error'] as Map; - final errorMessage = errors.values.join(', '); - - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: Exception(errorMessage), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'url': uri.toString(), - }, - ); - - throw Exception(errorMessage); - } - - if (response.statusCode != 200 && response.statusCode != 201) { - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: Exception('Unexpected http status: ${response.statusCode}'), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'url': uri.toString(), - }, - ); - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final responseJSON = json.decode(response.body) as Map; - final id = responseJSON['id'] as String; - final inputAddress = responseJSON['depositAddress'] as String; - final refundAddress = responseJSON['refundAddress'] as String?; - final extraId = responseJSON['depositExtraId'] as String?; - final payoutAddress = responseJSON['withdrawalAddress'] as String; - final amount = responseJSON['amount'].toString(); - final receiveAmount = responseJSON['amountTo']?.toString(); - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'createTrade', - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'url': uri.toString(), - }, - responseData: { - 'id': id, - 'inputAddress': inputAddress, - 'refundAddress': refundAddress, - 'extraId': extraId, - 'payoutAddress': payoutAddress, - 'amount': amount, - 'receiveAmount': receiveAmount, - 'statusCode': response.statusCode, - 'responseJSON': responseJSON, - }, - ); - - return Trade( - id: id, - from: request.fromCurrency, - to: request.toCurrency, - provider: description, - inputAddress: inputAddress, - refundAddress: refundAddress, - extraId: extraId, - createdAt: DateTime.now(), - amount: amount, - receiveAmount: receiveAmount ?? request.toAmount, - state: TradeState.created, - payoutAddress: payoutAddress, - isSendAll: isSendAll, - ); - } - - @override - Future findTradeById({required String id}) async { - final findTradeByIdPath = '$transactionsPath/$id'; - final uri = Uri.https(apiBaseUrl, findTradeByIdPath); - final response = await ProxyWrapper().get(clearnetUri: uri); - - if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description); - - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final errors = responseJSON['errors'] as Map; - final errorMessage = errors.values.join(', '); - - throw TradeNotFoundException(id, provider: description, description: errorMessage); - } - - if (response.statusCode != 200) - throw Exception('Unexpected http status: ${response.statusCode}'); - - final responseJSON = json.decode(response.body) as Map; - - // Parsing 'from' currency - final coinFrom = responseJSON['coinFrom']['coinCode'] as String; - final coinFromNetwork = responseJSON['coinFrom']['network'] as String?; - final _normalizedFromNetwork = _normalizeNetworkType(coinFromNetwork ?? ''); - final fromTag = coinFrom.toUpperCase() == _normalizedFromNetwork.toUpperCase() ? null : coinFromNetwork; - final from = CryptoCurrency.safeParseCurrencyFromString(coinFrom, tag: fromTag); - - // Parsing 'to' currency - final coinTo = responseJSON['coinTo']['coinCode'] as String; - final coinToNetwork = responseJSON['coinTo']['network'] as String?; - final _normalizedToNetwork = _normalizeNetworkType(coinToNetwork ?? ''); - final toTag = coinTo.toUpperCase() == _normalizedToNetwork.toUpperCase() ? null : coinToNetwork; - final to = CryptoCurrency.safeParseCurrencyFromString(coinTo, tag: toTag); - - final inputAddress = responseJSON['depositAddress'] as String; - final amount = responseJSON['amount'].toString(); - final status = responseJSON['status'] as String; - final extraId = responseJSON['depositExtraId'] as String?; - final outputTransaction = responseJSON['hashOut']['hash'] as String?; - final payoutAddress = responseJSON['withdrawalAddress'] as String; - - return Trade( - id: id, - from: from, - to: to, - provider: description, - inputAddress: inputAddress, - amount: amount, - state: TradeState.deserialize(raw: _prepareStatus(status)), - extraId: extraId, - outputTransaction: outputTransaction, - payoutAddress: payoutAddress, - ); - } - - String _getRateType(bool isFixedRate) => isFixedRate ? 'fixed' : 'float'; - - String _prepareStatus(String status) { - switch (status) { - case 'deleted': - case 'error': - return 'overdue'; - default: - return status; - } - } - - String _networkFor(CryptoCurrency currency) { - switch (currency) { - case CryptoCurrency.arb: - return 'ARBITRUM'; - case CryptoCurrency.btcln: - return 'LIGHTNING'; - default: - return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title; - } - } - - String _normalizeNetworkType(String network) { - return switch (network.toUpperCase()) { - 'ARBITRUM' => 'ARB', - _ => network, - }; - } - - CryptoCurrency _overrideFromCryptoCurrency(CryptoCurrency currency) { - if (currency == CryptoCurrency.zec) - return CryptoCurrency.zaddr; // Sending is always shielded zcash - return currency; - } - - CryptoCurrency _overrideToCryptoCurrency(CryptoCurrency currency, String address) { - if (RegExp(r'u1[a-zA-Z0-9]{100,300}').hasMatch(address) && currency == CryptoCurrency.zec) - return CryptoCurrency.zaddr; // If the user pastes a unified address use shielded zcash - return currency; - } - - String _normalizeCurrency(CryptoCurrency currency) { - switch (currency) { - case CryptoCurrency.nano: - return 'XNO'; - case CryptoCurrency.bttc: - return 'BTT'; - case CryptoCurrency.zec: - return 'ZEC'; - case CryptoCurrency.zaddr: - return 'ZEC-SHIELDED'; - default: - return currency.title; - } - } - - String _normalizeTag(String tag) { - switch (tag) { - case 'POLY': - return 'Polygon'; - case 'ARB': - return 'Arbitrum'; - default: - return tag; - } - } - - Future _normalizeAddress(String address) async { - if (address.startsWith('bitcoincash:')) - return address.replaceFirst('bitcoincash:', ''); - - // Lightning addresses - if(address.contains("@")) - return await getBolt11FromLightingAddress(address) ?? address; - - return address; - } - - - static double? _toDouble(dynamic value) { - if (value is int) { - return value.toDouble(); - } else if (value is double) { - return value; - } else if (value is String) { - return double.tryParse(value); - } - return null; - } -} diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart deleted file mode 100644 index 8668ba074..000000000 --- a/lib/exchange/provider/letsexchange_exchange_provider.dart +++ /dev/null @@ -1,447 +0,0 @@ -import 'dart:convert'; -import 'dart:developer'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_not_created_exception.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:hash_wallet/utils/exchange_provider_logger.dart'; - -class LetsExchangeExchangeProvider extends ExchangeProvider { - LetsExchangeExchangeProvider(); - - static const apiKey = secrets.letsExchangeBearerToken; - static const _baseUrl = 'api.letsexchange.io'; - static const _infoPath = '/api/v1/info'; - static const _infoRevertPath = '/api/v1/info-revert'; - static const _createTransactionPath = '/api/v1/transaction'; - static const _createTransactionRevertPath = '/api/v1/transaction-revert'; - static const _getTransactionPath = '/api/v1/transaction'; - - static const _affiliateId = secrets.letsExchangeAffiliateId; - - @override - String get title => 'LetsExchange'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => true; - - @override - ExchangeProviderDescription get description => ExchangeProviderDescription.letsExchange; - - @override - Future checkIsAvailable() async => true; - - @override - Future fetchLimits( - {required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode}) async { - final networkFrom = _getNetworkType(from); - final networkTo = _getNetworkType(to); - - try { - final params = { - 'from': from.title, - 'to': to.title, - if (networkFrom != null) 'network_from': networkFrom, - if (networkTo != null) 'network_to': networkTo, - 'amount': '1', - 'affiliate_id': _affiliateId, - 'float': isFixedRateMode ? 'false' : 'true', - }; - - final responseJSON = await _getInfo(params, isFixedRateMode); - final min = double.tryParse(responseJSON['min_amount'] as String); - final max = double.tryParse(responseJSON['max_amount'] as String); - return Limits(min: min, max: max); - } catch (e) { - log(e.toString()); - throw Exception('Failed to fetch limits'); - } - } - - @override - Future fetchRate( - {required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount}) async { - final networkFrom = _getNetworkType(from); - final networkTo = _getNetworkType(to); - try { - final params = { - 'from': from.title, - 'to': to.title, - if (networkFrom != null) 'network_from': networkFrom, - if (networkTo != null) 'network_to': networkTo, - 'amount': amount.toString(), - 'affiliate_id': _affiliateId, - 'float': isFixedRateMode ? 'false' : 'true', - }; - - final responseJSON = await _getInfo(params, isFixedRateMode); - - final amountToGet = double.tryParse(responseJSON['amount'] as String) ?? 0.0; - - if (amountToGet == 0.0) return 0.0; - - final rate = isFixedRateMode ? amount / amountToGet : amountToGet / amount; - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'fetchRate', - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'networkFrom': networkFrom, - 'networkTo': networkTo, - 'params': params, - }, - responseData: { - 'amountToGet': amountToGet, - 'rate': rate, - 'responseJSON': responseJSON, - }, - ); - - return rate; - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: e, - stackTrace: s, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'networkFrom': networkFrom, - 'networkTo': networkTo, - }, - ); - printV(e.toString()); - return 0.0; - } - } - - @override - Future createTrade( - {required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll}) async { - final networkFrom = _getNetworkType(request.fromCurrency); - final networkTo = _getNetworkType(request.toCurrency); - try { - final params = { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - if (networkFrom != null) 'network_from': networkFrom, - if (networkTo != null) 'network_to': networkTo, - 'amount': isFixedRateMode ? request.toAmount.toString() : request.fromAmount.toString(), - 'affiliate_id': _affiliateId, - 'float': isFixedRateMode ? 'false' : 'true', - }; - - final responseInfoJSON = await _getInfo(params, isFixedRateMode); - final rateId = responseInfoJSON['rate_id'] as String; - - final withdrawalAddress = _normalizeBchAddress(request.toAddress); - final returnAddress = _normalizeBchAddress(request.refundAddress); - - final tradeParams = { - 'coin_from': request.fromCurrency.title, - 'coin_to': request.toCurrency.title, - if (!isFixedRateMode) 'deposit_amount': request.fromAmount.toString(), - 'withdrawal': withdrawalAddress, - if (isFixedRateMode) 'withdrawal_amount': request.toAmount.toString(), - 'withdrawal_extra_id': '', - 'return': returnAddress, - 'rate_id': rateId, - if (networkFrom != null) 'network_from': networkFrom, - if (networkTo != null) 'network_to': networkTo, - 'affiliate_id': _affiliateId, - 'float': isFixedRateMode ? 'false' : 'true', - }; - - final headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': apiKey - }; - - final uri = Uri.https( - _baseUrl, isFixedRateMode ? _createTransactionRevertPath : _createTransactionPath); - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode(tradeParams), - ); - - if (response.statusCode != 200) { - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: Exception('LetsExchange create trade failed: ${response.body}'), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'networkFrom': networkFrom, - 'networkTo': networkTo, - 'tradeParams': tradeParams, - 'url': uri.toString(), - }, - ); - throw Exception('LetsExchange create trade failed: ${response.body}'); - } - final responseJSON = json.decode(response.body) as Map; - final id = responseJSON['transaction_id'] as String; - final from = responseJSON['coin_from'] as String; - final to = responseJSON['coin_to'] as String; - final payoutAddress = responseJSON['withdrawal'] as String; - final depositAddress = responseJSON['deposit'] as String; - final refundAddress = responseJSON['return'] as String; - final depositAmount = responseJSON['deposit_amount'] as String; - final receiveAmount = responseJSON['withdrawal_amount'] as String; - final status = responseJSON['status'] as String; - - // We ignore the created_at from response and use DateTime.now() instead - final createdAtString = responseJSON['created_at'] as String; - final expiredAtTimestamp = responseJSON['expired_at'] as int; - final extraId = responseJSON['deposit_extra_id'] as String?; - - final createdAt = DateTime.now(); - final expiredAt = createdAt.add(Duration(minutes: 30)); - - CryptoCurrency fromCurrency; - if (request.fromCurrency.tag != null && request.fromCurrency.title == from) { - fromCurrency = request.fromCurrency; - } else { - fromCurrency = CryptoCurrency.fromString(from); - } - - CryptoCurrency toCurrency; - if (request.toCurrency.tag != null && request.toCurrency.title == to) { - toCurrency = request.toCurrency; - } else { - toCurrency = CryptoCurrency.fromString(to); - } - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'createTrade', - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'networkFrom': networkFrom, - 'networkTo': networkTo, - 'tradeParams': tradeParams, - 'url': uri.toString(), - }, - responseData: { - 'id': id, - 'from': from, - 'to': to, - 'depositAddress': depositAddress, - 'payoutAddress': payoutAddress, - 'refundAddress': refundAddress, - 'depositAmount': depositAmount, - 'receiveAmount': receiveAmount, - 'status': status, - 'createdAt': createdAtString, - 'expiredAt': expiredAtTimestamp, - 'extraId': extraId, - 'statusCode': response.statusCode, - }, - ); - - return Trade( - id: id, - from: fromCurrency, - to: toCurrency, - provider: description, - inputAddress: depositAddress, - payoutAddress: payoutAddress, - refundAddress: refundAddress, - amount: depositAmount, - receiveAmount: receiveAmount, - state: TradeState.deserialize(raw: status), - createdAt: createdAt, - expiredAt: expiredAt, - extraId: extraId, - isSendAll: isSendAll, - ); - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: e, - stackTrace: s, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'networkFrom': networkFrom, - 'networkTo': networkTo, - }, - ); - log(e.toString()); - throw TradeNotCreatedException(description); - } - } - - @override - Future findTradeById({required String id}) async { - final headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': apiKey - }; - - final url = Uri.https(_baseUrl, '$_getTransactionPath/$id'); - final response = await ProxyWrapper().get(clearnetUri: url, headers: headers); - - if (response.statusCode != 200) { - throw Exception('LetsExchange fetch trade failed: ${response.body}'); - } - final responseJSON = json.decode(response.body) as Map; - - // Parsing 'from' currency - final fromCurrency = responseJSON['coin_from'] as String; - final fromNetwork = responseJSON['coin_from_network'] as String?; - final normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? ''); - final fromTag = fromCurrency == normalizedFromNetwork ? null : normalizedFromNetwork; - final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag); - - // Parsing 'to' currency - final toCurrency = responseJSON['coin_to'] as String; - final toNetwork = responseJSON['coin_to_network'] as String?; - final normalizedToNetwork = _normalizeNetworkType(toNetwork ?? ''); - final toTag = toCurrency == normalizedToNetwork ? null : normalizedToNetwork; - final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag); - - final payoutAddress = responseJSON['withdrawal'] as String; - final depositAddress = responseJSON['deposit'] as String; - final refundAddress = responseJSON['return'] as String; - final depositAmount = responseJSON['deposit_amount'] as String; - final receiveAmount = responseJSON['withdrawal_amount'] as String; - final status = responseJSON['status'] as String; - - final extraId = responseJSON['deposit_extra_id'] as String?; - - return Trade( - id: id, - from: from, - to: to, - provider: description, - inputAddress: depositAddress, - payoutAddress: payoutAddress, - refundAddress: refundAddress, - amount: depositAmount, - receiveAmount: receiveAmount, - state: TradeState.deserialize(raw: status), - isRefund: status == 'refund', - extraId: extraId, - ); - } - - Future> _getInfo(Map params, bool isFixedRateMode) async { - final headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': apiKey - }; - - try { - final uri = Uri.https(_baseUrl, isFixedRateMode ? _infoRevertPath : _infoPath); - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode(params), - ); - - if (response.statusCode != 200) { - throw Exception('LetsExchange fetch info failed: ${response.body}'); - } - return json.decode(response.body) as Map; - } catch (e) { - throw Exception('LetsExchange failed to fetch info ${e.toString()}'); - } - } - - String? _getNetworkType(CryptoCurrency currency) { - if (currency.tag != null && currency.tag!.isNotEmpty) { - switch (currency.tag!) { - case 'TRX': - return 'TRC20'; - case 'ETH': - return 'ERC20'; - case 'BSC': - return 'BEP20'; - case 'ARB': - return 'ARBITRUM'; - default: - return currency.tag!; - } - } - - return _normalizeTitleToNetwork(currency.title); - } - - String _normalizeNetworkType(String network) { - return switch (network.toUpperCase()) { - 'ERC20' => 'ETH', - 'TRC20' => 'TRX', - 'BEP20' => 'BSC', - 'ARBITRUM' => 'ARB', - _ => network, - }; - } - - String _normalizeTitleToNetwork(String title) { - return switch (title.toUpperCase()) { - 'ARB' => 'ARBITRUM', - _ => title, - }; - } - - String _normalizeBchAddress(String address) => - address.startsWith('bitcoincash:') ? address.substring(12) : address; -} diff --git a/lib/exchange/provider/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart deleted file mode 100644 index 6bfab9d8d..000000000 --- a/lib/exchange/provider/sideshift_exchange_provider.dart +++ /dev/null @@ -1,479 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_not_created_exception.dart'; -import 'package:hash_wallet/exchange/trade_not_found_exception.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:hash_wallet/utils/exchange_provider_logger.dart'; - -class SideShiftExchangeProvider extends ExchangeProvider { - SideShiftExchangeProvider(); - - static const affiliateId = secrets.sideShiftAffiliateId; - static const apiBaseUrl = 'https://sideshift.ai/api'; - static const rangePath = '/v2/pair'; - static const orderPath = '/v2/shifts'; - static const quotePath = '/v2/quotes'; - static const permissionPath = '/v2/permissions'; - - @override - String get title => 'SideShift'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => true; - - @override - ExchangeProviderDescription get description => ExchangeProviderDescription.sideShift; - - @override - Future checkIsAvailable() async { - const url = apiBaseUrl + permissionPath; - final uri = Uri.parse(url); - final response = await ProxyWrapper().get(clearnetUri: uri); - - if (response.statusCode == 500) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error']['message'] as String; - - throw Exception('$error'); - } - - if (response.statusCode != 200) return false; - - final responseJSON = json.decode(response.body) as Map; - return responseJSON['createShift'] as bool; - } - - @override - Future fetchLimits( - {required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode}) async { - final fromCurrency = isFixedRateMode ? to : from; - final toCurrency = isFixedRateMode ? from : to; - - final fromNetwork = _networkFor(fromCurrency); - final toNetwork = _networkFor(toCurrency); - - final url = - "$apiBaseUrl$rangePath/${fromCurrency.title.toLowerCase()}-$fromNetwork/${toCurrency.title.toLowerCase()}-$toNetwork"; - - final uri = Uri.parse(url); - final response = await ProxyWrapper().get(clearnetUri: uri); - - - if (response.statusCode == 500) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error']['message'] as String; - - throw Exception('$error'); - } - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final responseJSON = json.decode(response.body) as Map; - final min = double.tryParse(responseJSON['min'] as String? ?? ''); - final max = double.tryParse(responseJSON['max'] as String? ?? ''); - - if (isFixedRateMode) { - final currentRate = double.parse(responseJSON['rate'] as String); - return Limits( - min: min != null ? (min * currentRate) : null, - max: max != null ? (max * currentRate) : null, - ); - } - - return Limits(min: min, max: max); - } - - @override - Future fetchRate({ - required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount - }) async { - try { - if (amount == 0) return 0.0; - - final fromCurrency = from.title.toLowerCase(); - final toCurrency = to.title.toLowerCase(); - final depositNetwork = _networkFor(from); - final settleNetwork = _networkFor(to); - - final url = - "$apiBaseUrl$rangePath/$fromCurrency-$depositNetwork/$toCurrency-$settleNetwork?amount=$amount"; - - final uri = Uri.parse(url); - final response = await ProxyWrapper().get(clearnetUri: uri); - - final responseJSON = json.decode(response.body) as Map; - - if (response.statusCode == 500) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error']['message'] as String; - - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: Exception('SideShift Internal Server Error: $error'), - stackTrace: StackTrace.current, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'url': url, - }, - ); - - throw Exception('SideShift Internal Server Error: $error'); - } - - if (response.statusCode != 200) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: Exception('Unexpected http status: ${response.statusCode}'), - stackTrace: StackTrace.current, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'url': url, - }, - ); - - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final rate = double.parse(responseJSON['rate'] as String); - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'fetchRate', - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'url': url, - }, - responseData: { - 'rate': rate, - 'statusCode': response.statusCode, - }, - ); - - return rate; - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: e, - stackTrace: s, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - printV(e.toString()); - return 0.00; - } - } - - @override - Future createTrade({ - required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll, - }) async { - String url = ''; - final body = { - 'affiliateId': affiliateId, - 'settleAddress': request.toAddress, - 'refundAddress': request.refundAddress, - }; - - if (isFixedRateMode) { - final quoteId = await _createQuote(request); - body['quoteId'] = quoteId; - - url = apiBaseUrl + orderPath + '/fixed'; - } else { - url = apiBaseUrl + orderPath + '/variable'; - body["depositCoin"] = _normalizeCurrency(request.fromCurrency); - body["settleCoin"] = _normalizeCurrency(request.toCurrency); - body["settleNetwork"] = _networkFor(request.toCurrency); - body["depositNetwork"] = _networkFor(request.fromCurrency); - } - final headers = {'Content-Type': 'application/json'}; - - final uri = Uri.parse(url); - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode(body), - ); - - - if (response.statusCode != 201) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error']['message'] as String; - - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: TradeNotCreatedException(description, description: error), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'url': url, - 'body': body, - }, - ); - - throw TradeNotCreatedException(description, description: error); - } - - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: TradeNotCreatedException(description), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'url': url, - 'body': body, - }, - ); - - throw TradeNotCreatedException(description); - } - - final responseJSON = json.decode(response.body) as Map; - final id = responseJSON['id'] as String; - final inputAddress = responseJSON['depositAddress'] as String; - final settleAddress = responseJSON['settleAddress'] as String; - final depositAmount = responseJSON['depositAmount'] as String?; - final depositMemo = responseJSON['depositMemo'] as String?; - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'createTrade', - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'url': url, - 'body': body, - }, - responseData: { - 'id': id, - 'inputAddress': inputAddress, - 'settleAddress': settleAddress, - 'depositAmount': depositAmount, - 'depositMemo': depositMemo, - 'statusCode': response.statusCode, - }, - ); - - return Trade( - id: id, - provider: description, - from: request.fromCurrency, - to: request.toCurrency, - inputAddress: inputAddress, - refundAddress: settleAddress, - state: TradeState.created, - amount: depositAmount ?? request.fromAmount, - receiveAmount: request.toAmount, - payoutAddress: settleAddress, - createdAt: DateTime.now(), - isSendAll: isSendAll, - extraId: depositMemo, - ); - } - - @override - Future findTradeById({required String id}) async { - final url = apiBaseUrl + orderPath + '/' + id; - final uri = Uri.parse(url); - final response = await ProxyWrapper().get(clearnetUri: uri); - - if (response.statusCode == 404) { - throw TradeNotFoundException(id, provider: description); - } - - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error']['message'] as String; - - throw TradeNotFoundException(id, provider: description, description: error); - } - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final responseJSON = json.decode(response.body) as Map; - final fromCurrency = responseJSON['depositCoin'] as String; - final fromNetwork = responseJSON['depositNetwork'] as String?; - final toCurrency = responseJSON['settleCoin'] as String; - final toNetwork = responseJSON['settleNetwork'] as String?; - final inputAddress = responseJSON['depositAddress'] as String; - final expectedSendAmount = responseJSON['depositAmount'] as String?; - final status = responseJSON['status'] as String?; - final settleAddress = responseJSON['settleAddress'] as String; - final isVariable = (responseJSON['type'] as String) == 'variable'; - final expiredAtRaw = responseJSON['expiresAt'] as String; - final expiredAt = isVariable ? null : DateTime.tryParse(expiredAtRaw)?.toLocal(); - final depositMemo = responseJSON['depositMemo'] as String?; - - final fromParsed = CryptoCurrency.safeParseCurrencyFromString( - fromCurrency, - tag: fromNetwork, - ); - final toParsed = CryptoCurrency.safeParseCurrencyFromString( - toCurrency, - tag: toNetwork, - ); - return Trade( - id: id, - from: fromParsed, - to: toParsed, - provider: description, - inputAddress: inputAddress, - amount: expectedSendAmount ?? '', - state: TradeState.deserialize(raw: status ?? 'created'), - expiredAt: expiredAt, - payoutAddress: settleAddress, - extraId: depositMemo, - ); - } - - Future _createQuote(TradeRequest request) async { - final url = apiBaseUrl + quotePath; - final headers = {'Content-Type': 'application/json'}; - final body = { - 'depositCoin': _normalizeCurrency(request.fromCurrency), - 'settleCoin': _normalizeCurrency(request.toCurrency), - 'affiliateId': affiliateId, - 'settleAmount': request.toAmount, - 'settleNetwork': _networkFor(request.toCurrency), - 'depositNetwork': _networkFor(request.fromCurrency), - }; - final uri = Uri.parse(url); - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode(body), - ); - - - if (response.statusCode != 201) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error']['message'] as String; - - throw TradeNotCreatedException(description, description: error); - } - - throw TradeNotCreatedException(description); - } - - final responseJSON = json.decode(response.body) as Map; - - return responseJSON['id'] as String; - } - - String _normalizeCurrency(CryptoCurrency currency) { - switch (currency) { - case CryptoCurrency.usdcEPoly: - return 'usdc'; - default: - return currency.title.toLowerCase(); - } - } - - String _networkFor(CryptoCurrency currency) => - currency.tag != null ? _normalizeTag(currency.tag!) : 'mainnet'; - - String _normalizeTag(String tag) { - switch (tag) { - case 'ETH': - return 'ethereum'; - case 'TRX': - return 'tron'; - case 'LN': - return 'lightning'; - case 'POL': - return 'polygon'; - case 'ARB': - return 'arbitrum'; - case 'ZEC': - return 'zcash'; - case 'AVAXC': - return 'avax'; - default: - return tag.toLowerCase(); - } - } - - String _normalizeNetworkType(String network) { - return switch (network) { - 'ethereum' => 'ETH', - 'tron' => 'TRX', - 'lightning' => 'LN', - 'polygon' => 'POL', - 'arbitrum' => 'ARB', - 'zcash' => 'ZEC', - 'avax' => 'AVAXC', - _ => network, - }; - } -} diff --git a/lib/exchange/provider/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart deleted file mode 100644 index 9b96ca118..000000000 --- a/lib/exchange/provider/simpleswap_exchange_provider.dart +++ /dev/null @@ -1,371 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_not_created_exception.dart'; -import 'package:hash_wallet/exchange/trade_not_found_exception.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:hash_wallet/utils/device_info.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:hash_wallet/utils/exchange_provider_logger.dart'; - -class SimpleSwapExchangeProvider extends ExchangeProvider { - SimpleSwapExchangeProvider(); - - - static final apiKey = - DeviceInfo.instance.isMobile ? secrets.simpleSwapApiKey : secrets.simpleSwapApiKeyDesktop; - static const apiAuthority = 'api.simpleswap.io'; - static const getEstimatePath = '/v1/get_estimated'; - static const rangePath = '/v1/get_ranges'; - static const getExchangePath = '/v1/get_exchange'; - static const createExchangePath = '/v1/create_exchange'; - - @override - String get title => 'SimpleSwap'; - - @override - bool get isAvailable => false; - - @override - bool get isEnabled => false; - - @override - bool get supportsFixedRate => false; - - @override - ExchangeProviderDescription get description => ExchangeProviderDescription.simpleSwap; - - @override - Future checkIsAvailable() async { - final uri = Uri.https(apiAuthority, getEstimatePath, {'api_key': apiKey}); - final response = await ProxyWrapper().get(clearnetUri: uri); - - return !(response.statusCode == 403); - } - - @override - Future fetchLimits( - {required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode}) async { - final params = { - 'api_key': apiKey, - 'fixed': isFixedRateMode.toString(), - 'currency_from': _normalizeCurrency(from), - 'currency_to': _normalizeCurrency(to), - }; - final uri = Uri.https(apiAuthority, rangePath, params); - - final response = await ProxyWrapper().get(clearnetUri: uri); - - - if (response.statusCode == 500) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['message'] as String; - - throw Exception('$error'); - } - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final responseJSON = json.decode(response.body) as Map; - final min = double.tryParse(responseJSON['min'] as String? ?? ''); - final max = double.tryParse(responseJSON['max'] as String? ?? ''); - - return Limits(min: min, max: max); - } - - @override - Future fetchRate({ - required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount - }) async { - try { - if (amount == 0) return 0.0; - - final params = { - 'api_key': apiKey, - 'currency_from': _normalizeCurrency(from), - 'currency_to': _normalizeCurrency(to), - 'amount': amount.toString(), - 'fixed': isFixedRateMode.toString() - }; - final uri = Uri.https(apiAuthority, getEstimatePath, params); - final response = await ProxyWrapper().get(clearnetUri: uri); - - - if (response.body == "null") { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: Exception('Null response body'), - stackTrace: StackTrace.current, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'params': params, - 'url': uri.toString(), - }, - ); - return 0.00; - } - - final data = json.decode(response.body) as String; - final rate = double.parse(data) / amount; - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'fetchRate', - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'params': params, - 'url': uri.toString(), - }, - responseData: { - 'data': data, - 'rate': rate, - 'statusCode': response.statusCode, - 'responseBody': response.body, - }, - ); - - return rate; - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: e, - stackTrace: s, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - return 0.00; - } - } - - @override - Future createTrade({ - required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll, - }) async { - final headers = {'Content-Type': 'application/json'}; - final params = {'api_key': apiKey}; - final body = { - "currency_from": _normalizeCurrency(request.fromCurrency), - "currency_to": _normalizeCurrency(request.toCurrency), - "amount": request.fromAmount, - "fixed": isFixedRateMode, - "user_refund_address": _normalizeAddress(request.refundAddress), - "address_to": _normalizeAddress(request.toAddress) - }; - final uri = Uri.https(apiAuthority, createExchangePath, params); - - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: headers, - body: json.encode(body), - ); - - - if (response.statusCode != 200 && response.statusCode != 201) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['message'] as String; - - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: TradeNotCreatedException(description, description: error), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'url': uri.toString(), - }, - ); - - throw TradeNotCreatedException(description, description: error); - } - - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: TradeNotCreatedException(description), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'url': uri.toString(), - }, - ); - - throw TradeNotCreatedException(description); - } - - final responseJSON = json.decode(response.body) as Map; - final id = responseJSON['id'] as String; - final inputAddress = responseJSON['address_from'] as String; - final payoutAddress = responseJSON['address_to'] as String; - final settleAddress = responseJSON['user_refund_address'] as String; - final extraId = responseJSON['extra_id_from'] as String?; - final receiveAmount = responseJSON['amount_to'] as String?; - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'createTrade', - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'url': uri.toString(), - }, - responseData: { - 'id': id, - 'inputAddress': inputAddress, - 'payoutAddress': payoutAddress, - 'settleAddress': settleAddress, - 'extraId': extraId, - 'receiveAmount': receiveAmount, - 'statusCode': response.statusCode, - 'responseJSON': responseJSON, - }, - ); - - return Trade( - id: id, - provider: description, - from: request.fromCurrency, - to: request.toCurrency, - inputAddress: inputAddress, - refundAddress: settleAddress, - extraId: extraId, - state: TradeState.created, - amount: request.fromAmount, - receiveAmount: receiveAmount ?? request.toAmount, - payoutAddress: payoutAddress, - createdAt: DateTime.now(), - isSendAll: isSendAll, - ); - } - - @override - Future findTradeById({required String id}) async { - final params = {'api_key': apiKey, 'id': id}; - final uri = Uri.https(apiAuthority, getExchangePath, params); - final response = await ProxyWrapper().get(clearnetUri: uri); - - - if (response.statusCode == 404) { - throw TradeNotFoundException(id, provider: description); - } - - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['message'] as String; - - throw TradeNotFoundException(id, provider: description, description: error); - } - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final responseJSON = json.decode(response.body) as Map; - final fromCurrency = responseJSON['currency_from'] as String; - final toCurrency = responseJSON['currency_to'] as String; - final inputAddress = responseJSON['address_from'] as String; - final expectedSendAmount = responseJSON['expected_amount'].toString(); - final extraId = responseJSON['extra_id_from'] as String?; - final status = responseJSON['status'] as String; - final payoutAddress = responseJSON['address_to'] as String; - - final fromParsed = - CryptoCurrency.safeParseCurrencyFromString(fromCurrency); - final toParsed = CryptoCurrency.safeParseCurrencyFromString(toCurrency); - return Trade( - id: id, - from: fromParsed, - to: toParsed, - extraId: extraId, - provider: description, - inputAddress: inputAddress, - amount: expectedSendAmount, - state: TradeState.deserialize(raw: status), - payoutAddress: payoutAddress, - ); - } - - static String _normalizeCurrency(CryptoCurrency currency) { - switch (currency) { - case CryptoCurrency.zec: - return 'zec'; - case CryptoCurrency.bnb: - return currency.tag!.toLowerCase(); - case CryptoCurrency.usdterc20: - return 'usdterc20'; - case CryptoCurrency.usdttrc20: - return 'usdttrc20'; - case CryptoCurrency.usdcpoly: - return 'usdcpoly'; - case CryptoCurrency.usdtPoly: - return 'usdtpoly'; - case CryptoCurrency.usdcEPoly: - return 'usdcepoly'; - case CryptoCurrency.usdcsol: - return 'usdcspl'; - case CryptoCurrency.matic: - return 'pol'; - case CryptoCurrency.maticpoly: - return 'matic'; - default: - return currency.title.toLowerCase(); - } - } - - String _normalizeAddress(String address) => - address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address; -} diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart deleted file mode 100644 index 3c987790c..000000000 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ /dev/null @@ -1,448 +0,0 @@ -import 'dart:convert'; -import 'dart:developer'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_not_created_exception.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:hash_wallet/utils/exchange_provider_logger.dart'; - -class StealthExExchangeProvider extends ExchangeProvider { - StealthExExchangeProvider(); - - static final apiKey = secrets.stealthExBearerToken; - static final _additionalFeePercent = double.tryParse(secrets.stealthExAdditionalFeePercent); - static const _baseUrl = 'https://api.stealthex.io'; - static const _rangePath = '/v4/rates/range'; - static const _amountPath = '/v4/rates/estimated-amount'; - static const _exchangesPath = '/v4/exchanges'; - - @override - String get title => 'StealthEX'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => true; - - @override - ExchangeProviderDescription get description => ExchangeProviderDescription.stealthEx; - - @override - Future checkIsAvailable() async => true; - - @override - Future fetchLimits( - {required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode}) async { - final curFrom = isFixedRateMode ? to : from; - final curTo = isFixedRateMode ? from : to; - - final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; - final body = { - 'route': { - 'from': {'symbol': _getName(curFrom), 'network': _getNetwork(curFrom)}, - 'to': {'symbol': _getName(curTo), 'network': _getNetwork(curTo)} - }, - 'estimation': isFixedRateMode ? 'reversed' : 'direct', - 'rate': isFixedRateMode ? 'fixed' : 'floating', - 'additional_fee_percent': _additionalFeePercent, - }; - - try { - final response = await ProxyWrapper().post( - clearnetUri: Uri.parse(_baseUrl + _rangePath), - headers: headers, - body: json.encode(body), - ); - - if (response.statusCode != 200) { - throw Exception('StealthEx fetch limits failed: ${response.body}'); - } - final responseJSON = json.decode(response.body) as Map; - final min = _toDouble(responseJSON['min_amount']); - final max = _toDouble(responseJSON['max_amount']); - return Limits(min: min, max: max); - } catch (e) { - log(e.toString()); - throw Exception('StealthEx failed to fetch limits'); - } - } - - @override - Future fetchRate( - {required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount}) async { - try { - final response = await getEstimatedExchangeAmount( - from: from, - to: to, - amount: amount, - isFixedRateMode: isFixedRateMode, - ); - final estimatedAmount = response['estimated_amount'] as double? ?? 0.0; - - if (estimatedAmount <= 0.0) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: Exception('Invalid estimated amount: $estimatedAmount'), - stackTrace: StackTrace.current, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - return 0.0; - } - - final rate = isFixedRateMode ? amount / estimatedAmount : estimatedAmount / amount; - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'fetchRate', - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - responseData: { - 'estimatedAmount': estimatedAmount, - 'rate': rate, - 'response': response, - }, - ); - - return rate; - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: e, - stackTrace: s, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - return 0.0; - } - } - - @override - Future createTrade( - {required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll}) async { - String? rateId; - String? validUntil; - - try { - if (isFixedRateMode) { - final response = await getEstimatedExchangeAmount( - from: request.fromCurrency, - to: request.toCurrency, - amount: double.parse(request.toAmount), - isFixedRateMode: isFixedRateMode); - rateId = response['rate_id'] as String?; - validUntil = response['valid_until'] as String?; - if (rateId == null) throw TradeNotCreatedException(description); - } - - final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; - final body = { - 'route': { - 'from': { - 'symbol': _getName(request.fromCurrency), - 'network': _getNetwork(request.fromCurrency) - }, - 'to': {'symbol': _getName(request.toCurrency), 'network': _getNetwork(request.toCurrency)} - }, - 'estimation': isFixedRateMode ? 'reversed' : 'direct', - 'rate': isFixedRateMode ? 'fixed' : 'floating', - if (isFixedRateMode) 'rate_id': rateId, - 'amount': - isFixedRateMode ? double.parse(request.toAmount) : double.parse(request.fromAmount), - 'address': _normalizeAddress(request.toAddress), - 'refund_address': _normalizeAddress(request.refundAddress), - 'additional_fee_percent': _additionalFeePercent, - }; - - final response = await ProxyWrapper().post( - clearnetUri: Uri.parse(_baseUrl + _exchangesPath), - headers: headers, - body: json.encode(body), - ); - - if (response.statusCode != 201) { - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: Exception('StealthEx create trade failed: ${response.body}'), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'rateId': rateId, - }, - ); - throw Exception('StealthEx create trade failed: ${response.body}'); - } - final responseJSON = json.decode(response.body) as Map; - final deposit = responseJSON['deposit'] as Map; - final withdrawal = responseJSON['withdrawal'] as Map; - - final id = responseJSON['id'] as String; - final from = deposit['symbol'] as String; - final to = withdrawal['symbol'] as String; - final payoutAddress = withdrawal['address'] as String; - final depositAddress = deposit['address'] as String; - final refundAddress = responseJSON['refund_address'] as String; - final depositAmount = _toDouble(deposit['amount']); - final receiveAmount = _toDouble(withdrawal['amount']); - final status = responseJSON['status'] as String; - final createdAtString = responseJSON['created_at'] as String; - final extraId = deposit['extra_id'] as String?; - - final createdAt = DateTime.parse(createdAtString).toLocal(); - final expiredAt = validUntil != null - ? DateTime.parse(validUntil).toLocal() - : DateTime.now().add(Duration(minutes: 5)); - - CryptoCurrency fromCurrency; - if (request.fromCurrency.tag != null && request.fromCurrency.title.toLowerCase() == from) { - fromCurrency = request.fromCurrency; - } else { - fromCurrency = CryptoCurrency.fromString(from); - } - - CryptoCurrency toCurrency; - if (request.toCurrency.tag != null && request.toCurrency.title.toLowerCase() == to) { - toCurrency = request.toCurrency; - } else { - toCurrency = CryptoCurrency.fromString(to); - } - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'createTrade', - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'rateId': rateId, - }, - responseData: { - 'id': id, - 'from': from, - 'to': to, - 'depositAddress': depositAddress, - 'payoutAddress': payoutAddress, - 'refundAddress': refundAddress, - 'depositAmount': depositAmount, - 'receiveAmount': receiveAmount, - 'status': status, - 'createdAt': createdAtString, - 'extraId': extraId, - 'statusCode': response.statusCode, - 'responseJSON': responseJSON, - }, - ); - - return Trade( - id: id, - from: fromCurrency, - to: toCurrency, - provider: description, - inputAddress: depositAddress, - payoutAddress: payoutAddress, - refundAddress: refundAddress, - amount: depositAmount.toString(), - receiveAmount: receiveAmount.toString(), - state: TradeState.deserialize(raw: status), - createdAt: createdAt, - expiredAt: expiredAt, - extraId: extraId, - isSendAll: isSendAll, - ); - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: e, - stackTrace: s, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - }, - ); - log(e.toString()); - throw TradeNotCreatedException(description); - } - } - - @override - Future findTradeById({required String id}) async { - final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; - - final uri = Uri.parse('$_baseUrl$_exchangesPath/$id'); - final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers); - - if (response.statusCode != 200) { - throw Exception('StealthEx fetch trade failed: ${response.body}'); - } - final responseJSON = json.decode(response.body) as Map; - final deposit = responseJSON['deposit'] as Map; - final withdrawal = responseJSON['withdrawal'] as Map; - - final respId = responseJSON['id'] as String; - - // Parsing 'from' currency with network tag - final fromCurrency = deposit['symbol'] as String; - final fromNetwork = deposit['network'] as String?; - final fromTag = fromNetwork == 'mainnet' ? null : fromNetwork; - final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag); - - // Parsing 'to' currency with network tag - final toCurrency = withdrawal['symbol'] as String; - final toNetwork = withdrawal['network'] as String?; - final toTag = toNetwork == 'mainnet' ? null : toNetwork; - final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag); - - final payoutAddress = withdrawal['address'] as String; - final depositAddress = deposit['address'] as String; - final refundAddress = responseJSON['refund_address'] as String; - final depositAmount = _toDouble(deposit['amount']); - final receiveAmount = _toDouble(withdrawal['amount']); - final status = responseJSON['status'] as String; - final createdAtString = responseJSON['created_at'] as String; - final createdAt = DateTime.parse(createdAtString).toLocal(); - final extraId = deposit['extra_id'] as String?; - - return Trade( - id: respId, - from: from, - to: to, - provider: description, - inputAddress: depositAddress, - payoutAddress: payoutAddress, - refundAddress: refundAddress, - amount: depositAmount.toString(), - receiveAmount: receiveAmount.toString(), - state: TradeState.deserialize(raw: status), - createdAt: createdAt, - isRefund: status == 'refunded', - extraId: extraId, - ); - } - - Future> getEstimatedExchangeAmount( - {required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode}) async { - final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; - - final body = { - 'route': { - 'from': {'symbol': _getName(from), 'network': _getNetwork(from)}, - 'to': {'symbol': _getName(to), 'network': _getNetwork(to)} - }, - 'estimation': isFixedRateMode ? 'reversed' : 'direct', - 'rate': isFixedRateMode ? 'fixed' : 'floating', - 'amount': amount, - 'additional_fee_percent': _additionalFeePercent, - }; - - try { - final response = await ProxyWrapper().post( - clearnetUri: Uri.parse(_baseUrl + _amountPath), - headers: headers, - body: json.encode(body), - ); - - if (response.statusCode != 200) return {}; - final responseJSON = json.decode(response.body) as Map; - final rate = responseJSON['rate'] as Map?; - return { - 'estimated_amount': responseJSON['estimated_amount'] as double?, - if (rate != null) 'valid_until': rate['valid_until'] as String?, - if (rate != null) 'rate_id': rate['id'] as String? - }; - } catch (e) { - log(e.toString()); - return {}; - } - } - - static double? _toDouble(dynamic value) { - if (value is int) { - return value.toDouble(); - } else if (value is double) { - return value; - } else if (value is String) { - return double.tryParse(value); - } - return null; - } - - String _getName(CryptoCurrency currency) { - if (currency == CryptoCurrency.usdcEPoly) return 'usdce'; - return currency.title.toLowerCase(); - } - - String _getNetwork(CryptoCurrency currency) { - if (currency == CryptoCurrency.arb || currency.tag == 'ARB') return 'arbitrum'; - if (currency.tag == null) return 'mainnet'; - - if (currency == CryptoCurrency.maticpoly) return 'mainnet'; - - if (currency.tag == 'POLY') return 'matic'; - - return currency.tag!.toLowerCase(); - } - - String _normalizeAddress(String address) => - address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address; -} diff --git a/lib/exchange/provider/swapsxyz_exchange_provider.dart b/lib/exchange/provider/swapsxyz_exchange_provider.dart deleted file mode 100644 index c60531174..000000000 --- a/lib/exchange/provider/swapsxyz_exchange_provider.dart +++ /dev/null @@ -1,865 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_not_created_exception.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:cw_core/amount_converter.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/crypto_currency.dart'; - -class SwapsXyzExchangeProvider extends ExchangeProvider { - SwapsXyzExchangeProvider(); - - static final List _notSupportedAsSourceToken = [ - CryptoCurrency.sol, - ...CryptoCurrency.all.where( - (c) => (c.tag ?? '').toUpperCase() == 'SOL' || c.tag == CryptoCurrency.bnb.tag, - ), - ]; - - static const _transferSig = '0xa9059cbb'; - static const _swapAndExecuteSig = '0x9be111d1'; - - static final _apiKey = secrets.swapsXyzApiKey; - static const _baseUrl = 'api-v2.swaps.xyz'; - static const _getChainList = 'api/getChainList'; - static const _getPaths = 'api/getPaths'; - static const _getQuotePaths = 'api/getQuote'; - static const _getAction = 'api/getAction'; - static const _registerTxs = 'api/registerTxs'; - static const _getStatus = 'api/getStatus'; - - static final _headers = {'x-api-key': _apiKey}; - - static final _supportedChainList = []; - final Map> _tokensCache = {}; - - @override - String get title => 'Swaps.XYZ'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - // There is an issue with fixed-rate swaps on Swaps.XYZ - // when they don't guarantee that the trade will be created at the requested amount. - @override - bool get supportsFixedRate => false; - - @override - ExchangeProviderDescription get description => - ExchangeProviderDescription.swapsXyz; - - @override - Future checkIsAvailable() async => true; - - @override - Future fetchLimits({ - required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode, - }) async { - try { - final chains = await _geSupportedChain(); - if (chains.isEmpty) throw Exception('Failed to fetch supported chains'); - - final fromToUse = isFixedRateMode ? to : from; - final toToUse = isFixedRateMode ? from : to; - - final srcChain = _findChainByCurrency(fromToUse, chains); - final dstChain = _findChainByCurrency(toToUse, chains); - - await _ensureTokensCached( - fromChain: srcChain, toChain: dstChain, from: fromToUse, to: toToUse); - - final srcToken = _getTokenAddress(currency: fromToUse, chain: srcChain); - final dstToken = _getTokenAddress(currency: toToUse, chain: dstChain); - - final params = { - 'srcChainId': '${srcChain.chainId}', - 'srcToken': srcToken, - 'dstChainId': '${dstChain.chainId}', - 'dstToken': dstToken, - }; - - final uri = Uri.https(_baseUrl, _getPaths, params); - final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers); - if (res.statusCode != 200) { - throw Exception('Unexpected http status: ${res.statusCode}'); - } - - final body = json.decode(res.body) as Map; - - final paths = - (body['paths'] as List? ?? const []).cast>(); - if (paths.isEmpty) { - throw Exception('No paths for ${fromToUse.title} -> ${toToUse.title}'); - } - - final int requestedDstId = dstChain.chainId; - - Map path = paths.firstWhere( - (p) => p['chainId'] == requestedDstId, - orElse: () => {}, - ); - - if (path.isEmpty) { - path = paths.firstWhere( - (p) => (p['tokens'] is List) || p['amountLimits'] != null, - orElse: () => paths.first, - ); - } - - final supportsExactAmountIn = - path['supportsExactAmountIn'] as bool? ?? false; - final supportsExactAmountOut = - path['supportsExactAmountOut'] as bool? ?? false; - - if (isFixedRateMode && !supportsExactAmountOut) { - throw Exception( - 'This route does not support fixed receive (exact-amount-out)'); - } - if (!isFixedRateMode && !supportsExactAmountIn) { - throw Exception( - 'This route does not support exact send (exact-amount-in)'); - } - - Map? useLimits; - - if (isFixedRateMode) { - final tokensField = path['tokens']; - useLimits = null; - - if (tokensField is List && tokensField.isNotEmpty) { - final tokens = tokensField.cast>(); - String norm(String s) => s.toUpperCase(); - final wantSym = norm(_normalizeCakeNativeTokenName(toToUse.title)); - final wantAddr = (dstToken).toLowerCase(); - - final match = tokens.firstWhere( - (t) { - final sym = norm(t['symbol']?.toString() ?? ''); - final addr = (t['address']?.toString() ?? '').toLowerCase(); - return sym == wantSym || (addr.isNotEmpty && addr == wantAddr); - }, - orElse: () => const {}, - ); - - if (match.isNotEmpty) { - useLimits = { - 'minAmount': match['minAmount'], - 'maxAmount': match['maxAmount'], - }; - } - } - } else { - // Floating/Exact-in: use the route-level limits - useLimits = path['amountLimits'] as Map?; - } - - final min = double.tryParse((useLimits?['minAmount'])?.toString() ?? ''); - final max = double.tryParse((useLimits?['maxAmount'])?.toString() ?? ''); - return Limits(min: min, max: max); - } catch (e) { - printV('fetchLimits error: $e'); - throw Exception('Error fetching limits: $e'); - } - } - - Future<_PathInfo?> _pickPath({ - required int srcChainId, - required String srcToken, - required int dstChainId, - required String dstToken, - }) async { - final uri = Uri.https(_baseUrl, _getPaths, { - 'srcChainId': '$srcChainId', - 'srcToken': srcToken, - 'dstChainId': '$dstChainId', - 'dstToken': dstToken, - }); - final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers); - if (res.statusCode != 200) return null; - final body = json.decode(res.body) as Map; - final paths = - (body['paths'] as List?)?.cast>() ?? const []; - if (paths.isEmpty) return null; - final p = paths.first; - return _PathInfo( - supportsExactOut: p['supportsExactAmountOut'] == true, - minToAmountHuman: (p['amountLimits']?['minAmount'] as String?) ?? '0', - ); - } - - @override - Future fetchRate( - {required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount}) async { - try { - - if(_notSupportedAsSourceToken.contains(from) || _notSupportedAsSourceToken.contains(to)) { - printV( - 'fetchRate: source token ${from.title} is not supported as source token'); - return 0.0; - } - - - - final chains = await _geSupportedChain(); - if (chains.isEmpty) return 0.0; - - final srcChain = _findChainByCurrency(from, chains); - final dstChain = _findChainByCurrency(to, chains); - - await _ensureTokensCached( - fromChain: srcChain, toChain: dstChain, from: from, to: to); - - final srcToken = _getTokenAddress(currency: from, chain: srcChain); - final dstToken = _getTokenAddress(currency: to, chain: dstChain); - - if (isReceiveAmount) { - final path = await _pickPath( - srcChainId: srcChain.chainId, - srcToken: srcToken, - dstChainId: dstChain.chainId, - dstToken: dstToken, - ); - if (path == null || !path.supportsExactOut) { - printV( - 'fetchRate: route does not support exact-amount-out for ${from.title} -> ${to.title}'); - return 0.0; - } - } - - final humanAmountStr = amount.toString(); - final formattedAmount = AmountConverter.toBaseUnits( - humanAmountStr, - isReceiveAmount ? to.decimals : from.decimals, - ); - - final params = { - 'swapDirection': - isReceiveAmount ? 'exact-amount-out' : 'exact-amount-in', - 'srcToken': srcToken, - 'dstToken': dstToken, - 'srcChainId': '${srcChain.chainId}', - 'dstChainId': '${dstChain.chainId}', - 'amount': formattedAmount, - }; - - final uri = Uri.https(_baseUrl, _getQuotePaths, params); - final response = - await ProxyWrapper().get(clearnetUri: uri, headers: _headers); - - if (response.statusCode != 200) { - printV('fetchRate failed: ${response.body}'); - return 0.0; - } - - final data = json.decode(response.body) as Map; - final exchangeRate = (data['exchangeRate'] as num?)?.toDouble() ?? 0.0; - return exchangeRate; - } catch (e) { - printV('fetchRate error: $e'); - return 0.0; - } - } - - @override - Future createTrade({ - required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll, - }) async { - try { - final sender = request.refundAddress.trim(); - final recipient = request.toAddress.trim(); - if (sender.isEmpty || recipient.isEmpty) { - throw Exception( - 'Sender (refundAddress) or recipient (toAddress) is empty'); - } - - final chains = await _geSupportedChain(); - if (chains.isEmpty) throw Exception('Failed to fetch supported chains'); - final srcChain = _findChainByCurrency(request.fromCurrency, chains); - final dstChain = _findChainByCurrency(request.toCurrency, chains); - - await _ensureTokensCached( - fromChain: srcChain, - toChain: dstChain, - from: request.fromCurrency, - to: request.toCurrency, - ); - - final srcToken = - _getTokenAddress(currency: request.fromCurrency, chain: srcChain); - final dstToken = - _getTokenAddress(currency: request.toCurrency, chain: dstChain); - - final amountStr = isFixedRateMode ? request.toAmount : request.fromAmount; - final rawAmount = double.tryParse(amountStr) ?? 0.0; - if (rawAmount <= 0) throw Exception('Invalid amount'); - - final formattedAmount = AmountConverter.toBaseUnits( - amountStr, - isFixedRateMode - ? request.toCurrency.decimals - : request.fromCurrency.decimals, - ); - - final params = { - 'actionType': 'swap-action', - 'sender': sender, - 'srcChainId': '${srcChain.chainId}', - 'srcToken': srcToken, - 'dstChainId': '${dstChain.chainId}', - 'dstToken': dstToken, - 'slippage': '300', - 'swapDirection': - isFixedRateMode ? 'exact-amount-out' : 'exact-amount-in', - 'amount': formattedAmount, - 'recipient': recipient, - }; - - final uri = Uri.https(_baseUrl, _getAction, params); - final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers); - - if (res.statusCode != 200) { - throw Exception('getAction failed: ${res.statusCode} ${res.body}'); - } - - final data = json.decode(res.body) as Map; - - final txId = data['txId'] as String? ?? ''; - - final vmId = data['vmId'] as String? ?? ''; - final txObj = (data['tx'] as Map?) ?? const {}; - - final txTo = txObj['to']?.toString(); - final chainId = txObj['chainId']?.toString(); - final routerData = txObj['data']?.toString(); - - // Allow only: - // - null (native / deposit-address flow) - // - '0x' (no call data) - // - ERC20 transfer(0xa9059cbb) selector - // - swapAndExecute(0x9be111d1) selector - final isAllowed = routerData == null || - routerData == '0x' || - _decodeMethodSelector(routerData) == _transferSig || - _decodeMethodSelector(routerData) == _swapAndExecuteSig; - - if (!isAllowed) { - throw Exception('Does not support that method selector'); - } - - final txValue = txObj['value']?.toString() ?? '0'; - - final bridgeIds = (data['bridgeIds'] as List?) ?? const []; - if (txId.isEmpty) throw Exception('No txId returned by getAction'); - - final amtIn = (data['amountIn'] as Map?) ?? const {}; - final amtInMax = (data['amountInMax'] as Map?) ?? const {}; - final srcTokenAddr = amtIn['address']?.toString(); - final srcTokenDecs = - (amtIn['decimals'] as num?)?.toInt() ?? request.fromCurrency.decimals; - final requiresTokenApproval = - data['requiresTokenApproval'] as bool? ?? false; - - final reqAmountStr = - (amtInMax['amount'] ?? amtIn['amount'])?.toString() ?? '0'; - final reqAmountRaw = reqAmountStr.replaceAll('n', ''); - - final needToRegisterInSwapXyz = - vmId == 'alt-vm' || bridgeIds.contains('alt-vm') - || chainId == 'solana' || bridgeIds.contains('solana'); - - final trade = Trade( - id: txId, - router: chainId, - providerId: vmId, - from: request.fromCurrency, - to: request.toCurrency, - provider: description, - inputAddress: txTo, - refundAddress: request.refundAddress, - state: TradeState.created, - providerName: title, - createdAt: DateTime.now(), - amount: request.fromAmount, - receiveAmount: request.toAmount, - payoutAddress: request.toAddress, - isSendAll: isSendAll, - needToRegisterInSwapXyz: needToRegisterInSwapXyz, - sourceTokenAddress: srcTokenAddr ?? srcToken, - sourceTokenDecimals: srcTokenDecs, - sourceTokenAmountRaw: reqAmountRaw, - requiresTokenApproval: requiresTokenApproval, - routerData: routerData, - routerValue: txValue, - ); - - return trade; - } catch (e) { - printV('createTrade error: $e'); - throw TradeNotCreatedException(description, description: e.toString()); - } - } - - /// Register a broadcasted tx with Swaps.xyz (required for alt-vm). - static Future registerAltVmTx({ - required String txId, - required String txHash, - required int chainId, - required String vmId, - }) async { - try { - final uri = Uri.https(_baseUrl, _registerTxs); - final payload = { - 'txId': txId, - 'vmId': vmId, - 'txHash': txHash, - 'chainId': chainId, - }; - - final res = await ProxyWrapper().post( - clearnetUri: uri, - headers: { - ..._headers, - 'content-type': 'application/json', - }, - body: jsonEncode(payload), - ); - - if (res.statusCode != 200) { - printV('registerTxs failed: ${res.statusCode} ${res.body}'); - return false; - } - final List body = json.decode(res.body) as List; - if (body.isEmpty) return false; - - final isSuccess = - (body[0] as Map)['success'] as bool? ?? false; - - return isSuccess; - } catch (e) { - printV('registerAltVmTx error: $e'); - return false; - } - } - - @override - Future findTradeById({required String id}) async { - if (id.isEmpty) { - throw Exception('Trade id is empty'); - } - - final uri = Uri.https(_baseUrl, _getStatus, {'txId': id}); - final resp = await ProxyWrapper().get(clearnetUri: uri, headers: _headers); - - if (resp.statusCode != 200) { - throw Exception('getStatus failed: ${resp.statusCode} ${resp.body}'); - } - - final data = json.decode(resp.body) as Map; - final isSuccess = (data['success'] as bool?); - - if (isSuccess != null && !isSuccess) { - final error = data['error'] as Map?; - if (error != null) { - final code = error['code']?.toString() ?? 'unknown'; - throw Exception( - 'SwapXyzExchangeProvider findTradeById error: ($id) $code'); - } - } - - final statusStr = (data['status'] as String?)?.toLowerCase() ?? 'NOT_FOUND'; - final state = _mapSwapsStatusToTradeState(statusStr); - - final refundAddress = data['sender']?.toString(); - - final srcTransaction = (data['srcTx'] as Map?)?.cast(); - final dstTransaction = (data['dstTx'] as Map?)?.cast(); - - final inputAddress = srcTransaction?['toAddress']?.toString(); - - final payoutAddress = dstTransaction?['toAddress']?.toString(); - - final srcPaymentToken = - (srcTransaction?['paymentToken'] as Map?)?.cast(); - final dstPaymentToken = - (dstTransaction?['paymentToken'] as Map?)?.cast(); - - final fromSymbol = (srcPaymentToken?['symbol'] as String?) ?? ''; - final toSymbol = (dstPaymentToken?['symbol'] as String?) ?? ''; - - CryptoCurrency? toCurrency; - if (toSymbol.isNotEmpty) { - toCurrency = CryptoCurrency.safeParseCurrencyFromString(toSymbol); - } - - final srcDecimals = (srcPaymentToken?['decimals'] as num?)?.toInt() ?? 0; - final dstDecimals = (dstPaymentToken?['decimals'] as num?)?.toInt() ?? 0; - - final txHash = srcTransaction?['txHash'] as String?; - - // Minimal-unit amounts like "12000n" - final srcAmountRaw = srcPaymentToken?['amount']?.toString(); - final dstAmountRaw = dstPaymentToken?['amount']?.toString(); - String? receiveAmount; - if (dstAmountRaw != null) { - final dstAmountMinimal = _stripN(dstAmountRaw); - receiveAmount = - AmountConverter.fromBaseUnits(dstAmountMinimal, dstDecimals); - } - - final srcAmountMinimal = _stripN(srcAmountRaw); - final amount = AmountConverter.fromBaseUnits(srcAmountMinimal, srcDecimals); - final fromCurrency = CryptoCurrency.safeParseCurrencyFromString(fromSymbol); - - // Timestamps can be num or "123n" handle both - final srcTs = _parseUnixSeconds(srcTransaction?['timestamp']); - final dstTs = _parseUnixSeconds(dstTransaction?['timestamp']); - final timestamp = srcTs ?? dstTs; - - final createdAt = timestamp != null - ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000, isUtc: true) - .toLocal() - : null; - - return Trade( - id: (data['txId'] as String?) ?? id, - from: fromCurrency, - to: toCurrency, - provider: description, - inputAddress: inputAddress, - payoutAddress: payoutAddress, - amount: amount, - receiveAmount: receiveAmount, - txId: txHash, - state: state, - createdAt: createdAt, - refundAddress: refundAddress, - ); - } - - TradeState _mapSwapsStatusToTradeState(String s) { - switch (s) { - case 'pending': - case 'processing': - return TradeState.pending; - case 'success': - case 'completed': - case 'complete': - return TradeState.finished; - case 'failed': - case 'cancelled': - case 'canceled': - case 'error': - return TradeState.failed; - default: - return TradeState.pending; - } - } - - // Load & cache supported chains once - Future> _geSupportedChain() async { - if (_supportedChainList.isNotEmpty) return _supportedChainList; - try { - final uri = Uri.https(_baseUrl, _getChainList); - final response = - await ProxyWrapper().get(clearnetUri: uri, headers: _headers); - if (response.statusCode != 200) return []; - - final data = json.decode(response.body) as List; - _supportedChainList - ..clear() - ..addAll(data.map((e) => Chain.fromJson(e as Map))); - return _supportedChainList; - } catch (e) { - printV(e); - return []; - } - } - - Future _ensureTokensCached({ - required Chain fromChain, - required Chain toChain, - required CryptoCurrency from, - required CryptoCurrency to, - }) async { - final needSrc = !_tokensCache.containsKey(fromChain.chainId) || - (_tokensCache[fromChain.chainId]?.isEmpty ?? true); - - final needDst = !_tokensCache.containsKey(toChain.chainId) || - (_tokensCache[toChain.chainId]?.isEmpty ?? true); - - if (!needSrc && !needDst) return; - - if (needSrc) { - await _fetchAndCacheTokens(srcChainId: fromChain.chainId); - } - if (needDst) { - await _fetchAndCacheTokens(srcChainId: toChain.chainId); - } - } - - // call getPaths and merge tokens into cache keyed by the chainId - Future _fetchAndCacheTokens({ - required int srcChainId, - }) async { - final params = { - 'srcChainId': '$srcChainId', - 'srcToken': '0x0000000000000000000000000000000000000000', - // Native placeholder - }; - - final uri = Uri.https(_baseUrl, _getPaths, params); - final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers); - if (res.statusCode != 200) { - printV('getPaths failed: ${res.statusCode} ${res.body}'); - return; - } - - Map body; - try { - body = json.decode(res.body) as Map; - } catch (e) { - printV('getPaths JSON decode error: $e'); - return; - } - - // Always cache the source chain's native token (from body['srcToken']) - final srcTokenJson = body['srcToken'] as Map?; - if (srcTokenJson != null) { - final symbol = (srcTokenJson['symbol'] as String? ?? '').toUpperCase(); - if (symbol.isNotEmpty) { - final isNative = srcTokenJson['isNative'] == true; - final decimals = (srcTokenJson['decimals'] as num?)?.toInt(); - // Treat native token as address = null so _getTokenAddress() emits zero-address - final addr = isNative ? null : (srcTokenJson['address'] as String?); - _mergeCache(srcChainId, [ - TokenPathInfo( - symbol: symbol, - address: addr, - decimals: decimals, - minAmount: srcTokenJson['minAmount']?.toString(), - maxAmount: srcTokenJson['maxAmount']?.toString(), - ), - ]); - } - } - - final paths = (body['paths'] as List?) ?? const []; - if (paths.isEmpty) return; - - for (final path in paths) { - final map = path as Map; - final pathChainId = (map['chainId'] as num?)?.toInt(); - if (pathChainId == null) continue; - - final tokensField = map['tokens']; - - // Case 1: String "all" -> cache empty list to indicate all tokens supported - if (tokensField is String) { - if (tokensField.toLowerCase() == 'all') { - _tokensCache[pathChainId] = - _tokensCache[pathChainId] ?? []; - } - continue; - } - - // Case 2: List -> parse and merge - if (tokensField is List) { - final parsed = []; - for (final token in tokensField) { - if (token is Map) { - try { - parsed.add(TokenPathInfo.fromJson(token)); - } catch (e) { - printV('Token parse error on chain $pathChainId: $e : $token'); - } - } - } - if (parsed.isNotEmpty) { - _mergeCache(pathChainId, parsed); - } - } - } - } - -// Merge by symbol, prefer entries that have a non-empty address/decimals - void _mergeCache(int chainId, List incoming) { - final existing = _tokensCache[chainId] ?? const []; - final bySymbol = { - for (final t in existing) t.symbol: t - }; - - for (final t in incoming) { - final cur = bySymbol[t.symbol]; - if (cur == null) { - bySymbol[t.symbol] = t; - } else { - // If incoming has a real address/decimals, prefer it - final hasBetterAddr = (t.address != null && t.address!.isNotEmpty) && - (cur.address == null || cur.address!.isEmpty); - final hasBetterDec = (t.decimals != null) && (cur.decimals == null); - if (hasBetterAddr || hasBetterDec) { - bySymbol[t.symbol] = t; - } - } - } - - _tokensCache[chainId] = bySymbol.values.toList(); - } - - String _normalizeCakeNativeTokenName(String title) { - final name = title.toUpperCase(); - return switch (name) { - 'ZZEC' => 'ZEC', - _ => name, - }; - } - - String _getTokenAddress({ - required CryptoCurrency currency, - required Chain chain, - }) { - final symbol = _normalizeCakeNativeTokenName(currency.title); - final list = _tokensCache[chain.chainId]; - - // Try cache hit - if (list != null && list.isNotEmpty) { - for (final t in list) { - if (t.symbol == symbol && t.address != null && t.address!.isNotEmpty) { - return t.address!; - } else if (t.symbol == symbol && (t.address == null)) { - // Native token on this chain - return '0x0000000000000000000000000000000000000000'; - } - } - } - - // May fail for non-native Alt-VM assets - return symbol; - } - - Map? findTokenBySymbol( - {required String title, required List tokens}) { - final reqSymbol = title.toUpperCase(); - for (final token in tokens) { - final map = token as Map; - final symbol = (map['symbol'] as String?)?.toUpperCase(); - if (symbol == reqSymbol) return map; - } - return null; - } - - Chain _findChainByCurrency(CryptoCurrency cur, List chains) { - final network = _normalizeCakeNetwork(cur.tag ?? cur.title); - return chains.firstWhere( - (c) { - return c.name.toUpperCase() == network; - }, - orElse: () => throw Exception('Unsupported chain for ${cur.title}'), - ); - } - - String _normalizeCakeNetwork(String network) { - return switch (network.toUpperCase()) { - 'ETH' => 'ETHEREUM', - 'BSC' => 'BNB SMART CHAIN', - 'POL' => 'POLYGON', - 'AVAXC' => 'AVALANCHE', - 'TRX' => 'TRON', - 'SOL' => 'SOLANA', - 'CRO' => 'CRONOS', - 'ADA' => 'CARDANO', - 'KAS' => 'KASPA', - 'TON' => 'TONCOIN', - 'BCH' => 'BITCOIN CASH', - 'ARB' => 'ARBITRUM', - _ => network.toUpperCase(), - }; - } - - int? _parseUnixSeconds(dynamic value) { - if (value == null) return null; - if (value is num) return value.toInt(); - if (value is String) { - final clean = _stripN(value); - return int.tryParse(clean); - } - return null; - } - - String _stripN(String? str) { - final s = str ?? '0'; - return s.endsWith('n') ? s.substring(0, s.length - 1) : s; - } - - String _decodeMethodSelector(String s) => - (s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : ''; -} - -class TokenPathInfo { - final String symbol; - final String? address; - final int? decimals; - final String? minAmount; - final String? maxAmount; - - TokenPathInfo({ - required this.symbol, - required this.address, - required this.decimals, - required this.minAmount, - required this.maxAmount, - }); - - factory TokenPathInfo.fromJson(Map json) => TokenPathInfo( - symbol: (json['symbol'] as String?)?.toUpperCase() ?? '', - address: json['address'] as String?, - decimals: json['decimals'] as int?, - minAmount: json['minAmount']?.toString(), - maxAmount: json['maxAmount']?.toString(), - ); -} - -class Chain { - final int chainId; - final String name; - final String vmId; - - Chain({ - required this.chainId, - required this.name, - required this.vmId, - }); - - factory Chain.fromJson(Map json) { - return Chain( - chainId: json['chainId'] as int, - name: json['name'] as String, - vmId: json['vmId'] as String, - ); - } -} - -class _PathInfo { - final bool supportsExactOut; - final String minToAmountHuman; - - _PathInfo({required this.supportsExactOut, required this.minToAmountHuman}); -} diff --git a/lib/exchange/provider/swaptrade_exchange_provider.dart b/lib/exchange/provider/swaptrade_exchange_provider.dart deleted file mode 100644 index d339e9c3c..000000000 --- a/lib/exchange/provider/swaptrade_exchange_provider.dart +++ /dev/null @@ -1,411 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/.secrets.g.dart' as secrets; -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_not_created_exception.dart'; -import 'package:hash_wallet/exchange/trade_not_found_exception.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:hash_wallet/utils/exchange_provider_logger.dart'; - -class SwapTradeExchangeProvider extends ExchangeProvider { - SwapTradeExchangeProvider(); - - static final markup = secrets.swapTradeExchangeMarkup; - - static const apiAuthority = 'api.swaptrade.io'; - static const getRate = '/api/swap/get-rate'; - static const getCoins = '/api/swap/get-coins'; - static const createOrder = '/api/swap/create-order'; - static const order = '/api/swap/order'; - - @override - String get title => 'SwapTrade'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => false; - - @override - ExchangeProviderDescription get description => ExchangeProviderDescription.swapTrade; - - static const _headers = {'Content-Type': 'application/json'}; - - @override - Future checkIsAvailable() async => true; - - @override - Future fetchLimits({ - required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode, - }) async { - try { - final uri = Uri.https(apiAuthority, getCoins); - final response = await ProxyWrapper().get(clearnetUri: uri); - - final responseJSON = json.decode(response.body) as Map; - - if (response.statusCode != 200) - throw Exception('Unexpected http status: ${response.statusCode}'); - - final coinsInfoRaw = responseJSON['data']; - final coinsInfo = coinsInfoRaw is List ? coinsInfoRaw : []; - - final normalized = _normalizeCurrency(from); - final coin = coinsInfo.cast>().firstWhere( - (c) => (c['id']?.toString().toUpperCase() ?? '') == normalized, - orElse: () => {}, - ); - - if (coin.isEmpty) { - // Currency not supported by SwapTrade (e.g. USDC, DOGE). - return null; - } - - final min = double.tryParse(coin['min']?.toString() ?? '') ?? 0.0; - final max = double.tryParse(coin['max']?.toString() ?? '') ?? 0.0; - if (max == 0) return null; - return Limits(min: min, max: max); - } catch (e) { - printV(e.toString()); - return null; - } - } - - @override - Future fetchRate({ - required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount - }) async { - try { - if (amount == 0) return 0.0; - if (isFixedRateMode && !supportsFixedRate) { - return 0.0; - } - if(from == CryptoCurrency.btcln || to == CryptoCurrency.btcln) return 0; - - final params = {}; - final body = { - 'coin_send': _normalizeCurrency(from), - 'coin_receive': _normalizeCurrency(to), - 'amount': amount.toString(), - 'ref': 'cake', - }; - - final uri = Uri.https(apiAuthority, getRate, params); - final response = await ProxyWrapper().post( - clearnetUri: uri, - body: json.encode(body), - headers: _headers, - ); - - final responseBody = json.decode(response.body) as Map; - - if (response.statusCode != 200) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: Exception('Unexpected http status: ${response.statusCode}'), - stackTrace: StackTrace.current, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'body': body, - 'url': uri.toString(), - }, - ); - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final data = responseBody['data'] as Map; - double rate = double.parse(data['price'].toString()); - final calculatedRate = rate > 0 ? isFixedRateMode ? amount / rate : rate / amount : 0.0; - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'fetchRate', - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'body': body, - 'url': uri.toString(), - }, - responseData: { - 'rate': rate, - 'calculatedRate': calculatedRate, - 'statusCode': response.statusCode, - 'responseBody': responseBody, - }, - ); - - return calculatedRate; - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: e, - stackTrace: s, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - printV("error fetching rate: ${e.toString()}"); - return 0.0; - } - } - - @override - Future createTrade({ - required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll, - }) async { - try { - final params = {}; - var body = { - 'coin_send': _normalizeCurrency(request.fromCurrency), - 'coin_send_network': _networkFor(request.fromCurrency), - 'coin_receive': _normalizeCurrency(request.toCurrency), - 'coin_receive_network': _networkFor(request.toCurrency), - 'amount_send': request.fromAmount, - 'recipient': request.toAddress, - 'ref': 'cake', - 'markup': double.tryParse(markup.toString()) ?? 0, - 'refund_address': request.refundAddress, - }; - - final uri = Uri.https(apiAuthority, createOrder, params); - final response = await ProxyWrapper().post( - clearnetUri: uri, - body: json.encode(body), - headers: _headers, - ); - - final responseBody = json.decode(response.body) as Map; - - if (response.statusCode == 400 || responseBody["success"] == false) { - final List errorsList = responseBody['errors'] as List? ?? []; - final error = errorsList.isNotEmpty - ? (errorsList[0]['msg'] as String?) ?? responseBody.toString() - : responseBody.toString(); - - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: TradeNotCreatedException(description, description: error), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'url': uri.toString(), - }, - ); - - throw TradeNotCreatedException(description, description: error); - } - - if (response.statusCode != 200) { - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: Exception('Unexpected http status: ${response.statusCode}'), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'url': uri.toString(), - }, - ); - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final responseData = responseBody['data'] as Map; - final receiveAmount = responseData["amount_receive"]?.toString(); - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'createTrade', - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'body': body, - 'url': uri.toString(), - }, - responseData: { - 'id': responseData["order_id"] as String, - 'inputAddress': responseData["server_address"] as String, - 'receiveAmount': receiveAmount, - 'statusCode': response.statusCode, - 'responseBody': responseBody, - }, - ); - - return Trade( - id: responseData["order_id"] as String, - inputAddress: responseData["server_address"] as String, - amount: request.fromAmount, - receiveAmount: receiveAmount ?? request.toAmount, - from: request.fromCurrency, - to: request.toCurrency, - provider: description, - createdAt: DateTime.now(), - state: TradeState.created, - payoutAddress: request.toAddress, - isSendAll: isSendAll, - ); - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: e, - stackTrace: s, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - }, - ); - printV("error creating trade: ${e.toString()}"); - throw TradeNotCreatedException(description, description: e.toString()); - } - } - - @override - Future findTradeById({required String id}) async { - try { - final params = {}; - var body = { - 'order_id': id, - }; - - final uri = Uri.https(apiAuthority, order, params); - final response = await ProxyWrapper().post( - clearnetUri: uri, - body: json.encode(body), - headers: _headers, - ); - - final responseBody = json.decode(response.body) as Map; - - if (response.statusCode == 400 || responseBody["success"] == false) { - final error = responseBody['errors'][0]['msg'] as String; - throw TradeNotCreatedException(description, description: error); - } - - if (response.statusCode != 200) - throw Exception('Unexpected http status: ${response.statusCode}'); - - final responseData = responseBody['data'] as Map; - final fromCurrency = responseData['coin_send'] as String; - final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency); - final toCurrency = responseData['coin_receive'] as String; - final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency); - final inputAddress = responseData['server_address'] as String; - final payoutAddress = responseData['recipient'] as String; - final status = responseData['status'] as String; - final state = TradeState.deserialize(raw: status); - final response_id = responseData['order_id'] as String; - final expectedSendAmount = responseData['amount_send'] as String; - final expectedReceiveAmount = responseData['amount_receive'] as String; - final memo = responseData['memo'] as String?; - final createdAt = responseData['created_at'] as String?; - - return Trade( - id: response_id, - from: from, - to: to, - provider: description, - inputAddress: inputAddress, - amount: expectedSendAmount, - payoutAddress: payoutAddress, - state: state, - receiveAmount: expectedReceiveAmount, - memo: memo, - createdAt: DateTime.tryParse(createdAt ?? ''), - ); - } catch (e) { - printV("error getting trade: ${e.toString()}"); - throw TradeNotFoundException( - id, - provider: description, - description: e.toString(), - ); - } - } - - String _normalizeCurrency(CryptoCurrency currency) { - switch (currency) { - default: - return currency.title.toUpperCase(); - } - } - - String _networkFor(CryptoCurrency currency) { - final network = switch (currency) { - CryptoCurrency.eth => 'ETH', - CryptoCurrency.bnb => 'BNB_BSC', - CryptoCurrency.usdterc20 => 'USDT_ERC20', - CryptoCurrency.usdttrc20 => 'TRX_USDT_S2UZ', - CryptoCurrency.usdtbsc => 'USDT_BSC', - CryptoCurrency.sol => 'SOL', - CryptoCurrency.btc => 'BTC', - CryptoCurrency.xmr => 'XMR', - CryptoCurrency.ltc => 'LTC', - CryptoCurrency.ada => 'ADA', - CryptoCurrency.bch => 'BCH', - CryptoCurrency.zec => 'ZEC', - _ => currency.title.toUpperCase(), - }; - return network; - } -} diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart deleted file mode 100644 index a27c50605..000000000 --- a/lib/exchange/provider/thorchain_exchange.provider.dart +++ /dev/null @@ -1,349 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:hash_wallet/utils/exchange_provider_logger.dart'; - -class ThorChainExchangeProvider extends ExchangeProvider { - ThorChainExchangeProvider(); - - static final isRefundAddressSupported = [CryptoCurrency.eth]; - - static const _baseNodeURL = 'thornode.ninerealms.com'; - static const _baseURL = 'midgard.ninerealms.com'; - static const _quotePath = '/thorchain/quote/swap'; - static const _txInfoPath = '/thorchain/tx/status/'; - static const _affiliateName = 'cakewallet'; - static const _affiliateBps = '175'; - static const _nameLookUpPath = 'v2/thorname/lookup/'; - - @override - String get title => 'THORChain'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => false; - - @override - ExchangeProviderDescription get description => ExchangeProviderDescription.thorChain; - - @override - Future checkIsAvailable() async => true; - - @override - Future fetchRate({ - required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount - }) async { - try { - if (amount == 0) return 0.0; - - final params = { - 'from_asset': _normalizeCurrency(from), - 'to_asset': _normalizeCurrency(to), - 'amount': _doubleToThorChainString(amount), - 'affiliate': _affiliateName, - 'affiliate_bps': _affiliateBps - }; - - final responseJSON = await _getSwapQuote(params); - - final expectedAmountOut = responseJSON['expected_amount_out'] as String? ?? '0.0'; - final rate = _thorChainAmountToDouble(expectedAmountOut) / amount; - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'fetchRate', - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - 'params': params, - }, - responseData: { - 'expectedAmountOut': expectedAmountOut, - 'rate': rate, - 'responseJSON': responseJSON, - }, - ); - - return rate; - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: e, - stackTrace: s, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - printV(e.toString()); - return 0.0; - } - } - - @override - Future fetchLimits( - {required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode}) async { - final params = { - 'from_asset': _normalizeCurrency(from), - 'to_asset': _normalizeCurrency(to), - 'amount': _doubleToThorChainString(1), - 'affiliate': _affiliateName, - 'affiliate_bps': _affiliateBps - }; - - final responseJSON = await _getSwapQuote(params); - final minAmountIn = responseJSON['recommended_min_amount_in'] as String? ?? '0.0'; - - return Limits(min: _thorChainAmountToDouble(minAmountIn)); - } - - @override - Future createTrade({ - required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll, - }) async { - - - final formattedFromAmount = double.parse(request.fromAmount); - - final params = { - 'from_asset': _normalizeCurrency(request.fromCurrency), - 'to_asset': _normalizeCurrency(request.toCurrency), - 'amount': _doubleToThorChainString(formattedFromAmount), - 'destination': _normalizeAddress(request.toAddress), - 'affiliate': _affiliateName, - 'affiliate_bps': _affiliateBps, - 'refund_address': - isRefundAddressSupported.contains(request.fromCurrency) ? _normalizeAddress(request.refundAddress) : '', - }; - - final responseJSON = await _getSwapQuote(params); - - final inputAddress = responseJSON['inbound_address'] as String?; - final memo = responseJSON['memo'] as String?; - final directAmountOutResponse = responseJSON['expected_amount_out'] as String?; - - String? receiveAmount; - if (directAmountOutResponse != null) { - receiveAmount = _thorChainAmountToDouble(directAmountOutResponse).toString(); - } - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'createTrade', - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'params': params, - }, - responseData: { - 'inputAddress': inputAddress, - 'memo': memo, - 'directAmountOutResponse': directAmountOutResponse, - 'receiveAmount': receiveAmount, - 'responseJSON': responseJSON, - }, - ); - - return Trade( - id: '', - from: request.fromCurrency, - to: request.toCurrency, - provider: description, - inputAddress: inputAddress, - createdAt: DateTime.now(), - amount: request.fromAmount, - receiveAmount: receiveAmount ?? request.toAmount, - state: TradeState.notFound, - payoutAddress: request.toAddress, - memo: memo, - isSendAll: isSendAll, - ); - } - - @override - Future findTradeById({required String id}) async { - if (id.isEmpty) throw Exception('Trade id is empty'); - final formattedId = id.startsWith('0x') ? id.substring(2) : id; - final uri = Uri.https(_baseNodeURL, '$_txInfoPath$formattedId'); - final response = await ProxyWrapper().get(clearnetUri: uri); - - - if (response.statusCode == 404) { - throw Exception('Trade not found for id: $formattedId'); - } else if (response.statusCode != 200) { - throw Exception('Unexpected HTTP status: ${response.statusCode}'); - } - - final responseJSON = json.decode(response.body); - final Map stagesJson = responseJSON['stages'] as Map; - - final inboundObservedStarted = stagesJson['inbound_observed']?['started'] as bool? ?? true; - if (!inboundObservedStarted) { - throw Exception('Trade has not started for id: $formattedId'); - } - - final currentState = _updateStateBasedOnStages(stagesJson) ?? TradeState.notFound; - - final tx = responseJSON['tx']; - final String fromAddress = tx['from_address'] as String? ?? ''; - final String toAddress = tx['to_address'] as String? ?? ''; - final List coins = tx['coins'] as List; - final String? memo = tx['memo'] as String?; - - final parts = memo?.split(':') ?? []; - - final String toChain = parts.length > 1 ? parts[1].split('.')[0] : ''; - final String toAsset = parts.length > 1 && parts[1].split('.').length > 1 - ? parts[1].split('.')[1].split('-')[0] - : ''; - - final formattedToChain = CryptoCurrency.safeParseCurrencyFromString(toChain); - final toAssetWithChain = CryptoCurrency.safeParseCurrencyFromString(toAsset, walletCurrency: formattedToChain); - - final plannedOutTxs = responseJSON['planned_out_txs'] as List?; - final isRefund = plannedOutTxs?.any((tx) => tx['refund'] == true) ?? false; - - return Trade( - id: id, - from: CryptoCurrency.fromString(tx['chain'] as String? ?? ''), - to: toAssetWithChain, - provider: description, - inputAddress: fromAddress, - payoutAddress: toAddress, - amount: coins.first['amount'] as String? ?? '0.0', - state: currentState, - memo: memo, - isRefund: isRefund, - ); - } - - static Future?>? lookupAddressByName(String name) async { - final uri = Uri.https(_baseURL, '$_nameLookUpPath$name'); - final response = await ProxyWrapper().get(clearnetUri: uri); - - if (response.statusCode != 200) { - return null; - } - - final body = json.decode(response.body) as Map; - final entries = body['entries'] as List?; - - if (entries == null || entries.isEmpty) { - return null; - } - - Map chainToAddressMap = {}; - - for (final entry in entries) { - final chain = entry['chain'] as String; - final address = entry['address'] as String; - chainToAddressMap[chain] = address; - } - - return chainToAddressMap; - } - - Future> _getSwapQuote(Map params) async { - Uri uri = Uri.https(_baseNodeURL, _quotePath, params); - - final response = await ProxyWrapper().get(clearnetUri: uri); - - if (response.statusCode != 200) { - ExchangeProviderLogger.logError( - provider: description, - function: '_getSwapQuote', - error: Exception('Unexpected HTTP status: ${response.statusCode}'), - stackTrace: StackTrace.current, - requestData: { - 'params': params, - 'url': uri.toString(), - }, - ); - throw Exception('Unexpected HTTP status: ${response.statusCode}'); - } - - if (response.body.contains('error')) { - ExchangeProviderLogger.logError( - provider: description, - function: '_getSwapQuote', - error: Exception('Unexpected response: ${response.body}'), - stackTrace: StackTrace.current, - requestData: { - 'params': params, - 'url': uri.toString(), - }, - ); - throw Exception('Unexpected response: ${response.body}'); - } - - return json.decode(response.body) as Map; - } - - String _normalizeCurrency(CryptoCurrency currency) { - final networkTitle = currency.tag == 'ETH' ? 'ETH' : currency.tag ?? currency.title; - return '$networkTitle.${currency.title}'; - } - - String _doubleToThorChainString(double amount) => (amount * 1e8).toInt().toString(); - - double _thorChainAmountToDouble(String amount) => double.parse(amount) / 1e8; - - TradeState? _updateStateBasedOnStages(Map stages) { - TradeState? currentState; - - if (stages['inbound_observed']['completed'] as bool? ?? false) { - currentState = TradeState.confirmation; - } - if (stages['inbound_confirmation_counted']['completed'] as bool? ?? false) { - currentState = TradeState.confirmed; - } - if (stages['inbound_finalised']['completed'] as bool? ?? false) { - currentState = TradeState.processing; - } - if (stages['swap_finalised']['completed'] as bool? ?? false) { - currentState = TradeState.traded; - } - if (stages['outbound_signed']['completed'] as bool? ?? false) { - currentState = TradeState.success; - } - - return currentState; - } - - String _normalizeAddress(String address) => - address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address; -} diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart deleted file mode 100644 index 5e5a1ddc3..000000000 --- a/lib/exchange/provider/xoswap_exchange_provider.dart +++ /dev/null @@ -1,565 +0,0 @@ -import 'dart:convert'; - -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/limits.dart'; -import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/trade.dart'; -import 'package:hash_wallet/exchange/trade_not_created_exception.dart'; -import 'package:hash_wallet/exchange/trade_request.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:hash_wallet/utils/package_info.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/utils/proxy_wrapper.dart'; -import 'package:hash_wallet/utils/exchange_provider_logger.dart'; -class XOSwapExchangeProvider extends ExchangeProvider { - XOSwapExchangeProvider() { - _addAppVersionHeader(); - } - - void _addAppVersionHeader() async { - final packageInfo = await PackageInfo.fromPlatform(); - final currentVersion = packageInfo.version; - _headers['App-Version'] = currentVersion; - } - - static const _apiAuthority = 'exchange.exodus.io'; - static const _apiPath = '/v3'; - static const _pairsPath = '/pairs'; - static const _ratePath = '/rates'; - static const _orders = '/orders'; - static const _assets = '/assets'; - - static final _headers = {'Content-Type': 'application/json', 'App-Name': 'cake-labs'}; - - final _networks = { - 'POL': 'matic', - 'ETH': 'ethereum', - 'BTC': 'bitcoin', - 'BSC': 'bsc', - 'SOL': 'solana', - 'TRX': 'tronmainnet', - 'ZEC': 'zcash', - 'ADA': 'cardano', - 'DOGE': 'dogecoin', - 'XMR': 'monero', - 'BCH': 'bcash', - 'BSV': 'bitcoinsv', - 'XRP': 'ripple', - 'LTC': 'litecoin', - 'EOS': 'eosio', - 'XLM': 'stellar', - 'BASE': 'basemainnet', - 'ARB': 'arbitrum', - }; - - static const supportedTags = [ - 'POL', - 'ETH', - 'BTC', - 'BSC', - 'SOL', - 'TRX', - 'ZEC', - 'ADA', - 'DOGE', - 'XMR', - 'BCH', - 'BSV', - 'XRP', - 'LTC', - 'EOS', - 'XLM', - 'BASE', - 'ARB', - ]; - - - String _normalizeXOSwapsNetwork(String string) { - final lower = string.toLowerCase(); - - if (lower.endsWith('matic0a883d9b')) return string.replaceFirst(RegExp(r'matic0a883d9b$', caseSensitive: false), 'POL'); - if (lower.endsWith('matic86e249c1')) return string.replaceFirst(RegExp(r'matic86e249c1$', caseSensitive: false), 'POL'); - if (lower.endsWith('bscddedf0f8')) return string.replaceFirst(RegExp(r'bscddedf0f8$', caseSensitive: false), 'BSC'); - if (lower.endsWith('basemainnetb5a52617')) return string.replaceFirst(RegExp(r'basemainnetb5a52617$', caseSensitive: false), 'BASE'); - - return string; - } - - @override - String get title => 'XOSwap'; - - @override - bool get isAvailable => true; - - @override - bool get isEnabled => true; - - @override - bool get supportsFixedRate => true; - - @override - ExchangeProviderDescription get description => ExchangeProviderDescription.xoSwap; - - @override - Future checkIsAvailable() async => true; - - Future _getAssets(CryptoCurrency currency) async { - if (currency.tag == null) return currency.title; - try { - final normalizedNetwork = _networks[currency.tag]; - if (normalizedNetwork == null) return null; - - final uri = Uri.https(_apiAuthority, _apiPath + _assets, - {'networks': normalizedNetwork, 'query': currency.title}); - - final response = await ProxyWrapper().get(clearnetUri: uri, headers: _headers); - - if (response.statusCode != 200) { - throw Exception('Failed to fetch assets for ${currency.title} on ${currency.tag}'); - } - - final decoded = jsonDecode(response.body); - if (decoded is! List) throw const FormatException('Unexpected response format'); - final assets = decoded.map((e) => Map.from(e as Map)).toList(); - - - final asset = assets.firstWhere( - (asset) => removeNonAlphanumeric((asset['symbol'] ?? '').toString()) == currency.title, - orElse: () => const {}, - ); - - return asset.isEmpty ? null : asset['id'] as String; - } catch (e) { - printV(e.toString()); - return null; - } - } - - String removeNonAlphanumeric(String str) => str.toUpperCase().replaceAll(RegExp(r'[^A-Z0-9]'), ''); - - Future> getRatesForPair({ - required CryptoCurrency from, - required CryptoCurrency to, - }) async { - try { - final curFrom = await _getAssets(from); - final curTo = await _getAssets(to); - if (curFrom == null || curTo == null) return []; - final pairId = curFrom + '_' + curTo; - final uri = Uri.https(_apiAuthority, '$_apiPath$_pairsPath/$pairId$_ratePath'); - final response = await ProxyWrapper().get(clearnetUri: uri, headers: _headers); - - if (response.statusCode != 200) return []; - return json.decode(response.body) as List; - } catch (e) { - printV(e.toString()); - return []; - } - } - - Future fetchLimits({ - required CryptoCurrency from, - required CryptoCurrency to, - required bool isFixedRateMode, - }) async { - try { - final rates = await getRatesForPair(from: from, to: to); - if (rates.isEmpty) throw Exception('No rates found for $from to $to'); - - double minLimit = double.infinity; - double maxLimit = 0; - - for (var rate in rates) { - final double currentMin = double.parse(rate['min']['value'].toString()); - final double currentMax = double.parse(rate['max']['value'].toString()); - if (currentMin < minLimit) minLimit = currentMin; - if (currentMax > maxLimit) maxLimit = currentMax; - } - return Limits(min: minLimit, max: maxLimit); - } catch (e) { - printV(e.toString()); - throw Exception('StealthEx failed to fetch limits'); - } - } - - @override - Future fetchRate({ - required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount - }) async { - try { - final rates = await getRatesForPair(from: from, to: to); - if (rates.isEmpty) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: Exception('No rates found for $from to $to'), - stackTrace: StackTrace.current, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - return 0; - } - - double result; - if (!isFixedRateMode) { - double bestOutput = 0.0; - for (var rate in rates) { - final double minVal = double.parse(rate['min']['value'].toString()); - final double maxVal = double.parse(rate['max']['value'].toString()); - if (amount >= minVal && amount <= maxVal) { - final double rateMultiplier = double.parse(rate['amount']['value'].toString()); - final double minerFee = double.parse(rate['minerFee']['value'].toString()); - final double outputAmount = (amount * rateMultiplier) - minerFee; - if (outputAmount > bestOutput) { - bestOutput = outputAmount; - } - } - } - result = bestOutput > 0 ? (bestOutput / amount) : 0; - } else { - double bestInput = double.infinity; - for (var rate in rates) { - final double rateMultiplier = double.parse(rate['amount']['value'].toString()); - final double minerFee = double.parse(rate['minerFee']['value'].toString()); - final double minVal = double.parse(rate['min']['value'].toString()); - final double maxVal = double.parse(rate['max']['value'].toString()); - final double requiredSend = (amount + minerFee) / rateMultiplier; - if (requiredSend >= minVal && requiredSend <= maxVal) { - if (requiredSend < bestInput) { - bestInput = requiredSend; - } - } - } - result = bestInput < double.infinity ? amount / bestInput : 0; - } - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'fetchRate', - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - responseData: { - 'result': result, - 'ratesCount': rates.length, - 'rates': rates, - }, - ); - - return result; - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'fetchRate', - error: e, - stackTrace: s, - requestData: { - 'from': from.title, - 'to': to.title, - 'amount': amount, - 'isFixedRateMode': isFixedRateMode, - 'isReceiveAmount': isReceiveAmount, - }, - ); - printV(e.toString()); - return 0; - } - } - - @override - Future createTrade({ - required TradeRequest request, - required bool isFixedRateMode, - required bool isSendAll, - }) async { - try { - final uri = Uri.https(_apiAuthority, '$_apiPath$_orders'); - - final curFrom = await _getAssets(request.fromCurrency); - final curTo = await _getAssets(request.toCurrency); - - if (curFrom == null || curTo == null) { - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: TradeNotCreatedException(description), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'curFrom': curFrom, - 'curTo': curTo, - }, - ); - throw TradeNotCreatedException(description); - } - - final pairId = curFrom + '_' + curTo; - - final payload = { - 'fromAmount': request.fromAmount, - 'fromAddress': request.refundAddress, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'pairId': pairId, - }; - - final response = await ProxyWrapper().post( - clearnetUri: uri, - headers: _headers, - body: json.encode(payload), - ); - - if (response.statusCode != 201) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['error'] ?? 'Unknown error'; - final message = responseJSON['message'] ?? ''; - - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: Exception('$error\n$message'), - stackTrace: StackTrace.current, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'payload': payload, - 'url': uri.toString(), - }, - ); - - throw Exception('$error\n$message'); - } - final responseJSON = json.decode(response.body) as Map; - - final amount = responseJSON['amount'] as Map; - final toAmount = responseJSON['toAmount'] as Map; - final orderId = responseJSON['id'] as String; - final from = request.fromCurrency; - final to = request.toCurrency; - final payoutAddress = responseJSON['toAddress'] as String; - final depositAddress = responseJSON['payInAddress'] as String; - final refundAddress = responseJSON['fromAddress'] as String; - final depositAmountStr = amount['value'].toString(); - final parsedAmount = double.tryParse(depositAmountStr); - - if (parsedAmount == null || parsedAmount <= 0) { - throw Exception('Invalid deposit amount received from API'); - } - - final receiveAmount = toAmount['value'] as String; - final status = responseJSON['status'] as String; - final createdAtString = responseJSON['createdAt'] as String; - final extraId = responseJSON['payInAddressTag'] as String?; - - final createdAt = DateTime.parse(createdAtString).toLocal(); - - ExchangeProviderLogger.logSuccess( - provider: description, - function: 'createTrade', - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - 'payload': payload, - 'url': uri.toString(), - }, - responseData: { - 'orderId': orderId, - 'depositAddress': depositAddress, - 'payoutAddress': payoutAddress, - 'refundAddress': refundAddress, - 'depositAmount': depositAmountStr, - 'receiveAmount': receiveAmount, - 'status': status, - 'createdAt': createdAtString, - 'extraId': extraId, - 'statusCode': response.statusCode, - 'responseJSON': responseJSON, - }, - ); - - return Trade( - id: orderId, - from: from, - to: to, - provider: description, - inputAddress: depositAddress, - refundAddress: refundAddress, - state: TradeState.deserialize(raw: status), - createdAt: createdAt, - amount: depositAmountStr, - receiveAmount: receiveAmount.toString(), - payoutAddress: payoutAddress, - extraId: extraId, - isSendAll: isSendAll, - ); - } catch (e, s) { - ExchangeProviderLogger.logError( - provider: description, - function: 'createTrade', - error: e, - stackTrace: s, - requestData: { - 'from': request.fromCurrency.title, - 'to': request.toCurrency.title, - 'fromAmount': request.fromAmount, - 'toAmount': request.toAmount, - 'toAddress': request.toAddress, - 'refundAddress': request.refundAddress, - 'isFixedRateMode': isFixedRateMode, - 'isSendAll': isSendAll, - }, - ); - printV(e.toString()); - throw TradeNotCreatedException(description); - } - } - - @override - Future findTradeById({required String id}) async { - try { - final uri = Uri.https(_apiAuthority, '$_apiPath$_orders/$id'); - final response = await ProxyWrapper().get(clearnetUri: uri, headers: _headers); - - if (response.statusCode != 200) { - final responseJSON = json.decode(response.body) as Map; - if (responseJSON.containsKey('code') && responseJSON['code'] == 'NOT_FOUND') { - throw Exception('Trade not found'); - } - final error = responseJSON['error'] ?? 'Unknown error'; - final message = responseJSON['message'] ?? responseJSON['details'] ?? ''; - throw Exception('$error\n$message'); - } - final responseJSON = json.decode(response.body) as Map; - - final pairId = responseJSON['pairId'] as String; - final pairParts = pairId.split('_'); - final fromAsset = pairParts.isNotEmpty ? pairParts[0] : ''; - final normalizedFromAsset = _normalizeXOSwapsNetwork(fromAsset); - String? fromAssetTag = _extractTagFromAsset(normalizedFromAsset); - - String fromAssetBase = fromAssetTag != null - ? normalizedFromAsset.substring(0, normalizedFromAsset.length - fromAssetTag.length) - : normalizedFromAsset; - - // Special case for USDT defaulting to ETH tag - if (fromAssetBase == 'USDT' && fromAssetTag == null) { - fromAssetTag = 'ETH'; - } - - // Special case for BASE defaulting to BASE tag - if (fromAssetBase == 'BASE' && fromAssetTag == null) { - fromAssetTag = 'BASE'; - fromAssetBase = 'ETH'; - } - - final toAsset = pairParts.length > 1 ? pairParts[1] : ''; - final normalizedToAsset = _normalizeXOSwapsNetwork(toAsset); - String? toAssetTag = _extractTagFromAsset(normalizedToAsset); - - String toAssetBase = toAssetTag != null - ? normalizedToAsset.substring(0, normalizedToAsset.length - toAssetTag.length) - : normalizedToAsset; - - // Special case for USDT defaulting to ETH tag - if (toAssetBase == 'USDT' && toAssetTag == null) { - toAssetTag = 'ETH'; - } - - // Special case for BASE defaulting to BASE tag - if (toAssetBase == 'BASE' && toAssetTag == null) { - toAssetTag = 'ETH'; - toAssetBase = 'BASE'; - } - - final fromCurrency = CryptoCurrency.safeParseCurrencyFromString(fromAssetBase,tag: fromAssetTag); - final toCurrency = CryptoCurrency.safeParseCurrencyFromString(toAssetBase,tag: toAssetTag); - - - final amount = responseJSON['amount'] as Map; - final toAmount = responseJSON['toAmount'] as Map; - final orderId = responseJSON['id'] as String; - final depositAmountStr = amount['value'].toString(); - final parsedAmount = double.tryParse(depositAmountStr); - - if (parsedAmount == null || parsedAmount <= 0) { - throw Exception('Invalid deposit amount received from API'); - } - - final receiveAmount = toAmount['value'] as String; - final depositAddress = responseJSON['payInAddress'] as String; - final payoutAddress = responseJSON['toAddress'] as String; - final refundAddress = responseJSON['fromAddress'] as String; - final status = responseJSON['status'] as String; - final createdAtString = responseJSON['createdAt'] as String; - final createdAt = DateTime.parse(createdAtString).toLocal(); - final extraId = responseJSON['payInAddressTag'] as String?; - - return Trade( - id: orderId, - from: fromCurrency, - to: toCurrency, - provider: description, - inputAddress: depositAddress, - refundAddress: refundAddress, - state: TradeState.deserialize(raw: status), - createdAt: createdAt, - amount: depositAmountStr, - receiveAmount: receiveAmount, - payoutAddress: payoutAddress, - extraId: extraId, - ); - } catch (e) { - printV(e.toString()); - throw TradeNotCreatedException(description); - } - } - - // ensure something remains before tag (at least 2 chars) - String? _extractTagFromAsset(String asset) { - - - - for (final tag in supportedTags) { - if (asset.endsWith(tag)) { - final prefixLength = asset.length - tag.length; - if (prefixLength >= 2) { - return tag; - } - } - } - return null; - } -} diff --git a/lib/new-ui/pages/swap_page.dart b/lib/new-ui/pages/swap_page.dart index f5f003037..cfcc0adbf 100644 --- a/lib/new-ui/pages/swap_page.dart +++ b/lib/new-ui/pages/swap_page.dart @@ -5,8 +5,6 @@ import 'package:hash_wallet/di.dart'; import 'package:hash_wallet/entities/parse_address_from_domain.dart'; import 'package:hash_wallet/entities/qr_scanner.dart'; import 'package:hash_wallet/exchange/exchange_trade_state.dart'; -import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/new-ui/widgets/keyboard_hide_overlay.dart'; import 'package:hash_wallet/new-ui/widgets/modern_button.dart'; @@ -281,14 +279,7 @@ class _NewSwapPageState extends State { if (depositAmountController.text != widget.exchangeViewModel.depositAmount && depositAmountController.text != S.of(context).all) { widget.exchangeViewModel.isSendAllEnabled = false; - final isThorChain = widget.exchangeViewModel.selectedProviders - .any((provider) => provider is ThorChainExchangeProvider); - final isChainflip = widget.exchangeViewModel.selectedProviders - .any((provider) => provider is ChainflipExchangeProvider); - - _depositAmountDebounce = isThorChain || isChainflip - ? Debounce(Duration(milliseconds: 1000)) - : Debounce(Duration(milliseconds: 500)); + _depositAmountDebounce = Debounce(Duration(milliseconds: 500)); _depositAmountDebounce.run(() { widget.exchangeViewModel.calculateBestRate(); diff --git a/lib/new-ui/widgets/coins_page/assets_history/history_order_tile.dart b/lib/new-ui/widgets/coins_page/assets_history/history_order_tile.dart index 8ca7da8a0..0a6984dac 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/history_order_tile.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/history_order_tile.dart @@ -32,7 +32,7 @@ class HistoryOrderTile extends StatelessWidget { date: date, amount: amount, amountFiat: amountFiat, - leadingIcon:Image.asset("assets/images/cakepay.png"), + leadingIcon: Image.asset("assets/images/card.png"), roundedTop: roundedTop, roundedBottom: roundedBottom, bottomSeparator: bottomSeparator, diff --git a/lib/order/order_provider_adapter/cake_pay_order_provider_adapter.dart b/lib/order/order_provider_adapter/cake_pay_order_provider_adapter.dart deleted file mode 100644 index d94ee356c..000000000 --- a/lib/order/order_provider_adapter/cake_pay_order_provider_adapter.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart'; -import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:hash_wallet/order/order.dart'; -import 'package:hash_wallet/order/order_provider.dart'; -import 'package:hash_wallet/order/order_provider_description.dart'; -import 'package:hash_wallet/order/order_source_description.dart'; -import 'package:cw_core/wallet_base.dart'; - -class CakePayOrderProviderAdapter implements OrderProvider { - CakePayOrderProviderAdapter({ - required this.wallet, - required this.cakePayService, - }); - - final WalletBase wallet; - final CakePayService cakePayService; - - @override - String get title => 'Cake Pay'; - - @override - String get trackUrl => ''; - - @override - Future<(Order, Object?)> findOrderById(String id) async { - final cakePayOrder = await cakePayService.findOrderById(orderId: id); - final cards = cakePayOrder.cards; - final order = Order( - id: cakePayOrder.orderId, - state: determineState(cakePayOrder.status), - transferId: '', - createdAt: DateTime.now(), - amount: '', - receiveAmount: cakePayOrder.totalReceiveAmount, - quantity: cakePayOrder.quantity.toString(), - receiveAddress: '', - walletId: wallet.id, - from: '', - to: cakePayOrder.fiatCurrencyCode, - source: OrderSourceDescription.order, - giftCardProvider: OrderProviderDescription.cakePay, - ); - return (order, cards); - } - - TradeState determineState(String state) { - final swapState = switch (state) { - 'new' => TradeState.pending, - 'expired_but_still_pending' => TradeState.overdue, - 'expired' => TradeState.expired, - 'failed' => TradeState.failed, - 'paid' => TradeState.paid, - 'paid_partial' => TradeState.underpaid, - 'pending_purchase' => TradeState.processing, - 'purchase_processing' => TradeState.processing, - 'purchased' => TradeState.confirmed, - 'pending_email' => TradeState.awaiting, - 'complete' => TradeState.complete, - 'pending_refund' => TradeState.refund, - 'refunded' => TradeState.refunded, - _ => TradeState.notFound - }; - - return swapState; - } -} diff --git a/lib/router.dart b/lib/router.dart index 608cc6ab4..5c6c27f17 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -30,7 +30,6 @@ import 'package:hash_wallet/src/screens/buy/buy_sell_options_page.dart'; import 'package:hash_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:hash_wallet/src/screens/buy/payment_method_options_page.dart'; import 'package:hash_wallet/src/screens/buy/webview_page.dart'; -import 'package:hash_wallet/cake_pay/cake_pay.dart'; import 'package:hash_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:hash_wallet/src/screens/connect_device/monero_hardware_wallet_options_page.dart'; import 'package:hash_wallet/src/screens/connect_device/select_device_manufacturer_page.dart'; @@ -778,37 +777,10 @@ Route createRoute(RouteSettings settings) { param1: settings.arguments as QrViewData, )); - case Routes.cakePayCardsPage: - return handleRouteWithPlatformAwareness( - (context) => getIt.get(), - ); - case Routes.aboutPage: final page = getIt.get(); return handleRouteWithPlatformAwareness((context) => page); - case Routes.cakePayBuyCardPage: - final args = settings.arguments as List; - return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: args), - ); - - case Routes.cakePayWelcomePage: - return handleRouteWithPlatformAwareness( - (context) => getIt.get(), - ); - - case Routes.cakePayVerifyOtpPage: - final args = settings.arguments as List; - return handleRouteWithPlatformAwareness( - (context) => getIt.get(param1: args), - ); - - case Routes.cakePayAccountPage: - return handleRouteWithPlatformAwareness( - (context) => getIt.get(), - ); - case Routes.webViewPage: final args = settings.arguments as List; final title = args.first as String; diff --git a/lib/routes.dart b/lib/routes.dart index 96ac8264d..6290dd247 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -72,12 +72,6 @@ class Routes { static const unspentCoinsDetails = '/unspent_coins_details'; static const addressPage = '/address_page'; static const fullscreenQR = '/fullscreen_qr'; - static const cakePayWelcomePage = '/cake_pay_welcome_page'; - static const cakePayLoginPage = '/cake_pay_login_page'; - static const cakePayCardsPage = '/cake_pay_cards_page'; - static const cakePayBuyCardPage = '/cake_pay_buy_card_page'; - static const cakePayVerifyOtpPage = '/cake_pay_verify_otp_page'; - static const cakePayAccountPage = '/cake_pay_account_page'; static const webViewPage = '/web_view_page'; static const silentPaymentsSettings = '/silent_payments_settings'; static const silentPaymentsLogs = '/silent_payments_logs'; diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 7beefcf89..394f40b0b 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -14,7 +14,6 @@ import 'package:hash_wallet/src/widgets/vulnerable_seeds_popup.dart'; import 'package:hash_wallet/utils/device_info.dart'; import 'package:hash_wallet/utils/feature_flag.dart'; import 'package:hash_wallet/utils/version_comparator.dart'; -import 'package:hash_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/routes.dart'; import 'package:hash_wallet/src/screens/yat_emoji_id.dart'; @@ -327,7 +326,6 @@ class _DashboardPageView extends BasePage { label: S.of(context).apps, child: CakeFeaturesPage( dashboardViewModel: dashboardViewModel, - cakeFeaturesViewModel: getIt.get(), ), ), ); diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart index 161d7fac8..20c0f793b 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_dashboard_actions.dart @@ -1,9 +1,7 @@ -import 'package:hash_wallet/di.dart'; import 'package:hash_wallet/entities/main_actions.dart'; import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_action_button.dart'; import 'package:hash_wallet/src/screens/dashboard/pages/cake_features_page.dart'; import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:hash_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -75,7 +73,6 @@ class DesktopDashboardActions extends StatelessWidget { Expanded( child: CakeFeaturesPage( dashboardViewModel: dashboardViewModel, - cakeFeaturesViewModel: getIt.get(), ), ), ], diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart index f622d9d51..001056b6d 100644 --- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -7,10 +7,8 @@ import 'package:hash_wallet/src/screens/dashboard/widgets/info_card.dart'; import 'package:hash_wallet/src/widgets/alert_with_one_action.dart'; import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:hash_wallet/src/widgets/dashboard_card_widget.dart'; -import 'package:hash_wallet/src/widgets/introducing_card.dart'; import 'package:hash_wallet/src/widgets/standard_switch.dart'; import 'package:hash_wallet/themes/core/theme_extension.dart'; -import 'package:hash_wallet/utils/feature_flag.dart'; import 'package:hash_wallet/utils/show_pop_up.dart'; import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/wallet_info.dart'; @@ -139,18 +137,6 @@ class CryptoBalanceWidget extends StatelessWidget { ), ], )), - Observer( - builder: (_) { - if (dashboardViewModel.balanceViewModel.isShowCard && FeatureFlag.isCakePayEnabled) { - return IntroducingCard( - title: S.of(context).introducing_cake_pay, - subTitle: S.of(context).cake_pay_learn_more, - borderColor: Theme.of(context).colorScheme.outline, - closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard); - } - return Container(); - }, - ), Observer(builder: (_) { if (!dashboardViewModel.showRepWarning) { return const SizedBox(); diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index d33998c2d..02b69ca8b 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -1,24 +1,17 @@ -import 'dart:io'; - import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/new-ui/widgets/apps_widget.dart'; -import 'package:hash_wallet/routes.dart'; import 'package:hash_wallet/src/widgets/hash_image_widget.dart'; import 'package:hash_wallet/src/widgets/dashboard_card_widget.dart'; import 'package:hash_wallet/utils/feature_flag.dart'; -import 'package:hash_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:hash_wallet/src/widgets/gradient_background.dart'; class CakeFeaturesPage extends StatelessWidget { - CakeFeaturesPage({required this.dashboardViewModel, required this.cakeFeaturesViewModel}); + CakeFeaturesPage({required this.dashboardViewModel}); final DashboardViewModel dashboardViewModel; - final CakeFeaturesViewModel cakeFeaturesViewModel; @override Widget build(BuildContext context) { @@ -102,23 +95,4 @@ class CakeFeaturesPage extends StatelessWidget { ); } - void _onCakePayTap(BuildContext context) { - if (Platform.isMacOS) { - _launchUrl("buy.cakepay.com"); - } else { - _navigatorToGiftCardsPage(context); - } - } - - void _launchUrl(String url) { - try { - launchUrl(Uri.https(url), mode: LaunchMode.externalApplication); - } catch (e) { - printV(e); - } - } - - void _navigatorToGiftCardsPage(BuildContext context) { - Navigator.pushNamed(context, Routes.cakePayCardsPage); - } } \ No newline at end of file diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 6c95977ac..e479ba271 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -1,6 +1,4 @@ import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:hash_wallet/core/auth_service.dart'; import 'package:hash_wallet/di.dart'; import 'package:hash_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart'; @@ -542,14 +540,7 @@ class ExchangePage extends BasePage { if (depositAmountController.text != exchangeViewModel.depositAmount && depositAmountController.text != S.of(context).all) { exchangeViewModel.isSendAllEnabled = false; - final isThorChain = exchangeViewModel.selectedProviders - .any((provider) => provider is ThorChainExchangeProvider); - final isChainflip = exchangeViewModel.selectedProviders - .any((provider) => provider is ChainflipExchangeProvider); - - _depositAmountDebounce = isThorChain || isChainflip - ? Debounce(Duration(milliseconds: 1000)) - : Debounce(Duration(milliseconds: 500)); + _depositAmountDebounce = Debounce(Duration(milliseconds: 500)); _depositAmountDebounce.run(() { exchangeViewModel.calculateBestRate(); diff --git a/lib/src/screens/order_details/cake_pay_detail_list_card_item.dart b/lib/src/screens/order_details/cake_pay_detail_list_card_item.dart deleted file mode 100644 index f998ee63f..000000000 --- a/lib/src/screens/order_details/cake_pay_detail_list_card_item.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart'; -import 'package:hash_wallet/src/screens/transaction_details/standart_list_item.dart'; -import 'package:flutter/material.dart'; - -class CakePayDetailsListCardItem extends StandartListItem { - CakePayDetailsListCardItem({ - required String title, - required String value, - required this.id, - required this.createdAt, - this.price, - this.quantity, - required this.from, - required this.to, - required this.onTap, - required this.cards, - }) : super(title: title, value: value); - - final String id; - final String createdAt; - final String? price; - final String? quantity; - final String from; - final String to; - final void Function(BuildContext) onTap; - final List cards; -} diff --git a/lib/src/screens/order_details/order_details_page.dart b/lib/src/screens/order_details/order_details_page.dart index 3a1cec756..282917aaa 100644 --- a/lib/src/screens/order_details/order_details_page.dart +++ b/lib/src/screens/order_details/order_details_page.dart @@ -1,6 +1,4 @@ -import 'package:hash_wallet/src/screens/order_details/cake_pay_detail_list_card_item.dart'; import 'package:hash_wallet/src/screens/trade_details/trade_details_status_item.dart'; -import 'package:hash_wallet/src/widgets/cake_pay_order_list_card.dart'; import 'package:hash_wallet/src/widgets/standard_list.dart'; import 'package:hash_wallet/src/widgets/standard_list_status_row.dart'; import 'package:hash_wallet/themes/core/material_base_theme.dart'; @@ -64,21 +62,6 @@ class OrderDetailsPageBodyState extends State { return StandardListStatusRow(title: item.title, value: item.value); } - if (item is CakePayDetailsListCardItem) { - final cardImagePath = (item.cards.isNotEmpty) ? item.cards.first.cardImagePath : null; - - return CakePayOrderListCard( - id: item.id, - create: item.createdAt, - price: item.price, - quantity: item.quantity, - pair: '${item.from} → ${item.to}', - currentTheme: widget.currentTheme.type, - onTap: item.onTap, - backgroundImage: cardImagePath, - ); - } - if (item is TrackTradeListItem) { return GestureDetector( onTap: item.onTap, diff --git a/lib/src/widgets/bottom_sheet/cake_pay_card_info_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/cake_pay_card_info_bottom_sheet_widget.dart deleted file mode 100644 index e947c69da..000000000 --- a/lib/src/widgets/bottom_sheet/cake_pay_card_info_bottom_sheet_widget.dart +++ /dev/null @@ -1,338 +0,0 @@ -import 'package:hash_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/flip_card_widget.dart'; -import 'package:hash_wallet/cake_pay/src/widgets/link_extractor.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/utils/image_utill.dart'; -import 'package:hash_wallet/utils/show_pop_up.dart'; -import 'package:flutter/material.dart'; - -import 'base_bottom_sheet_widget.dart'; - -class CakePayCardInfoBottomSheet extends BaseBottomSheet { - CakePayCardInfoBottomSheet({ - required String titleText, - required FooterType footerType, - String? titleIconPath, - String? singleActionButtonText, - VoidCallback? onSingleActionButtonPressed, - Key? singleActionButtonKey, - String? doubleActionLeftButtonText, - String? doubleActionRightButtonText, - VoidCallback? onLeftActionButtonPressed, - VoidCallback? onRightActionButtonPressed, - Key? rightActionButtonKey, - Key? leftActionButtonKey, - required this.onUpdateBalancePressed, - required this.isReloadable, - required this.balance, - this.contentImage, - this.howToUse, - this.applyBoxShadow = false, - Key? key, - }) : super( - titleText: titleText, - maxHeight: 900, - titleIconPath: titleIconPath, - footerType: footerType, - singleActionButtonText: singleActionButtonText, - onSingleActionButtonPressed: onSingleActionButtonPressed, - singleActionButtonKey: singleActionButtonKey, - doubleActionLeftButtonText: doubleActionLeftButtonText, - doubleActionRightButtonText: doubleActionRightButtonText, - onLeftActionButtonPressed: onLeftActionButtonPressed, - onRightActionButtonPressed: onRightActionButtonPressed, - leftActionButtonKey: leftActionButtonKey, - rightActionButtonKey: rightActionButtonKey, - key: key); - - final VoidCallback onUpdateBalancePressed; - final String? contentImage; - final String? howToUse; - final bool isReloadable; - final String balance; - final bool applyBoxShadow; - - final _cardKey = GlobalKey(); - - @override - Widget contentWidget(BuildContext context) { - final itemTitleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 16, - fontWeight: FontWeight.w500, - decoration: TextDecoration.none, - ); - final itemSubTitleTextStyle = Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurfaceVariant, - decoration: TextDecoration.none, - ); - - final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer; - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (contentImage != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 16), - child: AspectRatio( - aspectRatio: 1.6, - child: FlipCard( - key: _cardKey, - flipOnTouch: true, - front: _buildCardImage(context, contentImage!, applyBoxShadow), - back: _buildBarcodeSide(context, - cardNumber: '6006491979836784204', pin: '4782'), - ), - ), - ), - Positioned( - bottom: 0, - right: 0, - child: GestureDetector( - onTap: () => _cardKey.currentState?.toggleCard(), - child: Container( - height: 43, - width: 43, - decoration: BoxDecoration( - border: Border.all(color: Colors.white, width: 1), - color: Colors.white.withAlpha(75), - shape: BoxShape.circle, - ), - child: Transform.scale( - scale: .8, - child: const ImageIcon( - AssetImage('assets/images/transfer.png'), - color: Colors.white, - ), - )), - ), - ), - ], - ), - ), - Container(), - Text( - 'Tap card to show details', - style: itemTitleTextStyle.copyWith(fontSize: 12, fontWeight: FontWeight.w700), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - children: [ - const SizedBox(height: 34), - CakePayInfoTile( - isReloadable: isReloadable, - itemValue: balance, - itemTitleTextStyle: itemTitleTextStyle, - itemSubTitleTextStyle: itemSubTitleTextStyle, - tileBackgroundColor: tileBackgroundColor), - const SizedBox(height: 8), - _HowToUseTile(howToUse: howToUse ?? '', tileBackgroundColor: tileBackgroundColor), - const SizedBox(height: 40), - ], - ), - ), - ], - ); - } -} - -class _HowToUseTile extends StatelessWidget { - const _HowToUseTile({required this.howToUse, required this.tileBackgroundColor}); - - final String howToUse; - final Color tileBackgroundColor; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: () => _showHowToUseCard(context: context, howToUse: howToUse), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - decoration: - BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(S.of(context).how_to_use_card, style: Theme.of(context).textTheme.bodyLarge), - Icon( - Icons.chevron_right_rounded, - color: Theme.of(context).textTheme.titleLarge!.color!, - ), - ], - )), - ); - } -} - -class CakePayInfoTile extends StatelessWidget { - const CakePayInfoTile({ - super.key, - required this.isReloadable, - required this.itemValue, - required this.itemTitleTextStyle, - this.itemSubTitle, - required this.itemSubTitleTextStyle, - required this.tileBackgroundColor, - }); - - final bool isReloadable; - final String itemValue; - final TextStyle itemTitleTextStyle; - final String? itemSubTitle; - final TextStyle itemSubTitleTextStyle; - final Color tileBackgroundColor; - - @override - Widget build(BuildContext context) { - return Semantics( - container: true, - label: isReloadable ? 'Balance' : 'Total Value', - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - decoration: - BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(isReloadable ? 'Balance' : 'Total Value', style: itemTitleTextStyle), - Text(itemValue, - style: itemTitleTextStyle.copyWith(fontSize: 18, fontWeight: FontWeight.w600)), - ], - ), - const SizedBox(height: 14), - Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - child: Text(isReloadable ? 'Top Up Balance' : 'Manually update Balance', - style: itemTitleTextStyle.copyWith(fontSize: 12, fontWeight: FontWeight.w600)), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18), - color: Theme.of(context).dialogBackgroundColor)), - const SizedBox(height: 4), - ], - ), - ), - ); - } -} - -void _showHowToUseCard({required BuildContext context, String? howToUse}) { - showPopUp( - context: context, - builder: (BuildContext context) { - return CakePayAlertModal( - title: S.of(context).how_to_use_card, - content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClickableLinksText( - text: howToUse ?? '', - textStyle: Theme.of(context).textTheme.bodyMedium!, - linkStyle: TextStyle( - color: Theme.of(context).textTheme.titleLarge!.color!, - fontSize: 18, - fontStyle: FontStyle.italic, - fontWeight: FontWeight.w400, - ), - ), - ]), - actionTitle: S.current.got_it, - ); - }); -} - -Widget _buildCardImage(BuildContext ctx, String path, bool addShadow) { - final border = BorderRadius.circular(10); - - return Container( - decoration: addShadow - ? BoxDecoration( - borderRadius: border, - boxShadow: [BoxShadow(color: Colors.black.withAlpha(150), blurRadius: 5)], - ) - : null, - child: ClipRRect( - borderRadius: border, - child: Stack( - fit: StackFit.expand, - children: [ - Container(color: Theme.of(ctx).cardColor.withAlpha(200)), - ImageUtil.getImageFromPath( - imagePath: path, - fit: BoxFit.cover, - ), - ], - ), - ), - ); -} - -Widget _buildBarcodeSide(BuildContext context, {required String cardNumber, required String pin}) => - SizedBox.expand( - child: Container( - padding: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 34), - decoration: BoxDecoration( - color: Theme.of(context).cardColor.withAlpha(200), - borderRadius: BorderRadius.circular(10), - boxShadow: [BoxShadow(color: Colors.black.withAlpha(150), blurRadius: 5)], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - color: Theme.of(context).textTheme.titleLarge!.color!.withOpacity(.1), - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Gift Card Number', - style: TextStyle( - fontFamily: 'Lato', - fontSize: 10, - fontWeight: FontWeight.w500, - color: Color.fromRGBO(207, 207, 207, 1))), - const SizedBox(height: 4), - Text(cardNumber, - style: TextStyle( - fontFamily: 'Lato', - fontSize: 12, - fontWeight: FontWeight.w900, - color: Color.fromRGBO(146, 146, 146, 1))), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('PIN Number', - style: TextStyle( - fontFamily: 'Lato', - fontSize: 10, - fontWeight: FontWeight.w500, - color: Color.fromRGBO(207, 207, 207, 1))), - const SizedBox(height: 4), - Text(pin, - style: TextStyle( - fontFamily: 'Lato', - fontSize: 12, - fontWeight: FontWeight.w900, - color: Color.fromRGBO(146, 146, 146, 1))), - ], - ), - ], - ), - ], - ), - ), - ); diff --git a/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart b/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart deleted file mode 100644 index eb871d16f..000000000 --- a/lib/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart +++ /dev/null @@ -1,315 +0,0 @@ -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/src/widgets/primary_button.dart'; -import 'package:hash_wallet/src/widgets/rounded_icon_button.dart'; -import 'package:hash_wallet/utils/image_utill.dart'; -import 'package:hash_wallet/view_model/send/output.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class CakePayTransactionSentBottomSheet extends StatelessWidget { - const CakePayTransactionSentBottomSheet({ - super.key, - required this.titleText, - required this.currency, - required this.amount, - required this.amountValue, - required this.fiatAmountValue, - required this.output, - required this.fee, - required this.feeValue, - required this.feeFiatAmount, - this.titleIconWidget, - required this.quantity, - required this.onClose, - required this.paymentId, - required this.paymentIdValue, - }); - - final String titleText; - final Widget? titleIconWidget; - final CryptoCurrency currency; - final String amount; - final String amountValue; - final String fiatAmountValue; - final Output output; - final String fee; - final String feeValue; - final String feeFiatAmount; - final String quantity; - final VoidCallback onClose; - final String paymentId; - final String paymentIdValue; - - TextStyle _titleStyle(BuildContext ctx) => - Theme - .of(ctx) - .textTheme - .bodyLarge!; - - TextStyle _valueStyle(BuildContext ctx) => - Theme - .of(ctx) - .textTheme - .titleMedium! - .copyWith( - fontSize: 18, - fontWeight: FontWeight.w600, - ); - - TextStyle _subStyle(BuildContext ctx) => - Theme - .of(ctx) - .textTheme - .labelSmall! - .copyWith( - color: Theme - .of(ctx) - .colorScheme - .onSurfaceVariant, - ); - - Widget _buildHeader(BuildContext ctx) => - Column( - children: [ - const SizedBox(height: 12), - Container( - width: 64, - height: 5, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Theme - .of(ctx) - .colorScheme - .onSurface, - ), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (titleIconWidget != null) titleIconWidget!, - const SizedBox(width: 6), - Text( - titleText, - style: Theme - .of(ctx) - .textTheme - .titleLarge! - .copyWith( - fontSize: 20, - fontWeight: FontWeight.w600, - decoration: TextDecoration.none, - ), - ), - ], - ), - const SizedBox(height: 12), - ], - ); - - Widget _buildBody(BuildContext context) => - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - children: [ - _StandardTile( - itemTitle: amount, - titleStyle: _titleStyle(context), - itemValue: '$amountValue ${currency.title}', - itemValueStyle: _valueStyle(context), - itemSubTitle: fiatAmountValue, - itemSubTitleStyle: _subStyle(context), - ), - const SizedBox(height: 8), - _StandardTile( - itemTitle: fee, - titleStyle: _titleStyle(context), - itemValue: feeValue, - itemValueStyle: _valueStyle(context), - itemSubTitle: feeFiatAmount, - itemSubTitleStyle: _subStyle(context), - ), - const SizedBox(height: 8), - _StandardTile( - itemTitle: output.parsedAddress.profileName, - titleStyle: _titleStyle(context), - itemValue: output.fiatAmount, - itemValueStyle: _valueStyle(context), - itemSubTitle: quantity, - itemSubTitleStyle: _subStyle(context), - imagePath: output.parsedAddress.profileImageUrl, - ), - const SizedBox(height: 8), - _StandardTile( - itemTitle: paymentId + ': \n' + paymentIdValue, - titleStyle: _titleStyle(context).copyWith( - fontSize: 14 - ), - itemValue: '', - itemValueStyle: _valueStyle(context), - copyButton: true, - copyButtonOnPressed: () async => - await Clipboard.setData(ClipboardData(text: paymentIdValue)), - ), - - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Column( - children: [ - const SizedBox(height: 18), - Image.asset('assets/images/envelope.png'), - const SizedBox(height: 18), - Text( - S - .of(context) - .cake_pay_card_email_delivered_message, - style: Theme - .of(context) - .textTheme - .bodyLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - PrimaryButton( - text: S - .of(context) - .close, - color: Theme - .of(context) - .colorScheme - .primary, - textColor: Theme - .of(context) - .colorScheme - .onPrimaryContainer, - onPressed: onClose, - ), - ], - ), - ), - ], - ), - ); - - @override - Widget build(BuildContext context) { - final maxHeight = MediaQuery - .of(context) - .size - .height * 0.9; - - return ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), - child: Material( - color: Theme - .of(context) - .colorScheme - .surface, - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: maxHeight), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildHeader(context), - _buildBody(context), - ], - ), - ), - ), - ), - ); - } -} - -class _StandardTile extends StatelessWidget { - const _StandardTile({ - required this.itemTitle, - required this.titleStyle, - required this.itemValue, - required this.itemValueStyle, - this.itemSubTitle, - this.itemSubTitleStyle, - this.imagePath, - this.copyButton = false, - this.copyButtonOnPressed, - }); - - final String itemTitle; - final TextStyle titleStyle; - final String itemValue; - final TextStyle itemValueStyle; - final String? itemSubTitle; - final TextStyle? itemSubTitleStyle; - final String? imagePath; - final bool copyButton; - final VoidCallback? copyButtonOnPressed; - - @override - Widget build(BuildContext context) { - return Semantics( - container: true, - label: itemTitle, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme - .of(context) - .colorScheme - .surfaceContainerLowest - .withAlpha(80)), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Row( - mainAxisAlignment: copyButton ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start, - children: [ - if (imagePath != null) - Padding( - padding: const EdgeInsets.only(right: 8), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: ImageUtil.getImageFromPath( - imagePath: imagePath!, height: 40, width: 40), - ), - ), - Flexible( - child: Padding( - padding: const EdgeInsets.only(right: 8), - child: Text( - itemTitle, - style: titleStyle, - overflow: TextOverflow.ellipsis, - maxLines: 2, - softWrap: true, - )), - ), - SizedBox(width: 8), - if (copyButton) - RoundedIconButton( - icon: Icons.copy_all_outlined, - onPressed: copyButtonOnPressed ?? () {}, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - ), - ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Text(itemValue, style: itemValueStyle.copyWith(height: 1.0)), - if (itemSubTitle != null) Text(itemSubTitle!, style: itemSubTitleStyle), - ], - ), - ], - ), - ), - ); - } -} diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart index 3a20ca2ca..817ffbe5b 100644 --- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -5,14 +5,12 @@ import 'package:hash_wallet/src/widgets/rounded_icon_button.dart'; import 'package:hash_wallet/themes/core/theme_extension.dart'; import 'package:hash_wallet/utils/address_formatter.dart'; import 'package:hash_wallet/utils/image_utill.dart'; -import 'package:hash_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart'; import 'package:hash_wallet/view_model/send/output.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; import 'base_bottom_sheet_widget.dart'; @@ -26,9 +24,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { bool isSlideActionEnabled = true, String? accessibleNavigationModeSlideActionButtonText, required this.currency, - this.paymentId, - this.paymentIdValue, - this.expirationTime, required this.amount, required this.amountValue, required this.fiatAmountValue, @@ -41,8 +36,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { this.change, this.explanation, this.isOpenCryptoPay = false, - this.cakePayBuyCardViewModel, - this.quantity, Key? key, }) : showScrollbar = outputs.length > 3, super( @@ -58,9 +51,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { key: key); final CryptoCurrency currency; - final String? paymentId; - final String? paymentIdValue; - final String? expirationTime; final String amount; final String amountValue; final String fiatAmountValue; @@ -71,8 +61,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { final WalletType walletType; final PendingChange? change; final bool isOpenCryptoPay; - final CakePayBuyCardViewModel? cakePayBuyCardViewModel; - final String? quantity; final String? explanation; final AmountParsingProxy? amountParsingProxy; @@ -103,26 +91,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { padding: EdgeInsets.fromLTRB(8, 0, showScrollbar ? 16 : 8, 8), child: Column( children: [ - if (paymentId != null && paymentIdValue != null && cakePayBuyCardViewModel != null) - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Observer( - builder: (_) => AddressTile( - itemTitle: paymentId!, - itemTitleTextStyle: itemTitleTextStyle, - amountTextStyle: itemSubTitleTextStyle, - walletType: walletType, - amount: expirationTime != null - ? S.current.offer_expires_in + - ' ${cakePayBuyCardViewModel!.formattedRemainingTime}' - : null, - address: paymentIdValue!, - itemSubTitleTextStyle: itemSubTitleTextStyle, - tileBackgroundColor: tileBackgroundColor, - applyAddressFormatting: false, - copyButton: true, - )), - ), if (explanation != null) Padding( padding: const EdgeInsets.only(bottom: 8), @@ -163,12 +131,11 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { final isBatchSending = outputs.length > 1; final item = outputs[index]; final contactName = item.parsedAddress.name; - final isCakePayName = contactName == 'Cake Pay'; final batchContactTitle = '${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}'; final _address = item.isParsedAddress ? item.extractedAddress : item.address; final _amount = '${item.cryptoAmount.replaceAll(',', '.')} ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}'; - return isBatchSending || (contactName.isNotEmpty && !isCakePayName) + return isBatchSending || contactName.isNotEmpty ? ExpansionAddressTile( contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact, name: isBatchSending ? batchContactTitle : contactName, @@ -181,15 +148,11 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet { tileBackgroundColor: tileBackgroundColor, ) : AddressTile( - itemTitle: isCakePayName - ? item.parsedAddress.profileName - : S.of(context).address, - imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null, + itemTitle: S.of(context).address, itemTitleTextStyle: itemTitleTextStyle, walletType: walletType, - amount: isCakePayName ? item.fiatAmount : _amount, + amount: _amount, address: _address, - itemSubTitle: isCakePayName ? quantity : null, itemSubTitleTextStyle: itemSubTitleTextStyle, tileBackgroundColor: tileBackgroundColor, ); diff --git a/lib/src/widgets/cake_pay_order_list_card.dart b/lib/src/widgets/cake_pay_order_list_card.dart deleted file mode 100644 index 2a3c7327e..000000000 --- a/lib/src/widgets/cake_pay_order_list_card.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hash_wallet/themes/core/material_base_theme.dart'; - -class CakePayOrderListCard extends StatelessWidget { - const CakePayOrderListCard({ - Key? key, - required this.id, - required this.create, - this.quantity, - this.price, - required this.pair, - required this.onTap, - required this.currentTheme, - this.backgroundImage, - this.aspectRatio = 16 / 9, - }) : super(key: key); - - final String id; - final String create; - final String? quantity; - final String? price; - final String pair; - final ThemeType currentTheme; - final void Function(BuildContext) onTap; - final String? backgroundImage; - final double aspectRatio; - - ImageProvider? _imageProviderFor(String? path) { - if (path == null || path.isEmpty) return null; - if (path.startsWith('http://') || path.startsWith('https://')) return NetworkImage(path); - return AssetImage(path); - } - - Color _labelBackground(BuildContext context) { - final surface = Theme.of(context).colorScheme.surface; - return surface.withOpacity(0.75); - } - - Color _titleLabelBackground(BuildContext context) { - final surface = Theme.of(context).colorScheme.surface; - return surface.withOpacity(0.85); - } - - Widget _labelBox( - BuildContext context, { - required String text, - EdgeInsets padding = const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - double radius = 10, - Color? background, - TextStyle? style, - }) { - return Container( - padding: padding, - decoration: BoxDecoration( - color: background ?? _labelBackground(context), - borderRadius: BorderRadius.circular(radius), - ), - child: Text( - text, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: style ?? - Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - final image = _imageProviderFor(backgroundImage); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: () => onTap(context), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: AspectRatio( - aspectRatio: aspectRatio, - child: Stack( - fit: StackFit.expand, - children: [ - if (image != null) - Image(image: image, fit: BoxFit.cover) - else - Container(color: Theme.of(context).colorScheme.surfaceContainerHighest), - if (image != null) - Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: [0.0, 0.6, 1.0], - colors: [Colors.transparent, Colors.black26, Colors.black54], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _labelBox( - context, - text: 'Create • $create', - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - radius: 8, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontWeight: FontWeight.w700, - fontSize: 10, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 4), - _labelBox( - context, - text: 'Qty • $quantity Total • $price', - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - radius: 8, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontWeight: FontWeight.w700, - fontSize: 10, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], - ), - _labelBox( - context, - text: pair, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - radius: 12, - background: _titleLabelBackground(context), - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w800, - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], - ), - const Spacer(), - _labelBox( - context, - text: 'Order • $id', - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - radius: 10, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w800, - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], - ), - ), - IgnorePointer( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(currentTheme == ThemeType.dark ? 0.08 : 0.12), - width: 1, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index a7cd73f2a..52cfb8ffe 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -3,10 +3,6 @@ import 'package:cw_core/utils/tor/disabled.dart'; import 'package:flutter/foundation.dart'; class FeatureFlag { - static const bool isCakePayEnabled = false; - static const bool isCakePayPurchaseSimulationEnabled = true; - static const bool isCakePayRedemptionFlowEnabled = false; - static const bool isExolixEnabled = true; // Hash Bags: Lightning disabled — running Greenlight/LSP infra is out of // scope for now. Re-enable once we have an LSP relationship lined up. static const bool isLightningEnabled = false; diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index a64b0a5cd..7813da73b 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:hash_wallet/buy/buy_provider.dart'; import 'package:hash_wallet/buy/buy_quote.dart'; -import 'package:hash_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:hash_wallet/buy/payment_method.dart'; import 'package:hash_wallet/buy/sell_buy_states.dart'; import 'package:hash_wallet/core/selectable_option.dart'; @@ -455,18 +454,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S return true; }).toList(); - final List successRateQuotes = validQuotes - .where((element) => - element.provider is OnRamperBuyProvider && - element.recommendations.contains(ProviderRecommendation.successRate)) - .toList(); - - for (final quote in successRateQuotes) { - if (!uniqueProviderQuotes.contains(quote)) { - uniqueProviderQuotes.add(quote); - } - } - sortedRecommendedQuotes.addAll(uniqueProviderQuotes); sortedQuotes = ObservableList.of( @@ -477,12 +464,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S ..setIsBestRate = true; bestRateQuote = sortedRecommendedQuotes.first; - sortedRecommendedQuotes.sort((a, b) { - if (a.provider is OnRamperBuyProvider) return -1; - if (b.provider is OnRamperBuyProvider) return 1; - return 0; - }); - final Quote effectiveBestRateQuote = sortedRecommendedQuotes.reduce((a, b) { return isBuyAction ? a.rate < b.rate ? a : b : a.rate > b.rate ? a : b; diff --git a/lib/view_model/cake_pay/cake_pay_account_view_model.dart b/lib/view_model/cake_pay/cake_pay_account_view_model.dart deleted file mode 100644 index 4a9591fca..000000000 --- a/lib/view_model/cake_pay/cake_pay_account_view_model.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart'; -import 'package:mobx/mobx.dart'; - -part 'cake_pay_account_view_model.g.dart'; - -class CakePayAccountViewModel = CakePayAccountViewModelBase with _$CakePayAccountViewModel; - -abstract class CakePayAccountViewModelBase with Store { - CakePayAccountViewModelBase({required this.cakePayService}) : email = '' { - cakePayService.getUserEmail().then((email) => this.email = email ?? ''); - } - - final CakePayService cakePayService; - - @observable - String email; - - @action - Future logout() async => cakePayService.logout(email); -} diff --git a/lib/view_model/cake_pay/cake_pay_auth_view_model.dart b/lib/view_model/cake_pay/cake_pay_auth_view_model.dart deleted file mode 100644 index ff3eb9430..000000000 --- a/lib/view_model/cake_pay/cake_pay_auth_view_model.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:hash_wallet/cake_pay/src/cake_pay_states.dart'; -import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart'; -import 'package:mobx/mobx.dart'; - -part 'cake_pay_auth_view_model.g.dart'; - -class CakePayAuthViewModel = CakePayAuthViewModelBase with _$CakePayAuthViewModel; - -abstract class CakePayAuthViewModelBase with Store { - CakePayAuthViewModelBase({required this.cakePayService}) - : userVerificationState = CakePayUserVerificationStateInitial(), - otpState = CakePayOtpSendDisabled(), - email = '', - otp = ''; - - final CakePayService cakePayService; - - @observable - CakePayUserVerificationState userVerificationState; - - @observable - CakePayOtpState otpState; - - @observable - String email; - - @observable - String otp; - - @action - Future verifyEmail(String code) async { - try { - otpState = CakePayOtpValidating(); - await cakePayService.verifyEmail(code); - otpState = CakePayOtpSuccess(); - } catch (_) { - otpState = CakePayOtpFailure(error: 'Invalid OTP. Try again'); - } - } - - @action - Future logIn(String email) async { - try { - userVerificationState = CakePayUserVerificationStateLoading(); - await cakePayService.logIn(email); - userVerificationState = CakePayUserVerificationStateSuccess(); - } catch (e) { - userVerificationState = CakePayUserVerificationStateFailure(error: e.toString()); - } - } -} diff --git a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart deleted file mode 100644 index 4d9ceee28..000000000 --- a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart +++ /dev/null @@ -1,267 +0,0 @@ -import 'dart:async'; - -import 'package:hash_wallet/order/order_provider_description.dart'; -import 'package:hash_wallet/order/order.dart'; -import 'package:hash_wallet/order/order_source_description.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_card.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_vendor.dart'; -import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart'; -import 'package:hash_wallet/core/execution_state.dart'; -import 'package:hash_wallet/exchange/trade_state.dart'; -import 'package:hash_wallet/utils/feature_flag.dart'; -import 'package:hash_wallet/view_model/send/send_view_model.dart'; -import 'package:hash_wallet/view_model/send/send_view_model_state.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:hive/hive.dart'; -import 'package:mobx/mobx.dart'; - -part 'cake_pay_buy_card_view_model.g.dart'; - -class CakePayBuyCardViewModel = CakePayBuyCardViewModelBase with _$CakePayBuyCardViewModel; - -abstract class CakePayBuyCardViewModelBase with Store { - CakePayBuyCardViewModelBase( - {required this.vendor, - required CakePayService cakePayService, - required this.sendViewModel, - required this.orders}) - : _cakePayService = cakePayService, - walletType = sendViewModel.walletType, - amount = vendor.card!.denominationItems.isNotEmpty - ? vendor.card!.denominationItems.first.value - : 0, - quantity = 1, - card = vendor.card!, - min = _toDouble(vendor.card!.minValue) ?? 0, - max = _toDouble(vendor.card!.maxValue) ?? 0 { - selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null; - } - - static double? _toDouble(String? value) { - if (value == null || value.isEmpty) return null; - return double.tryParse(value.replaceAll(',', '.')); - } - - final CakePayVendor vendor; - final SendViewModel sendViewModel; - final CakePayService _cakePayService; - final double max; - final double min; - final CakePayCard card; - final WalletType walletType; - final Box orders; - - CakePayOrder? order; - Timer? _timer; - DateTime? expirationTime; - Duration? remainingTime; - bool confirmsNoVpn = false; - bool confirmsVoidedRefund = false; - bool confirmsTermsAgreed = false; - (String, int?) selectedDenomination = ('', null); - - String simulatedResponse = ''; - - bool get isDenominationSelected => - card.denominationItems.isNotEmpty && - card.denominationItems.any((item) => item.value == amount); - - Future get isUserLogged async => await _cakePayService.isLogged(); - - @observable - double amount; - - @observable - int quantity; - - @observable - bool isPurchasing = false; - - @observable - bool isSimulatingFlow = false; - - @observable - bool isOrderExpired = false; - - @observable - String formattedRemainingTime = ''; - - @computed - bool get isAmountSufficient => - (amount >= min && amount <= max) || (isDenominationSelected && quantity > 0); - - @observable - CakePayPaymentMethod? selectedPaymentMethod; - - @computed - double get totalAmount => amount * quantity; - - @computed - bool get isSimulating => - isSimulatingFlow && - FeatureFlag.hasDevOptions && - FeatureFlag.isCakePayPurchaseSimulationEnabled; - - @computed - List get availableMethods { - switch (walletType) { - case WalletType.bitcoin: - return [ - CakePayPaymentMethod.BTC, - if (sendViewModel.wallet.isSoftwareWallet) CakePayPaymentMethod.BTC_LN - ]; - case WalletType.litecoin: - return [ - CakePayPaymentMethod.LTC, - if (sendViewModel.isMwebEnabled) CakePayPaymentMethod.LTC_MWEB - ]; - case WalletType.monero: - return [CakePayPaymentMethod.XMR]; - default: - return const []; - } - } - - @action - void chooseMethod(CakePayPaymentMethod method) { - selectedPaymentMethod = method; - if (walletType == WalletType.litecoin) { - sendViewModel.setAllowMwebCoins(method == CakePayPaymentMethod.LTC_MWEB); - } - } - - @action - void onQuantityChanged(int? input) => quantity = input ?? 1; - - @action - void onAmountChanged(String input) { - if (input.isEmpty) return; - amount = double.parse(input.replaceAll(',', '.')); - } - - @action - Future createOrder() async { - if (![ - WalletType.bitcoin, - WalletType.monero, - WalletType.litecoin, - ].contains(walletType)) { - sendViewModel.state = - FailureState('Unsupported wallet type, please use Bitcoin, Monero, or Litecoin.'); - } - try { - order = await _cakePayService.createOrder( - cardId: isDenominationSelected ? selectedDenomination.$2 ?? card.id : card.id, - price: isDenominationSelected ? selectedDenomination.$1 : amount.toString(), - quantity: quantity, - confirmsNoVpn: confirmsNoVpn, - confirmsVoidedRefund: confirmsVoidedRefund, - confirmsTermsAgreed: confirmsTermsAgreed, - ); - final paymentData = - CakePayOrder.getPaymentDataFor(method: selectedPaymentMethod, order: order); // TODO should hande other currencies if added - if (paymentData == null || order == null) - throw Exception('Payment data or order is not available.'); - - await confirmSending(paymentData); - expirationTime = order!.paymentData.expirationTime; - - final orderRecord = Order( - id: order!.orderId, - state: TradeState.deserialize(raw: order!.status), - transferId: order!.externalId ?? '', - from: CakePayOrder.getCurrencyCodeFromPaymentMethod(selectedPaymentMethod!), - to: order!.fiatCurrencyCode, - createdAt: DateTime.now(), - amount: paymentData.amount, - receiveAmount: order!.totalReceiveAmount, - quantity: order!.quantity.toString(), - receiveAddress: paymentData.address, - source: OrderSourceDescription.order, - giftCardProvider: OrderProviderDescription.cakePay, - walletId: sendViewModel.wallet.id); - orders.add(orderRecord); - updateRemainingTime(); - _startExpirationTimer(); - } catch (e) { - sendViewModel.state = FailureState( - sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency)); - } - } - - @action - Future confirmSending(CryptoPaymentData paymentData) async { - try { - sendViewModel.clearOutputs(); - final output = sendViewModel.outputs.first; - output.address = paymentData.address; - output.setCryptoAmount(paymentData.price); - - await sendViewModel.createTransaction(); - } catch (e) { - throw e; - } - } - - @action - Future simulatePayment() async { - if (order == null) { - throw Exception('Order is not created yet.'); - } - - try { - simulatedResponse = await _cakePayService.simulatePayment(orderId: order!.orderId); - sendViewModel.state = TransactionCommitted(); - } catch (e) { - sendViewModel.state = FailureState( - sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency)); - } - } - - @action - void updateRemainingTime() { - if (expirationTime == null) { - formattedRemainingTime = ''; - return; - } - - remainingTime = expirationTime!.difference(DateTime.now()); - - isOrderExpired = remainingTime!.isNegative; - - if (isOrderExpired) { - disposeExpirationTimer(); - sendViewModel.state = FailureState('Order has expired.'); - } else { - formattedRemainingTime = formatDuration(remainingTime!); - } - } - - Future logout() async => await _cakePayService.logout(); - - void _startExpirationTimer() { - _timer?.cancel(); - _timer = Timer.periodic(Duration(seconds: 1), (_) { - updateRemainingTime(); - }); - } - - String formatDuration(Duration duration) { - final hours = duration.inHours; - final minutes = duration.inMinutes.remainder(60); - final seconds = duration.inSeconds.remainder(60); - return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - } - - void disposeExpirationTimer() { - _timer?.cancel(); - remainingTime = null; - formattedRemainingTime = ''; - expirationTime = null; - } - - void dispose() { - disposeExpirationTimer(); - } -} diff --git a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart deleted file mode 100644 index eca7e03d6..000000000 --- a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart +++ /dev/null @@ -1,299 +0,0 @@ -import 'dart:async'; - -import 'package:hash_wallet/cake_pay/src/models/cake_pay_card.dart'; -import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart'; -import 'package:hash_wallet/cake_pay/src/cake_pay_states.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_vendor.dart'; -import 'package:hash_wallet/entities/country.dart'; -import 'package:hash_wallet/entities/fiat_currency.dart'; -import 'package:hash_wallet/generated/i18n.dart'; -import 'package:hash_wallet/store/settings_store.dart'; -import 'package:hash_wallet/view_model/dashboard/filter_item.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:mobx/mobx.dart'; - -part 'cake_pay_cards_list_view_model.g.dart'; - -class CakePayCardsListViewModel = CakePayCardsListViewModelBase with _$CakePayCardsListViewModel; - -abstract class CakePayCardsListViewModelBase with Store { - CakePayCardsListViewModelBase({ - required this.cakePayService, - required this.settingsStore, - }) : cakePayVendors = [], - userCards = [], - availableCountries = ObservableList.of(Country.allForCakePay), - page = 1, - displayPrepaidCards = true, - displayGiftCards = true, - displayDenominationsCards = true, - displayCustomValueCards = true, - scrollOffsetFromTop = 0.0, - vendorsState = InitialCakePayVendorLoadingState(), - createCardState = CakePayCreateCardState(), - userCardState = UserCakePayCardsStateInitial(), - searchString = '', - searchMyCardsString = '' { - checkAuth(); - initialization(); - } - - static Country _getInitialCountry(FiatCurrency fiatCurrency) { - if (fiatCurrency.countryCode == 'eur') { - return Country.deu; - } - return Country.fromCode(fiatCurrency.countryCode) ?? Country.usa; - } - - void initialization() async { - getVendors(); - getUserCards(); - } - - final CakePayService cakePayService; - final SettingsStore settingsStore; - - Map> get createFilterItems => { - 'Card Type': [ - FilterItem( - value: () => displayPrepaidCards, - caption: S.current.prepaid_cards, - onChanged: togglePrepaidCards), - FilterItem( - value: () => displayGiftCards, - caption: S.current.gift_cards, - onChanged: toggleGiftCards), - ], - // Uncomment if will be added to backend - // S.current.value_type: [ - // FilterItem( - // value: () => displayDenominationsCards, - // caption: S.current.denominations, - // onChanged: toggleDenominationsCards), - // FilterItem( - // value: () => displayCustomValueCards, - // caption: S.current.custom_value, - // onChanged: toggleCustomValueCards), - // ], - }; - - String searchString; - String? username; - int page; - - late Country _initialSelectedCountry; - late bool _initialDisplayPrepaidCards; - late bool _initialDisplayGiftCards; - late bool _initialDisplayDenominationsCards; - late bool _initialDisplayCustomValueCards; - - @observable - List userCards; - - @observable - String searchMyCardsString; - - @observable - double scrollOffsetFromTop; - - @observable - CakePayCreateCardState createCardState; - - @observable - UserCakePayCardsState userCardState; - - @observable - CakePayVendorState vendorsState; - - @observable - bool hasMoreDataToFetch = true; - - @observable - bool isLoadingNextPage = false; - - @observable - List cakePayVendors; - - @observable - List availableCountries; - - @observable - bool displayPrepaidCards; - - @observable - bool displayGiftCards; - - @observable - bool displayDenominationsCards; - - @observable - bool displayCustomValueCards; - - @observable - ObservableFuture? authFuture; - - @computed - bool? get isUserAuthenticated => - authFuture?.status == FutureStatus.fulfilled ? authFuture?.value : null; - - @computed - Country get selectedCountry => - settingsStore.selectedCakePayCountry ?? _getInitialCountry(settingsStore.fiatCurrency); - - @computed - bool get shouldShowCountryPicker => - settingsStore.selectedCakePayCountry == null && availableCountries.isNotEmpty; - - @computed - List get filteredUserCards { - final query = searchMyCardsString.trim().toLowerCase(); - if (query.isEmpty) return userCards; - return userCards - .where((card) => card.name.toLowerCase().contains(query)) - .toList(growable: false); - } - - - bool get hasFiltersChanged { - return selectedCountry != _initialSelectedCountry || - displayPrepaidCards != _initialDisplayPrepaidCards || - displayGiftCards != _initialDisplayGiftCards || - displayDenominationsCards != _initialDisplayDenominationsCards || - displayCustomValueCards != _initialDisplayCustomValueCards; - } - - Future getUserCards() async { - //Dummy user cards // TODO: fetch from API - userCardState = UserCakePayCardsStateFetching(); - try { - await Future.delayed(const Duration(seconds: 2)); - final vendorsCard = cakePayVendors - .where((vendor) => vendor.card != null) - .map((vendor) => vendor.card!) - .toList(); - userCards = vendorsCard.sublist(0, 10); - - vendorsCard.forEach((card) { - if (card.name.toLowerCase().contains('amazon.com')) { - userCards.add(card); - } - }); - - userCardState = UserCakePayCardsStateSuccess(); - if (userCards.isEmpty) { - userCardState = UserCakePayCardsStateNoCards(); - } - } catch (e) { - userCardState = UserCakePayCardsStateFailure(error: e.toString()); - } - } - - @action - Future checkAuth() async { - authFuture = ObservableFuture(cakePayService.isLogged()); - - final logged = await authFuture!; - if (logged) { - username = await cakePayService.getUserEmail(); - } - } - - @action - Future getVendors({ - String? text, - int? currentPage, - }) async { - vendorsState = CakePayVendorLoadingState(); - try { - searchString = text ?? ''; - var newVendors = await cakePayService.getVendors( - countryCode: selectedCountry.countryCode, - page: currentPage ?? page, - search: searchString, - giftCards: displayGiftCards, - prepaidCards: displayPrepaidCards); - - cakePayVendors = newVendors.where((vendor) { - if (vendor.card == null) return false; - if (vendor.available != true) return false; - return true; - }).toList(); - } catch (e) { - printV(e); - } - vendorsState = CakePayVendorLoadedState(); - } - - @action - Future fetchNextPage() async { - if (vendorsState is CakePayVendorLoadingState || !hasMoreDataToFetch || isLoadingNextPage) { - return; - } - - isLoadingNextPage = true; - page++; - try { - var newVendors = await cakePayService.getVendors( - countryCode: selectedCountry.countryCode, - page: page, - search: searchString, - giftCards: displayGiftCards, - prepaidCards: displayPrepaidCards); - - cakePayVendors.addAll(newVendors.where((vendor) { - if (vendor.card == null) return false; - if (vendor.available != true) return false; - return true; - }).toList()); - } catch (error) { - if (error.toString().contains('detail":"Invalid page."')) { - hasMoreDataToFetch = false; - } - } finally { - isLoadingNextPage = false; - } - } - - Future isCakePayUserAuthenticated() async { - return await cakePayService.isLogged(); - } - - void resetLoadingNextPageState() { - hasMoreDataToFetch = true; - page = 1; - } - - void storeInitialFilterStates() { - _initialSelectedCountry = selectedCountry; - _initialDisplayPrepaidCards = displayPrepaidCards; - _initialDisplayGiftCards = displayGiftCards; - _initialDisplayDenominationsCards = displayDenominationsCards; - _initialDisplayCustomValueCards = displayCustomValueCards; - } - - @action - void setSelectedCountry(Country country) { - // just so it triggers the reaction even when selecting the default country - settingsStore.selectedCakePayCountry = null; - settingsStore.selectedCakePayCountry = country; - } - - @action - void setMyCardsQuery(String text) => searchMyCardsString = text; - - @action - void togglePrepaidCards() => displayPrepaidCards = !displayPrepaidCards; - - @action - void toggleGiftCards() => displayGiftCards = !displayGiftCards; - - @action - void toggleDenominationsCards() => displayDenominationsCards = !displayDenominationsCards; - - @action - void toggleCustomValueCards() => displayCustomValueCards = !displayCustomValueCards; - - void setScrollOffsetFromTop(double scrollOffset) { - scrollOffsetFromTop = scrollOffset; - } -} diff --git a/lib/view_model/dashboard/cake_features_view_model.dart b/lib/view_model/dashboard/cake_features_view_model.dart deleted file mode 100644 index ec135d05d..000000000 --- a/lib/view_model/dashboard/cake_features_view_model.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart'; -import 'package:mobx/mobx.dart'; - -part 'cake_features_view_model.g.dart'; - -class CakeFeaturesViewModel = CakeFeaturesViewModelBase with _$CakeFeaturesViewModel; - -abstract class CakeFeaturesViewModelBase with Store { - final CakePayService _cakePayService; - - CakeFeaturesViewModelBase(this._cakePayService); - - Future isIoniaUserAuthenticated() async { - return await _cakePayService.isLogged(); - } -} diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 44e97ec51..ff770440d 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -4,19 +4,9 @@ import 'package:hash_wallet/core/amount_parsing_proxy.dart'; import 'package:hash_wallet/entities/calculate_fiat_amount.dart'; import 'package:hash_wallet/entities/fiat_currency.dart'; import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/exolix_exchange_provider.dart'; import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/swaptrade_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/sideshift_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/simpleswap_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:hash_wallet/exchange/trade.dart'; import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/reactions/wallet_connect.dart'; @@ -51,41 +41,12 @@ abstract class ExchangeTradeViewModelBase with Store { isSwapsXYZCanSendFromExternal = _checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet), items = ObservableList() { setUpOutput(); + // Trades from providers whose implementations were removed keep a null + // _provider; status polling is simply skipped for them. switch (trade.provider) { - case ExchangeProviderDescription.changeNow: - _provider = - ChangeNowExchangeProvider(settingsStore: sendViewModel.balanceViewModel.settingsStore); - break; - case ExchangeProviderDescription.sideShift: - _provider = SideShiftExchangeProvider(); - break; - case ExchangeProviderDescription.simpleSwap: - _provider = SimpleSwapExchangeProvider(); - break; case ExchangeProviderDescription.trocador: _provider = TrocadorExchangeProvider(); break; - case ExchangeProviderDescription.exolix: - _provider = ExolixExchangeProvider(); - break; - case ExchangeProviderDescription.swapTrade: - _provider = SwapTradeExchangeProvider(); - break; - case ExchangeProviderDescription.stealthEx: - _provider = StealthExExchangeProvider(); - break; - case ExchangeProviderDescription.thorChain: - _provider = ThorChainExchangeProvider(); - break; - case ExchangeProviderDescription.chainflip: - _provider = ChainflipExchangeProvider(); - break; - case ExchangeProviderDescription.xoSwap: - _provider = XOSwapExchangeProvider(); - break; - case ExchangeProviderDescription.swapsXyz: - _provider = SwapsXyzExchangeProvider(); - break; case ExchangeProviderDescription.nearIntents: _provider = NearIntentsExchangeProvider(); break; @@ -195,7 +156,6 @@ abstract class ExchangeTradeViewModelBase with Store { output = sendViewModel.outputs.first; output.address = trade.inputAddress ?? ''; output.setCryptoAmount(trade.amount); - if (_provider is ThorChainExchangeProvider) output.memo = trade.memo ?? ""; if (trade.isSendAll == true) output.sendAll = true; } @@ -211,19 +171,7 @@ abstract class ExchangeTradeViewModelBase with Store { sendViewModel.selectedCryptoCurrency = selected; - final pendingTransaction = - await sendViewModel.createTransaction(provider: _provider, trade: trade); - - if (_provider is SwapsXyzExchangeProvider) { - final hash = pendingTransaction?.evmTxHashFromRawHex ?? pendingTransaction?.id ?? ''; - trade.txId = hash; - await trade.save(); - } - - if (_provider is ThorChainExchangeProvider) { - trade.id = pendingTransaction?.id ?? ''; - await trade.save(); - } + await sendViewModel.createTransaction(provider: _provider, trade: trade); } @action diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 1e151ff17..c75a935ca 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -23,17 +23,8 @@ import 'package:hash_wallet/exchange/exchange_template.dart'; import 'package:hash_wallet/exchange/exchange_trade_state.dart'; import 'package:hash_wallet/exchange/limits.dart'; import 'package:hash_wallet/exchange/limits_state.dart'; -import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/letsexchange_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/exolix_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/swaptrade_exchange_provider.dart'; import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:hash_wallet/exchange/trade.dart'; import 'package:hash_wallet/exchange/trade_request.dart'; import 'package:hash_wallet/generated/i18n.dart'; @@ -49,7 +40,6 @@ import 'package:hash_wallet/store/settings_store.dart'; import 'package:hash_wallet/store/templates/exchange_template_store.dart'; import 'package:hash_wallet/evm/evm.dart'; import 'package:hash_wallet/reactions/wallet_connect.dart'; -import 'package:hash_wallet/utils/feature_flag.dart'; import 'package:hash_wallet/utils/token_utilities.dart'; import 'package:hash_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:hash_wallet/view_model/send/fees_view_model.dart'; @@ -197,8 +187,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final initialProvider = provider; provider!.checkIsAvailable().then((bool isAvailable) { if (!isAvailable && provider == initialProvider) { - provider = providerList.firstWhere((provider) => provider is ChangeNowExchangeProvider, - orElse: () => providerList.last); + provider = providerList.last; _onPairChange(); } }); @@ -323,20 +312,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with // Hash Bags: trimmed to Trocador only. Trocador's aggregator already fronts // ChangeNow, LetsExchange, Exolix, StealthEx, FixedFloat, Quantex, etc., - // letting us collect markup as the affiliate. Re-enable individual providers - // here only after registering Such Software affiliate keys per provider. + // letting us collect markup as the affiliate. The individual provider + // implementations were deleted; restore them from Cake Wallet upstream and + // register Such Software affiliate keys per provider before re-adding any. List get _allProviders => [ - // ChangeNowExchangeProvider(settingsStore: _settingsStore), - // SideShiftExchangeProvider(), - // ChainflipExchangeProvider(), - // if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), - // SwapTradeExchangeProvider(), - // LetsExchangeExchangeProvider(), - // StealthExExchangeProvider(), - // XOSwapExchangeProvider(), - // SwapsXyzExchangeProvider(), - // JupiterExchangeProvider(), - // NearIntentsExchangeProvider(), TrocadorExchangeProvider( useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), ]; @@ -1501,16 +1480,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } void _defineIsReceiveAmountEditable() { - /*if ((provider is ChangeNowExchangeProvider) - &&(depositCurrency == CryptoCurrency.xmr) - &&(receiveCurrency == CryptoCurrency.btc)) { - isReceiveAmountEditable = true; - } else { - isReceiveAmountEditable = false; - }*/ - //isReceiveAmountEditable = false; - // isReceiveAmountEditable = selectedProviders.any((provider) => provider is ChangeNowExchangeProvider); - // isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider; isReceiveAmountEditable = true; } diff --git a/lib/view_model/order_details_view_model.dart b/lib/view_model/order_details_view_model.dart index 635daedbb..d5d47a813 100644 --- a/lib/view_model/order_details_view_model.dart +++ b/lib/view_model/order_details_view_model.dart @@ -1,25 +1,16 @@ import 'dart:async'; import 'package:hash_wallet/buy/buy_provider_description.dart'; -import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart'; -import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart'; import 'package:hash_wallet/core/utilities.dart'; import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/order/order.dart'; import 'package:hash_wallet/order/order_provider.dart'; -import 'package:hash_wallet/order/order_provider_adapter/cake_pay_order_provider_adapter.dart'; -import 'package:hash_wallet/order/order_provider_description.dart'; import 'package:hash_wallet/order/order_source_description.dart'; -import 'package:hash_wallet/src/screens/order_details/cake_pay_detail_list_card_item.dart'; import 'package:hash_wallet/src/screens/trade_details/track_trade_list_item.dart'; import 'package:hash_wallet/src/screens/trade_details/trade_details_status_item.dart'; import 'package:hash_wallet/src/screens/transaction_details/standart_list_item.dart'; -import 'package:hash_wallet/utils/date_formatter.dart'; -import 'package:hash_wallet/utils/show_bar.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -32,7 +23,6 @@ abstract class OrderDetailsViewModelBase with Store { OrderDetailsViewModelBase({ required WalletBase wallet, required Order orderForDetails, - required this.cakePayService, required this.orders, }) : items = ObservableList(), order = orderForDetails { @@ -44,9 +34,8 @@ abstract class OrderDetailsViewModelBase with Store { break; case OrderSourceDescription.order: - if (order.orderProvider == OrderProviderDescription.cakePay) { - _provider = CakePayOrderProviderAdapter(wallet: wallet, cakePayService: cakePayService); - } + // Cake Pay orders: the Cake Pay integration was removed, so stored + // orders are shown from persisted data only (_provider stays null). break; } @@ -61,10 +50,8 @@ abstract class OrderDetailsViewModelBase with Store { @observable ObservableList items; - final CakePayService cakePayService; final Box orders; OrderProvider? _provider; - List cards = []; Timer? timer; @@ -76,10 +63,6 @@ abstract class OrderDetailsViewModelBase with Store { final updatedOrderObj = await _provider!.findOrderById(order.id); final updatedOrder = updatedOrderObj.$1; - if (_provider is CakePayOrderProviderAdapter) { - cards = updatedOrderObj.$2 as List? ?? []; - } - final existing = orders.values.firstWhereOrNull((e) => e.id == updatedOrder.id); if (existing != null) { existing.stateRaw = updatedOrder.stateRaw; @@ -98,30 +81,11 @@ abstract class OrderDetailsViewModelBase with Store { } void _updateItems() { - final dateFormat = DateFormatter.withCurrentLocal(); items.clear(); items.add( DetailsListStatusItem(title: S.current.trade_details_state, value: order.state.toString())); - if (_provider is CakePayOrderProviderAdapter) { - items.add(CakePayDetailsListCardItem( - title: '', - value: '', - id: order.id, - createdAt: dateFormat.format(order.createdAt), - price: order.receiveAmount, - quantity: order.quantity, - from: order.from ?? '', - to: order.to ?? '', - cards: cards, - onTap: (BuildContext context) { - Clipboard.setData(ClipboardData(text: '${order.id}')); - showBar(context, S.of(context).copied_to_clipboard); - }, - )); - } - items.add(StandartListItem(title: 'Order provider', value: _provider?.title ?? '')); final trackUrl = _provider?.trackUrl ?? ''; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index e0b4811b7..d83ba376e 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -21,11 +21,8 @@ import 'package:hash_wallet/entities/template.dart'; import 'package:hash_wallet/entities/transaction_description.dart'; import 'package:hash_wallet/entities/wallet_contact.dart'; import 'package:hash_wallet/evm/evm.dart'; -import 'package:hash_wallet/exchange/exchange_provider_description.dart'; import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:hash_wallet/exchange/trade.dart'; import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/monero/monero.dart'; @@ -47,13 +44,11 @@ import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_list_view_mod import 'package:hash_wallet/wownero/wownero.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; -import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/lnurl.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; @@ -131,10 +126,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor }); } - // Store trade and provider references for post-commit updates (e.g., Jupiter trade ID update) - Trade? _currentTrade; - ExchangeProvider? _currentProvider; - @observable ExecutionState state; @@ -596,8 +587,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @action Future createTransaction({ExchangeProvider? provider, Trade? trade}) async { - _currentTrade = trade; - _currentProvider = provider; pendingTransaction = null; try { @@ -615,184 +604,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } - // Swaps.xyz (EVM) path - if (isEVMWallet && trade != null && provider is SwapsXyzExchangeProvider) { - final routerTo = trade.inputAddress; - final routerData = trade.routerData; - - - if (routerData != null && routerData != '0x') { - final tokenContract = (trade.sourceTokenAddress ?? '').toLowerCase(); - final priority = _settingsStore.getPriority( - walletType, chainId: selectedChainId); - final routerValueWei = BigInt.tryParse(trade.routerValue ?? '0') ?? - BigInt.zero; - - if (routerTo == null || routerTo.isEmpty) { - state = FailureState('Invalid router address'); - return null; - } - - try { - final selector = _decodeMethodSelector(routerData); - const transferSig = '0xa9059cbb'; - const swapAndExecuteSig = '0x9be111d1'; - - // Direct Transfer (Simple routing, no approval needed) - if (selector == transferSig) { - pendingTransaction = await evm!.createRawCallDataTransaction( - wallet, - routerTo, - routerData, - BigInt.zero, - priority, - useBlinkProtection: canSupportBlinkProtection(selectedChainId) - ? _settingsStore.useBlinkProtection - : false, - ); - state = ExecutedSuccessfullyState(); - return pendingTransaction; - } - - // Smart Swap (Requires Approval) - if (selector == swapAndExecuteSig) { - final requiredAmount = BigInt.tryParse( - (trade.sourceTokenAmountRaw ?? '0').replaceAll('n', '')) ?? - BigInt.zero; - - final needsApproval = tokenContract.isNotEmpty && - requiredAmount > BigInt.zero - ? await evm!.isApprovalRequired( - wallet, tokenContract, routerTo, requiredAmount) - : false; - - printV( - '[Swaps.xyz sending flow] Approval required: $needsApproval for token ${trade - .from?.title} ${trade.from?.tag ?? - ''} with amount $requiredAmount'); - - if (needsApproval) { - // USDT Approval Flow (Special Case). We must reset allowance to 0 first. - final isUSDTMainnet = selectedChainId == 1 && - tokenContract.toLowerCase() == - '0xdac17f958d2ee523a2206206994597c13d831ec7'; - - if (isUSDTMainnet) { - final currentAllowance = await evm!.getAllowance( - wallet, tokenContract, routerTo); - - if (currentAllowance != null && - currentAllowance > BigInt.zero) { - printV( - '[Swaps.xyz sending flow] currentAllowance USDT: $currentAllowance. Resetting to 0 before setting new allowance.'); - - final resetTx = await buildApprovalNeeded( - spender: routerTo, - tokenContract: tokenContract, - requiredAmount: BigInt.zero, - // Approve 0 - sourceTokenDecimals: trade.sourceTokenDecimals, - priority: priority - ); - - if (resetTx != null) { - await resetTx.commit(); - - final resetConfirmed = await _waitForApprovalUpdate( - tokenContract: tokenContract, - spender: routerTo, - requiredAmount: BigInt.zero, // Wait until it equals 0 - waitForExactMatch: true, - ); - - if (!resetConfirmed) { - state = FailureState( - 'Failed to reset USDT allowance. Please try again.'); - return null; - } - printV( - '[Swaps.xyz sending flow] USDT allowance reset to 0 confirmed on-chain.'); - } - } - } - - // Standard Approval Flow - final approvalTx = await buildApprovalNeeded( - spender: routerTo, - tokenContract: tokenContract, - requiredAmount: requiredAmount, - sourceTokenDecimals: trade.sourceTokenDecimals, - priority: priority - ); - - if (approvalTx == null) { - state = FailureState('Failed to build approval transaction'); - return null; - } - - pendingTransaction = null; - - try { - printV( - '[Swaps.xyz sending flow] Submitting approval transaction for token ${trade - .from?.title} ${trade.from?.tag ?? ''} '); - await approvalTx.commit(); - - // Wait for the approval to be mined on-chain - final isApproved = await _waitForApprovalUpdate( - tokenContract: tokenContract, - spender: routerTo, - requiredAmount: requiredAmount, - ); - - if (!isApproved) { - state = FailureState( - 'Approval transaction failed or timed out on-chain. Try again.'); - return null; - } - printV( - '[Swaps.xyz sending flow] Approval transaction confirmed on-chain. Proceeding with swap execution.'); - } catch (e, s) { - printV( - '[Swaps.xyz sending flow] Approval transaction error: $e\n$s'); - state = FailureState( - translateErrorMessage(e, wallet.type, wallet.currency)); - return null; - } - } - - // Construct Final Swap Transaction - printV('[Swaps.xyz sending flow] Building swap transaction'); - pendingTransaction = await evm!.createRawCallDataTransaction( - wallet, - routerTo, - routerData, - routerValueWei, - priority, - sourceTokenAddress: tokenContract, - sourceTokenAmount: requiredAmount, - useBlinkProtection: canSupportBlinkProtection(selectedChainId) - ? _settingsStore.useBlinkProtection - : false, - ); - - state = ExecutedSuccessfullyState(); - return pendingTransaction; - } - - state = FailureState('Unsupported Swaps.xyz transaction type'); - return null; - } catch (e, s) { - printV('Swaps.xyz transaction error: $e\n$s'); - state = FailureState( - 'Failed to create Swaps.xyz transaction - ${translateErrorMessage( - e, wallet.type, wallet.currency)}'); - return null; - } - } - } - // END Swaps.xyz path - // Regular flow @@ -823,16 +634,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } - if (provider is ThorChainExchangeProvider) { - final outputCount = pendingTransaction?.outputCount ?? 0; - if (outputCount > 10) { - throw Exception("THORChain does not support more than 10 outputs"); - } - - if (_hasTaprootInput(pendingTransaction)) { - throw Exception("THORChain does not support Taproot addresses"); - } - } } @@ -936,13 +737,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = TransactionCommitted(); - if (_currentTrade != null) { - final provider = _currentTrade!.provider; - if (provider == ExchangeProviderDescription.swapsXyz) { - registerSwapsXyzTransaction(_currentTrade!); - } - } - // Immediate transaction update for EVM chains and Nano if (isEVMWallet || [WalletType.bitcoin, WalletType.nano].contains(walletType)) { Future.delayed(Duration(seconds: 4), () async { @@ -1121,88 +915,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return null; } - // Helper functions for EVM transaction monitoring and approval flow - - // Polls the token contract directly to see if allowance is updated. - Future _waitForApprovalUpdate({ - required String tokenContract, - required String spender, - required BigInt requiredAmount, - bool waitForExactMatch = false, - }) async { - if (!isEVMWallet || evm == null) return false; - - int attempts = 0; - const int maxAttempts = 30; // ~60 seconds - - printV('[Swaps.xyz sending flow] Starting allowance check. Target: $requiredAmount (Exact match: $waitForExactMatch)'); - - while (attempts < maxAttempts) { - try { - final currentAllowance = await evm!.getAllowance(wallet, tokenContract, spender); - - if (currentAllowance != null) { - printV('[Swaps.xyz sending flow] Current Allowance: $currentAllowance / Target: $requiredAmount'); - - if (waitForExactMatch) { - // For Reset (Target 0): We need it to be exactly 0 (or less, though it can't be negative) - if (currentAllowance <= requiredAmount) { - printV('[Swaps.xyz sending flow] Allowance reset verified!'); - return true; - } - } else { - // For Approval: We need it to be at least the required amount - if (currentAllowance >= requiredAmount) { - printV('[Swaps.xyz sending flow] Allowance verified!'); - return true; - } - } - } - } catch (e) { - printV('[Swaps.xyz sending flow] Allowance check error: $e'); - } - - await Future.delayed(const Duration(seconds: 1)); - attempts++; - } - - printV('[Swaps.xyz sending flow] Allowance check timed out.'); - return false; - } - - // Builds a token approval transaction - Future buildApprovalNeeded({ - required String spender, - required String tokenContract, - required BigInt requiredAmount, - required TransactionPriority? priority, - int? sourceTokenDecimals, - }) async { - - final erc20Token = wallet.balance.keys.whereType().firstWhere( - (t) => t.contractAddress.toLowerCase() == tokenContract.toLowerCase(), - orElse: () => Erc20Token( - name: '', - symbol: '', - contractAddress: tokenContract, - decimal: sourceTokenDecimals ?? 18, - enabled: true, - ), - ); - - return await evm!.createTokenApproval( - wallet, - requiredAmount, - spender, - erc20Token, - priority, - useBlinkProtection: - canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false, - ); - } - - // End EVM helper functions - String translateErrorMessage( Object error, WalletType walletType, @@ -1322,63 +1034,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return null; } - - bool _hasTaprootInput(PendingTransaction? pendingTransaction) { - if (walletType == WalletType.bitcoin && pendingTransaction != null) { - return bitcoin!.hasTaprootInput(pendingTransaction); - } - - return false; - } - - Future registerSwapsXyzTransaction(Trade trade) async { - try { - - // register only for vmId is alt-vm or bridgeId is alt-vm (trade.needToRegisterInSwapXyz) - final needToRegister = trade.needToRegisterInSwapXyz ?? false; - if (!needToRegister) return; - - final vmId = (trade.providerId ?? '').toLowerCase(); - if (vmId.isEmpty) { - printV('SwapsXyz: transaction register: skipped (vmId empty)'); - return; - } - - final txHash = pendingTransaction?.evmTxHashFromRawHex - ?? pendingTransaction?.id - ?? ''; - - if (txHash.isEmpty) { - printV('SwapsXyz: transaction register: skipped (txHash empty)'); - return; - } - - final chainId = int.tryParse(trade.router ?? '') ?? 0; - if (chainId <= 0) { - printV('SwapsXyz: transaction register: skipped (invalid chainId)'); - return; - } - - printV( - 'SwapsXyz: attempting to register transaction: tradeId = ${trade.id}, txHash = $txHash, chainId = $chainId, vmId = $vmId'); - - final registered = await SwapsXyzExchangeProvider.registerAltVmTx( - txId: trade.id, - txHash: txHash, - chainId: chainId, - vmId: vmId, - ); - - if (!registered) { - printV('SwapsXyz: transaction register: failed'); - } else { - printV('SwapsXyz: transaction register: success'); - } - } catch (e) { - printV('registerSwapsXyzTransaction error: $e'); - } - } - @computed bool get usePayjoin => _settingsStore.usePayjoin; @@ -1397,6 +1052,4 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } - String _decodeMethodSelector(String s) => - (s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : ''; } diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 7477fd86a..88be8e294 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -1,20 +1,9 @@ import 'dart:async'; import 'package:hash_wallet/exchange/exchange_provider_description.dart'; -import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:hash_wallet/exchange/provider/exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/exolix_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/letsexchange_exchange_provider.dart'; import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/swapsxyz_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/swaptrade_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/sideshift_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/simpleswap_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart'; -import 'package:hash_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:hash_wallet/exchange/trade.dart'; import 'package:hash_wallet/generated/i18n.dart'; import 'package:hash_wallet/src/screens/trade_details/track_trade_list_item.dart'; @@ -40,42 +29,12 @@ abstract class TradeDetailsViewModelBase with Store { required this.appStore, }) : items = ObservableList(), trade = tradeForDetails { + // Trades from providers whose implementations were removed keep a null + // _provider and show the "provider unsupported" item with stored data. switch (trade.provider) { - case ExchangeProviderDescription.changeNow: - _provider = ChangeNowExchangeProvider(settingsStore: appStore.settingsStore); - break; - case ExchangeProviderDescription.sideShift: - _provider = SideShiftExchangeProvider(); - break; - case ExchangeProviderDescription.simpleSwap: - _provider = SimpleSwapExchangeProvider(); - break; case ExchangeProviderDescription.trocador: _provider = TrocadorExchangeProvider(); break; - case ExchangeProviderDescription.exolix: - _provider = ExolixExchangeProvider(); - break; - case ExchangeProviderDescription.thorChain: - _provider = ThorChainExchangeProvider(); - break; - case ExchangeProviderDescription.swapTrade: - _provider = SwapTradeExchangeProvider(); - case ExchangeProviderDescription.letsExchange: - _provider = LetsExchangeExchangeProvider(); - break; - case ExchangeProviderDescription.stealthEx: - _provider = StealthExExchangeProvider(); - break; - case ExchangeProviderDescription.chainflip: - _provider = ChainflipExchangeProvider(); - break; - case ExchangeProviderDescription.xoSwap: - _provider = XOSwapExchangeProvider(); - break; - case ExchangeProviderDescription.swapsXyz: - _provider = SwapsXyzExchangeProvider(); - break; case ExchangeProviderDescription.nearIntents: _provider = NearIntentsExchangeProvider(); break;