Cw 793 implement kryptonim dfx to fiat buy sell option (#2068)

* init commit * add authorization data * Update lib/buy/kryptonim/kryptonim.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Serhii committed Mar 6, 2025 at 19:39 UTC 1aad8366c4786ffddbd0afcd5b18cd2a6257eff8
16 files changed +285 -10
.github/workflows/pr_test_build_android.yml
+1
@@ -172,6 +172,7 @@ jobs:
172 # end of test secrets
173 echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart
174 echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
175 + echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart
176 echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart
177
178 - name: prepare monero_c and cache
.github/workflows/pr_test_build_linux.yml
+1
@@ -168,6 +168,7 @@ jobs:
168 # end of test secrets
169 echo "const chainflipApiKey = '${{ secrets.CHAINFLIP_API_KEY }}';" >> lib/.secrets.g.dart
170 echo "const chainflipAffiliateFee = '${{ secrets.CHAINFLIP_AFFILIATE_FEE }}';" >> lib/.secrets.g.dart
171 + echo "const kryptonimApiKey = '${{ secrets.KRYPTONIM_API_KEY }}';" >> lib/.secrets.g.dart
172 echo "const walletGroupSalt = '${{ secrets.WALLET_GROUP_SALT }}';" >> lib/.secrets.g.dart
173
174 - name: prepare monero_c and cache
assets/images/kryptonim_dark.png
Binary files /dev/null and b/assets/images/kryptonim_dark.png differ
assets/images/kryptonim_light.png
Binary files /dev/null and b/assets/images/kryptonim_light.png differ
lib/buy/buy_provider.dart
+1 -1
@@ -49,7 +49,7 @@ abstract class BuyProvider {
49 throw UnimplementedError();
50
51 Future<List<PaymentMethod>> getAvailablePaymentTypes(
52 - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async =>
52 + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async =>
53 [];
54
55 Future<List<Quote>?> fetchQuote(
lib/buy/buy_quote.dart
+26
@@ -289,6 +289,29 @@ class Quote extends SelectableOption {
289 );
290 }
291
292 + factory Quote.fromKryptonimJson(
293 + Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
294 + final fees = json['fees'] as Map<String, dynamic>;
295 + final rate = _toDouble(json['rate']) ?? 0.0;
296 + final limits = json['limits'] as Map<String, dynamic>;
297 + final minLimit = _toDouble(limits['min_amount']) ?? 0.0;
298 + final maxLimit = _toDouble(limits['max_amount']) ?? double.infinity;
299 + final convertedAmount = _toDouble(json['converted_amount']) ?? 0.0;
300 + final amount = _toDouble(json['amount']) ?? 0.0;
301 + final calculatedRate = amount / convertedAmount;
302 + return Quote(
303 + rate: calculatedRate,
304 + feeAmount: _toDouble(fees['totalFee']) ?? 0.0,
305 + networkFee: _toDouble(fees['network_fee']) ?? 0.0,
306 + transactionFee: _toDouble(fees['operation_fee']) ?? 0.0,
307 + payout: _toDouble(json['amount']) ?? 0.0,
308 + paymentType: paymentType,
309 + recommendations: [],
310 + provider: ProvidersHelper.getProviderByType(ProviderType.kriptonim)!,
311 + isBuyAction: isBuyAction,
312 + limits: Limits(min: minLimit, max: maxLimit));
313 + }
314 +
315 static double? _toDouble(dynamic value) {
316 if (value is int) {
317 return value.toDouble();
@@ -299,4 +322,7 @@ class Quote extends SelectableOption {
322 }
323 return null;
324 }
325 +
326 + @override
327 + 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}]';
328 }
lib/buy/dfx/dfx_buy_provider.dart
+2 -2
@@ -168,7 +168,7 @@ class DFXBuyProvider extends BuyProvider {
168 }
169
170 Future<List<PaymentMethod>> getAvailablePaymentTypes(
171 - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
171 + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
172 final List<PaymentMethod> paymentMethods = [];
173
174 if (isBuyAction) {
@@ -190,7 +190,7 @@ class DFXBuyProvider extends BuyProvider {
190 });
191 }
192 } else {
193 - final assetCredentials = await fetchAssetCredential(cryptoCurrency);
193 + final assetCredentials = await fetchAssetCredential(cryptoCurrency.title);
194 if (assetCredentials.isNotEmpty) {
195 if (assetCredentials['sellable'] == true) {
196 final availablePaymentTypes = [
lib/buy/kryptonim/kryptonim.dart new
+222
@@ -0,0 +1,222 @@
1 +import 'dart:convert';
2 +
3 +import 'package:cake_wallet/.secrets.g.dart' as secrets;
4 +import 'package:cake_wallet/buy/buy_provider.dart';
5 +import 'package:cake_wallet/buy/buy_quote.dart';
6 +import 'package:cake_wallet/buy/payment_method.dart';
7 +import 'package:cake_wallet/entities/fiat_currency.dart';
8 +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
9 +import 'package:cake_wallet/utils/show_pop_up.dart';
10 +import 'package:cw_core/crypto_currency.dart';
11 +import 'package:cw_core/wallet_base.dart';
12 +import 'package:flutter/material.dart';
13 +import 'dart:developer';
14 +import 'package:http/http.dart' as http;
15 +import 'package:url_launcher/url_launcher.dart';
16 +
17 +class KryptonimBuyProvider extends BuyProvider {
18 + KryptonimBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
19 + : super(wallet: wallet, isTestEnvironment: isTestEnvironment, ledgerVM: null);
20 +
21 + static const _isProduction = true;
22 +
23 + static const _baseUrl = _isProduction ? 'app.kryptonim.com' : 'intg-api.kryptonim.com';
24 + static const _baseWidgetUrl = _isProduction ? 'buy.kryptonim.com' : 'intg.kryptonim.com';
25 + static const _quotePath = '/v2/ramp/buy/quotes';
26 + static const _merchantId = 'a70fe053';
27 +
28 + static String get _kryptonimApiKey => secrets.kryptonimApiKey;
29 +
30 + @override
31 + String get title => 'Kryptonim';
32 +
33 + @override
34 + String get providerDescription => 'Kryptonim Buy Provider';
35 +
36 + @override
37 + String get lightIcon => 'assets/images/kryptonim_light.png';
38 +
39 + @override
40 + String get darkIcon => 'assets/images/kryptonim_dark.png';
41 +
42 + @override
43 + bool get isAggregator => false;
44 +
45 + Future<Map<String, dynamic>> getExchangeRates(
46 + {required CryptoCurrency cryptoCurrency,
47 + required String fiatCurrency,
48 + required double amount}) async {
49 + final url = Uri.https(_baseUrl, _quotePath, {'m': _merchantId});
50 +
51 + final headers = {
52 + 'accept': 'application/json',
53 + 'Content-Type': 'application/json',
54 + 'Authorization': _kryptonimApiKey,
55 + };
56 +
57 + final body = jsonEncode({
58 + 'amount': amount,
59 + 'currency': fiatCurrency,
60 + 'converted_currency': cryptoCurrency.title,
61 + 'blockchain': _normalizeBlockChain(cryptoCurrency),
62 + 'quote_currency': fiatCurrency,
63 + });
64 +
65 + try {
66 + final response = await http.post(url, headers: headers, body: body);
67 +
68 + if (response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 401) {
69 + return jsonDecode(response.body) as Map<String, dynamic>;
70 + } else {
71 + return {};
72 + }
73 + } catch (e) {
74 + return {};
75 + }
76 + }
77 +
78 + @override
79 + Future<List<PaymentMethod>> getAvailablePaymentTypes(
80 + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
81 +
82 + final data = await getExchangeRates(
83 + cryptoCurrency: cryptoCurrency,
84 + fiatCurrency: fiatCurrency,
85 + amount: 100.0,
86 + );
87 +
88 + if (data.isEmpty || !data.containsKey('data')) return [];
89 +
90 + final paymentMethods = (data['data'] as List<dynamic>)
91 + .map((e) => PaymentMethod.fromKryptonimJson(e as Map<String, dynamic>))
92 + .toList();
93 +
94 + return paymentMethods;
95 + }
96 +
97 + @override
98 + Future<List<Quote>?> fetchQuote({
99 + required CryptoCurrency cryptoCurrency,
100 + required FiatCurrency fiatCurrency,
101 + required double amount,
102 + required bool isBuyAction,
103 + required String walletAddress,
104 + PaymentType? paymentType,
105 + String? countryCode,
106 + }) async {
107 + log('Kryptonim: Fetching quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount');
108 +
109 + final data = await getExchangeRates(
110 + cryptoCurrency: cryptoCurrency,
111 + fiatCurrency: fiatCurrency.toString(),
112 + amount: amount,
113 + );
114 +
115 + if (!data.containsKey('data') || (data['data'] as List).isEmpty) {
116 + return null;
117 + }
118 +
119 + final quotesList = data['data'] as List<dynamic>;
120 +
121 + Map<String, dynamic>? selectedPaymentMethod;
122 +
123 + if (paymentType == PaymentType.all || paymentType == null) {
124 + selectedPaymentMethod = quotesList.first as Map<String, dynamic>;
125 + } else {
126 + for (var quote in quotesList) {
127 + final quotePaymentType = PaymentMethod.getPaymentTypeId(quote['payment_method'] as String?);
128 + if (quotePaymentType == paymentType) {
129 + selectedPaymentMethod = quote as Map<String, dynamic>;
130 + break;
131 + }
132 + }
133 + }
134 +
135 + if (selectedPaymentMethod == null) {
136 + return null;
137 + }
138 +
139 + final selectedPaymentType =
140 + PaymentMethod.getPaymentTypeId(selectedPaymentMethod['payment_method'] as String?);
141 + final quote = Quote.fromKryptonimJson(selectedPaymentMethod, isBuyAction, selectedPaymentType);
142 +
143 + quote.setFiatCurrency = fiatCurrency;
144 + quote.setCryptoCurrency = cryptoCurrency;
145 +
146 + return [quote];
147 + }
148 +
149 + @override
150 + Future<void>? launchProvider(
151 + {required BuildContext context,
152 + required Quote quote,
153 + required double amount,
154 + required bool isBuyAction,
155 + required String cryptoCurrencyAddress,
156 + String? countryCode}) async {
157 + final params = {
158 + 'amount': amount.toInt().toString(),
159 + 'currency': quote.fiatCurrency.name,
160 + 'convertedCurrency': quote.cryptoCurrency.title,
161 + 'blockchain': _normalizeBlockChain(quote.cryptoCurrency),
162 + 'address': cryptoCurrencyAddress,
163 + 'paymentMethod': normalizePaymentMethod(quote.paymentType),
164 + };
165 +
166 + final uri = Uri.https(_baseWidgetUrl, '/redirect-form', params);
167 +
168 + try {
169 + if (await canLaunchUrl(uri)) {
170 + await launchUrl(uri, mode: LaunchMode.externalApplication);
171 + } else {
172 + throw Exception('Could not launch URL');
173 + }
174 + } catch (e) {
175 + await showPopUp<void>(
176 + context: context,
177 + builder: (BuildContext context) {
178 + return AlertWithOneAction(
179 + alertTitle: "Kryptonim",
180 + alertContent: "Payment provider is unavailable: $e",
181 + buttonText: "OK",
182 + buttonAction: () => Navigator.of(context).pop(),
183 + );
184 + },
185 + );
186 + }
187 + }
188 +
189 + String normalizePaymentMethod(PaymentType paymentType) {
190 + switch (paymentType) {
191 + case PaymentType.bankTransfer:
192 + return 'bank';
193 + case PaymentType.creditCard:
194 + case PaymentType.debitCard:
195 + return 'card';
196 + default:
197 + return paymentType.name.toLowerCase();
198 + }
199 + }
200 +
201 + String _normalizeBlockChain(CryptoCurrency cur) {
202 + String? blockchain = switch (cur.tag) {
203 + 'ETH' => 'Ethereum',
204 + 'POL' => 'Polygon',
205 + 'AVAXC' => 'Avalanche',
206 + 'SOL' => 'Solana',
207 + _ => null,
208 + };
209 +
210 + if (blockchain == null) {
211 + blockchain = switch (cur) {
212 + CryptoCurrency.btc => 'Bitcoin',
213 + CryptoCurrency.ltc => 'Litecoin',
214 + CryptoCurrency.eth => 'Ethereum',
215 + CryptoCurrency.maticpoly => 'Matic',
216 + _ => null,
217 + };
218 + }
219 +
220 + return blockchain ?? cur.fullName ?? '';
221 + }
222 +}
lib/buy/meld/meld_buy_provider.dart
+1 -1
@@ -53,7 +53,7 @@ class MeldBuyProvider extends BuyProvider {
53
54 @override
55 Future<List<PaymentMethod>> getAvailablePaymentTypes(
56 - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
56 + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
57 final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'};
58
59 final path = '$_providersProperties$_paymentMethodsPath';
lib/buy/moonpay/moonpay_provider.dart
+2 -2
@@ -126,11 +126,11 @@ class MoonPayProvider extends BuyProvider {
126 }
127
128 Future<List<PaymentMethod>> getAvailablePaymentTypes(
129 - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
129 + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
130 final List<PaymentMethod> paymentMethods = [];
131
132 if (isBuyAction) {
133 - final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency, null);
133 + final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency.title, null);
134 if (fiatBuyCredentials.isNotEmpty) {
135 final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?;
136 paymentMethods.add(PaymentMethod.fromMoonPayJson(
lib/buy/onramper/onramper_buy_provider.dart
+1 -1
@@ -48,7 +48,7 @@ class OnRamperBuyProvider extends BuyProvider {
48 bool get isAggregator => true;
49
50 Future<List<PaymentMethod>> getAvailablePaymentTypes(
51 - String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
51 + String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
52 final params = {
53 'fiatCurrency': fiatCurrency,
54 'type': isBuyAction ? 'buy' : 'sell',
lib/buy/payment_method.dart
+9
@@ -218,6 +218,15 @@ class PaymentMethod extends SelectableOption {
218 customDescription: json['description'] as String?);
219 }
220
221 + factory PaymentMethod.fromKryptonimJson(Map<String, dynamic> json) {
222 + final type = PaymentMethod.getPaymentTypeId(json['payment_method'] as String?);
223 + return PaymentMethod(
224 + paymentMethodType: type,
225 + customTitle: json['payment_method'] as String? ?? 'Unknown',
226 + customIconPath: 'assets/images/card.png',
227 + );
228 + }
229 +
230 static PaymentType getPaymentTypeId(String? type) {
231 switch (type?.toLowerCase()) {
232 case 'banktransfer':
lib/di.dart
+5
@@ -255,6 +255,7 @@ import 'package:get_it/get_it.dart';
255 import 'package:hive/hive.dart';
256 import 'package:mobx/mobx.dart';
257 import 'package:shared_preferences/shared_preferences.dart';
258 +import 'buy/kryptonim/kryptonim.dart';
259 import 'buy/meld/meld_buy_provider.dart';
260 import 'src/screens/buy/buy_sell_page.dart';
261 import 'cake_pay/cake_pay_payment_credantials.dart';
@@ -1019,6 +1020,10 @@ Future<void> setup({
1020 wallet: getIt.get<AppStore>().wallet!,
1021 ));
1022
1023 + getIt.registerFactory<KryptonimBuyProvider>(() => KryptonimBuyProvider(
1024 + wallet: getIt.get<AppStore>().wallet!,
1025 + ));
1026 +
1027 getIt.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri));
1028
1029 getIt.registerFactory(() => ExchangeViewModel(
lib/entities/provider_types.dart
+12 -2
@@ -1,5 +1,6 @@
1 import 'package:cake_wallet/buy/buy_provider.dart';
2 import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
3 +import 'package:cake_wallet/buy/kryptonim/kryptonim.dart';
4 import 'package:cake_wallet/buy/meld/meld_buy_provider.dart';
5 import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
6 import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
@@ -8,7 +9,7 @@ import 'package:cake_wallet/di.dart';
9 import 'package:cw_core/wallet_type.dart';
10 import 'package:http/http.dart';
11
11 -enum ProviderType { robinhood, dfx, onramper, moonpay, meld }
12 +enum ProviderType { robinhood, dfx, onramper, moonpay, meld, kriptonim }
13
14 extension ProviderTypeName on ProviderType {
15 String get title {
@@ -23,6 +24,8 @@ extension ProviderTypeName on ProviderType {
24 return 'MoonPay';
25 case ProviderType.meld:
26 return 'Meld';
27 + case ProviderType.kriptonim:
28 + return 'Kriptonim';
29 }
30 }
31
@@ -38,6 +41,8 @@ extension ProviderTypeName on ProviderType {
41 return 'moonpay_provider';
42 case ProviderType.meld:
43 return 'meld_provider';
44 + case ProviderType.kriptonim:
45 + return 'kriptonim_provider';
46 }
47 }
48 }
@@ -59,6 +64,7 @@ class ProvidersHelper {
64 ProviderType.dfx,
65 ProviderType.robinhood,
66 ProviderType.moonpay,
67 + ProviderType.kriptonim
68 ];
69 case WalletType.litecoin:
70 case WalletType.bitcoinCash:
@@ -66,13 +72,15 @@ class ProvidersHelper {
72 return [
73 ProviderType.onramper,
74 ProviderType.robinhood,
69 - ProviderType.moonpay
75 + ProviderType.moonpay,
76 + ProviderType.kriptonim
77 ];
78 case WalletType.tron:
79 return [
80 ProviderType.onramper,
81 ProviderType.robinhood,
82 ProviderType.moonpay,
83 + ProviderType.kriptonim
84 ];
85 case WalletType.none:
86 case WalletType.haven:
@@ -127,6 +135,8 @@ class ProvidersHelper {
135 return getIt.get<MoonPayProvider>();
136 case ProviderType.meld:
137 return getIt.get<MeldBuyProvider>();
138 + case ProviderType.kriptonim:
139 + return getIt.get<KryptonimBuyProvider>();
140 default:
141 return null;
142 }
lib/view_model/buy/buy_sell_view_model.dart
+1 -1
@@ -351,7 +351,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
351 paymentMethodState = PaymentMethodLoading();
352 selectedPaymentMethod = null;
353 final result = await Future.wait(providerList.map((element) => element
354 - .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency.title, isBuyAction)
354 + .getAvailablePaymentTypes(fiatCurrency.title, cryptoCurrency, isBuyAction)
355 .timeout(
356 Duration(seconds: 10),
357 onTimeout: () => [],
tool/utils/secret_key.dart
+1
@@ -77,6 +77,7 @@ class SecretKey {
77 SecretKey('moneroTestWalletBlockHeight', () => ''),
78 SecretKey('chainflipApiKey', () => ''),
79 SecretKey('chainflipAffiliateFee', () => ''),
80 + SecretKey('kryptonimApiKey', () => ''),
81 SecretKey('walletGroupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)),
82 ];
83