dev
dart 451 lines 15 KB
Raw
1 import 'dart:convert';
2 import 'dart:developer';
3
4 import 'package:cake_wallet/.secrets.g.dart' as secrets;
5 import 'package:cake_wallet/buy/buy_provider.dart';
6 import 'package:cake_wallet/buy/buy_quote.dart';
7 import 'package:cake_wallet/buy/pairs_utils.dart';
8 import 'package:cake_wallet/buy/payment_method.dart';
9 import 'package:cake_wallet/entities/fiat_currency.dart';
10 import 'package:cake_wallet/generated/i18n.dart';
11 import 'package:cw_core/utils/proxy_wrapper.dart';
12 import 'package:cw_core/crypto_currency.dart';
13 import 'package:cw_core/utils/print_verbose.dart';
14 import 'package:cw_core/wallet_base.dart';
15 import 'package:flutter/material.dart';
16 import 'package:url_launcher/url_launcher.dart';
17
18 class OnRamperBuyProvider extends BuyProvider {
19 OnRamperBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
20 : super(
21 wallet: wallet,
22 isTestEnvironment: isTestEnvironment,
23 hardwareWalletVM: null,
24 supportedCryptoList: supportedCryptoToFiatPairs(
25 notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat),
26 supportedFiatList: supportedFiatToCryptoPairs(
27 notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto));
28
29 static const _baseUrl = 'buy.onramper.com';
30 static const _baseApiUrl = 'api.onramper.com';
31 static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
32 static const quotes = '/quotes';
33 static const paymentTypes = '/payment-types';
34 static const supported = '/supported';
35 static const defaultsAll = '/defaults/all';
36
37 static const List<CryptoCurrency> _notSupportedCrypto = [];
38 static const List<FiatCurrency> _notSupportedFiat = [];
39 static Map<String, dynamic> _onrampMetadata = {};
40
41 String? recommendedPaymentType;
42
43 String get _apiKey => secrets.onramperApiKey;
44
45 String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
46
47 @override
48 String get title => 'Onramper';
49
50 @override
51 String get providerDescription => S.current.onramper_option_description;
52
53 @override
54 String get lightIcon => 'assets/images/onramper_light.png';
55
56 @override
57 String get darkIcon => 'assets/images/onramper_dark.png';
58
59 @override
60 bool get isAggregator => true;
61
62 Future<String> getOnramperSignature(String query) async {
63 final uri = Uri.https(_cIdBaseUrl, "/api/onramper");
64
65 final response = await ProxyWrapper().post(
66 clearnetUri: uri,
67 headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey},
68 body: json.encode({'query': query}),
69 );
70
71 if (response.statusCode == 200) {
72 return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
73 } else {
74 throw Exception(
75 'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
76 }
77 }
78
79 Future<String?> getRecommendedPaymentType(bool isBuyAction) async {
80 final params = {'type': isBuyAction ? 'buy' : 'sell'};
81
82 final url = Uri.https(_baseApiUrl, '$supported$defaultsAll', params);
83
84 try {
85 final response = await ProxyWrapper().get(
86 clearnetUri: url,
87 headers: {'Authorization': _apiKey, 'accept': 'application/json'},
88 );
89
90 if (response.statusCode == 200) {
91 final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
92 final recommended = data['message']['recommended'] as Map<String, dynamic>;
93
94 final recommendedPaymentType = recommended['paymentMethod'] as String?;
95
96 return recommendedPaymentType;
97 } else {
98 final responseBody = jsonDecode(response.body) as Map<String, dynamic>;
99 printV('Failed to fetch available payment types: ${responseBody['message']}');
100 }
101 } catch (e) {
102 printV('Failed to fetch available payment types: $e');
103 }
104 return null;
105 }
106
107 Future<List<PaymentMethod>> getAvailablePaymentTypes(
108 String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
109 final normalizedCryptoCurrency = cryptoCurrency.title + _getNormalizeNetwork(cryptoCurrency);
110
111 final sourceCurrency = (isBuyAction ? fiatCurrency : normalizedCryptoCurrency).toLowerCase();
112 final destinationCurrency =
113 (isBuyAction ? normalizedCryptoCurrency : fiatCurrency).toLowerCase();
114
115 final params = {'type': isBuyAction ? 'buy' : 'sell', 'destination': destinationCurrency};
116
117 final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$sourceCurrency', params);
118
119 try {
120 final response = await ProxyWrapper()
121 .get(clearnetUri: url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
122
123 if (response.statusCode == 200) {
124 final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
125 final List<dynamic> message = data['message'] as List<dynamic>;
126
127 final allAvailablePaymentMethods = message
128 .map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>))
129 .toList();
130
131 recommendedPaymentType = await getRecommendedPaymentType(isBuyAction);
132
133 return allAvailablePaymentMethods;
134 } else {
135 final responseBody = jsonDecode(response.body) as Map<String, dynamic>;
136 printV('Failed to fetch available payment types: ${responseBody['message']}');
137 return [];
138 }
139 } catch (e) {
140 printV('Failed to fetch available payment types: $e');
141 return [];
142 }
143 }
144
145 Future<Map<String, dynamic>> getOnrampMetadata() async {
146 final url = Uri.https(_baseApiUrl, '$supported/onramps/all');
147
148 try {
149 final response = await ProxyWrapper()
150 .get(clearnetUri: url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
151
152 if (response.statusCode == 200) {
153 final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
154
155 final List<dynamic> onramps = data['message'] as List<dynamic>;
156
157 final Map<String, dynamic> result = {
158 for (var onramp in onramps)
159 (onramp['id'] as String): {
160 'displayName': onramp['displayName'] as String,
161 'svg': onramp['icons']['svg'] as String
162 }
163 };
164
165 return result;
166 } else {
167 printV('Failed to fetch onramp metadata');
168 return {};
169 }
170 } catch (e) {
171 printV('Error occurred: $e');
172 return {};
173 }
174 }
175
176 @override
177 Future<List<Quote>?> fetchQuote(
178 {required CryptoCurrency cryptoCurrency,
179 required FiatCurrency fiatCurrency,
180 required double amount,
181 required bool isBuyAction,
182 required String walletAddress,
183 PaymentType? paymentType,
184 String? customPaymentMethodType,
185 String? countryCode}) async {
186 String? paymentMethod;
187
188 if (paymentType == PaymentType.all)
189 paymentMethod = null;
190 else if (paymentType == PaymentType.unknown)
191 paymentMethod = customPaymentMethodType;
192 else if (paymentType != null) paymentMethod = normalizePaymentMethod(paymentType);
193
194 final actionType = isBuyAction ? 'buy' : 'sell';
195
196 final normalizedCryptoCurrency =
197 cryptoCurrency.title + _getNormalizeNetwork(cryptoCurrency).toUpperCase();
198
199 final params = {
200 'amount': amount.toString(),
201 if (paymentMethod != null) 'paymentMethod': paymentMethod,
202 'clientName': 'CakeWallet',
203 if (actionType == 'sell') 'type': actionType,
204 };
205
206 log('Onramper: Fetching $actionType quote: ${isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name} -> ${isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
207
208 final sourceCurrency = isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency;
209 final destinationCurrency = isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name;
210
211 final url = Uri.https(_baseApiUrl, '$quotes/${sourceCurrency}/${destinationCurrency}', params);
212 final headers = {'Authorization': _apiKey, 'accept': 'application/json'};
213
214 try {
215 final response = await ProxyWrapper().get(clearnetUri: url, headers: headers);
216
217 if (response.statusCode == 200) {
218 final data = jsonDecode(response.body) as List<dynamic>;
219 if (data.isEmpty) return null;
220
221 List<Quote> validQuotes = [];
222
223 if (_onrampMetadata.isEmpty) _onrampMetadata = await getOnrampMetadata();
224
225 for (var item in data) {
226 if (item['errors'] != null) continue;
227
228 final paymentMethod = (item as Map<String, dynamic>)['paymentMethod'] as String;
229
230 final rampId = item['ramp'] as String?;
231 final rampMetaData = _onrampMetadata[rampId] as Map<String, dynamic>?;
232
233 if (rampMetaData == null) continue;
234
235 final quote = Quote.fromOnramperJson(item, isBuyAction, _onrampMetadata,
236 _getPaymentTypeByString(paymentMethod), customPaymentMethodType);
237 quote.setFiatCurrency = fiatCurrency;
238 quote.setCryptoCurrency = cryptoCurrency;
239 validQuotes.add(quote);
240 }
241
242 if (validQuotes.isEmpty) return null;
243
244 return validQuotes;
245 } else {
246 printV('Onramper: Failed to fetch rate');
247 return null;
248 }
249 } catch (e) {
250 printV('Onramper: Failed to fetch rate $e');
251 return null;
252 }
253 }
254
255 Future<void>? launchProvider(
256 {required BuildContext context,
257 required Quote quote,
258 required double amount,
259 required bool isBuyAction,
260 required String cryptoCurrencyAddress,
261 String? countryCode}) async {
262 final actionType = isBuyAction ? 'buy' : 'sell';
263
264 final primaryColor = getColorStr(
265 Theme.of(context).colorScheme.primary,
266 );
267 final secondaryColor = getColorStr(Theme.of(context).colorScheme.surface);
268 final primaryTextColor = getColorStr(Theme.of(context).colorScheme.onSurface);
269 final secondaryTextColor = getColorStr(Theme.of(context).colorScheme.onSurfaceVariant);
270 final containerColor = getColorStr(Theme.of(context).colorScheme.surface);
271 var cardColor = getColorStr(Theme.of(context).colorScheme.surfaceContainer);
272
273 final defaultCrypto =
274 quote.cryptoCurrency.title + _getNormalizeNetwork(quote.cryptoCurrency).toLowerCase();
275
276 final paymentMethod = quote.paymentType == PaymentType.unknown
277 ? quote.customPaymentMethodType
278 : normalizePaymentMethod(quote.paymentType);
279
280 final networkWallets =
281 '${_tagToNetwork(quote.cryptoCurrency.tag ?? quote.cryptoCurrency.title).toLowerCase()}:$cryptoCurrencyAddress';
282
283 final signature = await getOnramperSignature("networkWallets=$networkWallets");
284
285 final uri = Uri.https(_baseUrl, '', {
286 'apiKey': _apiKey,
287 'txnType': actionType,
288 'txnFiat': quote.fiatCurrency.name,
289 'txnCrypto': defaultCrypto,
290 'txnAmount': amount.toString(),
291 'skipTransactionScreen': "true",
292 if (paymentMethod != null) 'txnPaymentMethod': paymentMethod,
293 'txnOnramp': quote.rampId,
294 'networkWallets': networkWallets,
295 'supportSwap': "false",
296 'primaryColor': primaryColor,
297 'secondaryColor': secondaryColor,
298 'containerColor': containerColor,
299 'primaryTextColor': primaryTextColor,
300 'secondaryTextColor': secondaryTextColor,
301 'cardColor': cardColor,
302 'signature': signature,
303 });
304
305 if (await canLaunchUrl(uri)) {
306 await launchUrl(uri, mode: LaunchMode.externalApplication);
307 } else {
308 throw Exception('Could not launch URL ${uri.toString()}');
309 }
310 }
311
312 List<CryptoCurrency> mainCurrency = [
313 CryptoCurrency.btc,
314 CryptoCurrency.eth,
315 CryptoCurrency.sol,
316 ];
317
318 String _tagToNetwork(String tag) {
319 switch (tag) {
320 case 'POL':
321 return 'POLYGON';
322 case 'ARB':
323 return 'ARBITRUM';
324 case 'ETH':
325 return 'ETHEREUM';
326 case 'TRX':
327 return 'TRON';
328 case 'SOL':
329 return 'SOLANA';
330 case 'ZEC':
331 return 'ZCASH';
332 default:
333 return tag;
334 }
335 }
336
337 String _getNormalizeNetwork(CryptoCurrency currency) {
338 if (mainCurrency.contains(currency)) return '';
339
340 if (currency == CryptoCurrency.eos) return '_EOSIO';
341
342 if (currency.tag != null) return '_' + _tagToNetwork(currency.tag!);
343
344 return '_' + (currency.fullName?.replaceAll(' ', '') ?? currency.title);
345 }
346
347 String? normalizePaymentMethod(PaymentType paymentType) {
348 switch (paymentType) {
349 case PaymentType.bankTransfer:
350 return 'banktransfer';
351 case PaymentType.creditCard:
352 return 'creditcard';
353 case PaymentType.debitCard:
354 return 'debitcard';
355 case PaymentType.applePay:
356 return 'applepay';
357 case PaymentType.googlePay:
358 return 'googlepay';
359 case PaymentType.revolutPay:
360 return 'revolutpay';
361 case PaymentType.neteller:
362 return 'neteller';
363 case PaymentType.skrill:
364 return 'skrill';
365 case PaymentType.sepa:
366 return 'sepabanktransfer';
367 case PaymentType.sepaInstant:
368 return 'sepainstant';
369 case PaymentType.ach:
370 return 'ach';
371 case PaymentType.achInstant:
372 return 'iach';
373 case PaymentType.Khipu:
374 return 'khipu';
375 case PaymentType.palomaBanktTansfer:
376 return 'palomabanktransfer';
377 case PaymentType.ovo:
378 return 'ovo';
379 case PaymentType.zaloPay:
380 return 'zalopay';
381 case PaymentType.zaloBankTransfer:
382 return 'zalobanktransfer';
383 case PaymentType.gcash:
384 return 'gcash';
385 case PaymentType.imps:
386 return 'imps';
387 case PaymentType.dana:
388 return 'dana';
389 case PaymentType.ideal:
390 return 'ideal';
391 case PaymentType.pixPay:
392 return 'pix';
393 default:
394 return null;
395 }
396 }
397
398 PaymentType _getPaymentTypeByString(String paymentMethod) {
399 switch (paymentMethod.toLowerCase()) {
400 case 'banktransfer':
401 return PaymentType.bankTransfer;
402 case 'creditcard':
403 return PaymentType.creditCard;
404 case 'debitcard':
405 return PaymentType.debitCard;
406 case 'applepay':
407 return PaymentType.applePay;
408 case 'googlepay':
409 return PaymentType.googlePay;
410 case 'revolutpay':
411 return PaymentType.revolutPay;
412 case 'neteller':
413 return PaymentType.neteller;
414 case 'skrill':
415 return PaymentType.skrill;
416 case 'sepabanktransfer':
417 return PaymentType.sepa;
418 case 'sepainstant':
419 return PaymentType.sepaInstant;
420 case 'ach':
421 return PaymentType.ach;
422 case 'iach':
423 return PaymentType.achInstant;
424 case 'khipu':
425 return PaymentType.Khipu;
426 case 'palomabanktransfer':
427 return PaymentType.palomaBanktTansfer;
428 case 'ovo':
429 return PaymentType.ovo;
430 case 'zalopay':
431 return PaymentType.zaloPay;
432 case 'zalobanktransfer':
433 return PaymentType.zaloBankTransfer;
434 case 'gcash':
435 return PaymentType.gcash;
436 case 'imps':
437 return PaymentType.imps;
438 case 'dana':
439 return PaymentType.dana;
440 case 'ideal':
441 return PaymentType.ideal;
442 case 'pix':
443 return PaymentType.pixPay;
444 default:
445 return PaymentType.unknown;
446 }
447 }
448
449 String getColorStr(Color color) =>
450 color.toARGB32().toRadixString(16).replaceAll(RegExp(r'^ff'), "");
451 }