diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 6c7e82409..762144ac1 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -172,6 +172,7 @@ jobs: # end of test secrets echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart - name: prepare monero_c and cache diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index e307dc410..c5cb26dd9 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -168,6 +168,7 @@ jobs: # end of test secrets echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart + echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart - name: prepare monero_c and cache diff --git a/assets/images/kryptonim_dark.png b/assets/images/kryptonim_dark.png new file mode 100644 index 000000000..646d550ba Binary files /dev/null and b/assets/images/kryptonim_dark.png differ diff --git a/assets/images/kryptonim_light.png b/assets/images/kryptonim_light.png new file mode 100644 index 000000000..85e64a3f2 Binary files /dev/null and b/assets/images/kryptonim_light.png differ diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index 8e79e16b8..ea2bcae8f 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -49,7 +49,7 @@ abstract class BuyProvider { throw UnimplementedError(); Future> getAvailablePaymentTypes( - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async => + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async => []; Future?> fetchQuote( diff --git a/lib/buy/buy_quote.dart b/lib/buy/buy_quote.dart index 72ab7bd7d..8da70361d 100644 --- a/lib/buy/buy_quote.dart +++ b/lib/buy/buy_quote.dart @@ -289,6 +289,29 @@ class Quote extends SelectableOption { ); } + 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(); @@ -299,4 +322,7 @@ class Quote extends SelectableOption { } return null; } + + @override + String toString() => 'Quote: rate: $rate, feeAmount: $feeAmount, networkFee: $networkFee, transactionFee: $transactionFee, payout: $payout, paymentType: $paymentType, provider: $provider, quoteId: $quoteId, recommendations: $recommendations, isBuyAction: $isBuyAction, rampId: $rampId, rampName: $rampName, rampIconPath: $rampIconPath, [limits: min: ${limits?.min}, max: ${limits?.max}]'; } diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index eba48632b..ac50665f0 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -168,7 +168,7 @@ class DFXBuyProvider extends BuyProvider { } Future> getAvailablePaymentTypes( - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { final List paymentMethods = []; if (isBuyAction) { @@ -190,7 +190,7 @@ class DFXBuyProvider extends BuyProvider { }); } } else { - final assetCredentials = await fetchAssetCredential(cryptoCurrency); + final assetCredentials = await fetchAssetCredential(cryptoCurrency.title); if (assetCredentials.isNotEmpty) { if (assetCredentials['sellable'] == true) { final availablePaymentTypes = [ diff --git a/lib/buy/kryptonim/kryptonim.dart b/lib/buy/kryptonim/kryptonim.dart new file mode 100644 index 000000000..c439fa2f7 --- /dev/null +++ b/lib/buy/kryptonim/kryptonim.dart @@ -0,0 +1,222 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/buy/buy_quote.dart'; +import 'package:cake_wallet/buy/payment_method.dart'; +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/material.dart'; +import 'dart:developer'; +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; + +class KryptonimBuyProvider extends BuyProvider { + KryptonimBuyProvider({required WalletBase wallet, bool isTestEnvironment = false}) + : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null); + + 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; + + @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, + }; + + final body = jsonEncode({ + 'amount': amount, + 'currency': fiatCurrency, + 'converted_currency': cryptoCurrency.title, + 'blockchain': _normalizeBlockChain(cryptoCurrency), + 'quote_currency': fiatCurrency, + }); + + try { + final response = await http.post(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? 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); + + 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', + 'POL' => 'Polygon', + 'AVAXC' => 'Avalanche', + 'SOL' => 'Solana', + _ => null, + }; + + if (blockchain == null) { + blockchain = switch (cur) { + CryptoCurrency.btc => 'Bitcoin', + CryptoCurrency.ltc => 'Litecoin', + CryptoCurrency.eth => 'Ethereum', + 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 index a9759aab8..81ad35a40 100644 --- a/lib/buy/meld/meld_buy_provider.dart +++ b/lib/buy/meld/meld_buy_provider.dart @@ -53,7 +53,7 @@ class MeldBuyProvider extends BuyProvider { @override Future> getAvailablePaymentTypes( - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'}; final path = '$_providersProperties$_paymentMethodsPath'; diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index a6966db33..7bca7d405 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -126,11 +126,11 @@ class MoonPayProvider extends BuyProvider { } Future> getAvailablePaymentTypes( - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { final List paymentMethods = []; if (isBuyAction) { - final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency, null); + final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency.title, null); if (fiatBuyCredentials.isNotEmpty) { final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?; paymentMethods.add(PaymentMethod.fromMoonPayJson( diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index f229cb833..5480ab2cd 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -48,7 +48,7 @@ class OnRamperBuyProvider extends BuyProvider { bool get isAggregator => true; Future> getAvailablePaymentTypes( - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async { + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async { final params = { 'fiatCurrency': fiatCurrency, 'type': isBuyAction ? 'buy' : 'sell', diff --git a/lib/buy/payment_method.dart b/lib/buy/payment_method.dart index cf85c441b..14b119aa0 100644 --- a/lib/buy/payment_method.dart +++ b/lib/buy/payment_method.dart @@ -218,6 +218,15 @@ class PaymentMethod extends SelectableOption { customDescription: json['description'] as String?); } + factory PaymentMethod.fromKryptonimJson(Map json) { + final type = PaymentMethod.getPaymentTypeId(json['payment_method'] as String?); + return PaymentMethod( + paymentMethodType: type, + 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/di.dart b/lib/di.dart index 174183f9a..83b3efaea 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -255,6 +255,7 @@ import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'buy/kryptonim/kryptonim.dart'; import 'buy/meld/meld_buy_provider.dart'; import 'src/screens/buy/buy_sell_page.dart'; import 'cake_pay/cake_pay_payment_credantials.dart'; @@ -1019,6 +1020,10 @@ Future setup({ wallet: getIt.get().wallet!, )); + getIt.registerFactory(() => KryptonimBuyProvider( + wallet: getIt.get().wallet!, + )); + getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); getIt.registerFactory(() => ExchangeViewModel( diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index c65ac267b..5888970b0 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; +import 'package:cake_wallet/buy/kryptonim/kryptonim.dart'; import 'package:cake_wallet/buy/meld/meld_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; @@ -8,7 +9,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:http/http.dart'; -enum ProviderType { robinhood, dfx, onramper, moonpay, meld } +enum ProviderType { robinhood, dfx, onramper, moonpay, meld, kriptonim } extension ProviderTypeName on ProviderType { String get title { @@ -23,6 +24,8 @@ extension ProviderTypeName on ProviderType { return 'MoonPay'; case ProviderType.meld: return 'Meld'; + case ProviderType.kriptonim: + return 'Kriptonim'; } } @@ -38,6 +41,8 @@ extension ProviderTypeName on ProviderType { return 'moonpay_provider'; case ProviderType.meld: return 'meld_provider'; + case ProviderType.kriptonim: + return 'kriptonim_provider'; } } } @@ -59,6 +64,7 @@ class ProvidersHelper { ProviderType.dfx, ProviderType.robinhood, ProviderType.moonpay, + ProviderType.kriptonim ]; case WalletType.litecoin: case WalletType.bitcoinCash: @@ -66,13 +72,15 @@ class ProvidersHelper { return [ ProviderType.onramper, ProviderType.robinhood, - ProviderType.moonpay + ProviderType.moonpay, + ProviderType.kriptonim ]; case WalletType.tron: return [ ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay, + ProviderType.kriptonim ]; case WalletType.none: case WalletType.haven: @@ -127,6 +135,8 @@ class ProvidersHelper { return getIt.get(); case ProviderType.meld: return getIt.get(); + case ProviderType.kriptonim: + return getIt.get(); default: return null; } diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 508d68a82..02b2b3576 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -351,7 +351,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S paymentMethodState = PaymentMethodLoading(); selectedPaymentMethod = null; final result = await Future.wait(providerList.map((element) => element - .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency.title, isBuyAction) + .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction) .timeout( Duration(seconds: 10), onTimeout: () => [], diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 0ef38e939..a8ebcc8cc 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -77,6 +77,7 @@ class SecretKey { SecretKey('moneroTestWalletBlockHeight', () => ''), SecretKey('chainflipApiKey', () => ''), SecretKey('chainflipAffiliateFee', () => ''), + SecretKey('kryptonimApiKey', () => ''), SecretKey('walletGroupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)), ];