dev
dart 422 lines 14.9 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_exception.dart';
6 import 'package:cake_wallet/buy/buy_provider.dart';
7 import 'package:cake_wallet/buy/buy_provider_description.dart';
8 import 'package:cake_wallet/buy/buy_quote.dart';
9 import 'package:cake_wallet/order/order.dart';
10 import 'package:cake_wallet/order/order_source_description.dart';
11 import 'package:cake_wallet/buy/pairs_utils.dart';
12 import 'package:cake_wallet/buy/payment_method.dart';
13 import 'package:cake_wallet/entities/fiat_currency.dart';
14 import 'package:cake_wallet/exchange/trade_state.dart';
15 import 'package:cake_wallet/generated/i18n.dart';
16 import 'package:cake_wallet/palette.dart';
17 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
18 import 'package:cake_wallet/store/app_store.dart';
19 import 'package:cake_wallet/themes/core/material_base_theme.dart';
20 import 'package:cw_core/currency_for_wallet_type.dart';
21 import 'package:cw_core/utils/proxy_wrapper.dart';
22 import 'package:cw_core/crypto_currency.dart';
23 import 'package:cw_core/wallet_base.dart';
24 import 'package:cw_core/utils/print_verbose.dart';
25 import 'package:flutter/material.dart';
26 import 'package:url_launcher/url_launcher.dart';
27
28 class MoonPayProvider extends BuyProvider {
29 MoonPayProvider({
30 required AppStore appStore,
31 required WalletBase wallet,
32 bool isTestEnvironment = false,
33 }) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl,
34 baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl,
35 this._appStore = appStore,
36 super(
37 wallet: wallet,
38 isTestEnvironment: isTestEnvironment,
39 hardwareWalletVM: null,
40 supportedCryptoList: supportedCryptoToFiatPairs(
41 notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat),
42 supportedFiatList: supportedFiatToCryptoPairs(
43 notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto));
44
45 final AppStore _appStore;
46
47 static const _baseSellTestUrl = 'sell-sandbox.moonpay.com';
48 static const _baseSellProductUrl = 'sell.moonpay.com';
49 static const _baseBuyTestUrl = 'buy-staging.moonpay.com';
50 static const _baseBuyProductUrl = 'buy.moonpay.com';
51 static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
52 static const _apiUrl = 'https://api.moonpay.com';
53 static const _baseUrl = 'api.moonpay.com';
54 static const _currenciesPath = '/v3/currencies';
55 static const _buyQuote = '/buy_quote';
56 static const _sellQuote = '/sell_quote';
57
58 static const _transactionsSuffix = '/v1/transactions';
59
60 static const List<CryptoCurrency> _notSupportedCrypto = [];
61 static const List<FiatCurrency> _notSupportedFiat = [];
62
63 final String baseBuyUrl;
64 final String baseSellUrl;
65
66 @override
67 String get providerDescription =>
68 'MoonPay offers a fast and simple way to buy and sell cryptocurrencies';
69
70 @override
71 String get title => 'MoonPay';
72
73 @override
74 String get lightIcon => 'assets/images/moonpay_light.png';
75
76 @override
77 String get darkIcon => 'assets/images/moonpay_dark.png';
78
79 @override
80 bool get isAggregator => false;
81
82 String get _apiKey => isTestEnvironment ? secrets.moonPaySandboxApiKey : secrets.moonPayApiKey;
83
84 String get currencyCode =>
85 walletTypeToCryptoCurrency(wallet.type, chainId: wallet.chainId).title.toLowerCase();
86
87 String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
88
89 static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
90
91 static String themeToMoonPayTheme(MaterialThemeBase theme) {
92 switch (theme.type) {
93 case ThemeType.light:
94 return 'light';
95 case ThemeType.dark:
96 return 'dark';
97 }
98 }
99
100 Future<String> getMoonpaySignedQuery(String query) async {
101 final uri =
102 Uri.https(_cIdBaseUrl, "/api/moonpay", isTestEnvironment ? {"useSandbox": "true"} : null);
103
104 final response = await ProxyWrapper().post(
105 clearnetUri: uri,
106 headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey},
107 body: json.encode({'query': query}),
108 );
109
110 if (response.statusCode == 200) {
111 printV((jsonDecode(response.body) as Map<String, dynamic>));
112 return (jsonDecode(response.body) as Map<String, dynamic>)['query'] as String;
113 } else {
114 throw Exception(
115 'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
116 }
117 }
118
119 Future<Map<String, dynamic>> fetchFiatCredentials(
120 String fiatCurrency, String cryptocurrency, String? paymentMethod) async {
121 final params = {'baseCurrencyCode': fiatCurrency.toLowerCase(), 'apiKey': _apiKey};
122
123 if (paymentMethod != null) params['paymentMethod'] = paymentMethod;
124
125 final path = '$_currenciesPath/${cryptocurrency.toLowerCase()}/limits';
126 final url = Uri.https(_baseUrl, path, params);
127
128 try {
129 final response = await ProxyWrapper().get(
130 clearnetUri: url,
131 headers: {'accept': 'application/json'},
132 );
133
134 if (response.statusCode == 200) {
135 return jsonDecode(response.body) as Map<String, dynamic>;
136 } else {
137 printV('MoonPay does not support fiat: $fiatCurrency');
138 return {};
139 }
140 } catch (e) {
141 printV('MoonPay Error fetching fiat currencies: $e');
142 return {};
143 }
144 }
145
146 Future<List<PaymentMethod>> getAvailablePaymentTypes(
147 String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
148 final List<PaymentMethod> paymentMethods = [];
149
150 if (isBuyAction) {
151 final fiatBuyCredentials =
152 await fetchFiatCredentials(fiatCurrency, cryptoCurrency.title, null);
153 if (fiatBuyCredentials.isNotEmpty) {
154 final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?;
155 paymentMethods.add(PaymentMethod.fromMoonPayJson(
156 fiatBuyCredentials, _getPaymentTypeByString(paymentMethod)));
157 return paymentMethods;
158 }
159 }
160
161 return paymentMethods;
162 }
163
164 @override
165 Future<List<Quote>?> fetchQuote(
166 {required CryptoCurrency cryptoCurrency,
167 required FiatCurrency fiatCurrency,
168 required double amount,
169 required bool isBuyAction,
170 required String walletAddress,
171 PaymentType? paymentType,
172 String? customPaymentMethodType,
173 String? countryCode}) async {
174 String? paymentMethod;
175
176 if (paymentType != null && paymentType != PaymentType.all) {
177 paymentMethod = normalizePaymentMethod(paymentType);
178 if (paymentMethod == null) paymentMethod = paymentType.name;
179 } else {
180 paymentMethod = 'credit_debit_card';
181 }
182
183 final action = isBuyAction ? 'buy' : 'sell';
184
185 final formattedCryptoCurrency = _normalizeCurrency(cryptoCurrency);
186 final baseCurrencyCode =
187 isBuyAction ? fiatCurrency.name.toLowerCase() : cryptoCurrency.title.toLowerCase();
188
189 final params = {
190 'baseCurrencyCode': baseCurrencyCode,
191 'baseCurrencyAmount': amount.toStringAsFixed(2),
192 'paymentMethod': paymentMethod,
193 'areFeesIncluded': 'false',
194 'apiKey': _apiKey
195 };
196
197 log('MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
198
199 final quotePath = isBuyAction ? _buyQuote : _sellQuote;
200
201 final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath';
202 final url = Uri.https(_baseUrl, path, params);
203 try {
204 final response = await ProxyWrapper().get(clearnetUri: url);
205
206 if (response.statusCode == 200) {
207 final data = jsonDecode(response.body) as Map<String, dynamic>;
208
209 // Check if the response is for the correct fiat currency
210 if (isBuyAction) {
211 final fiatCurrencyCode = data['baseCurrencyCode'] as String?;
212 if (fiatCurrencyCode == null || fiatCurrencyCode != fiatCurrency.name.toLowerCase())
213 return null;
214 } else {
215 final quoteCurrency = data['quoteCurrency'] as Map<String, dynamic>?;
216 if (quoteCurrency == null || quoteCurrency['code'] != fiatCurrency.name.toLowerCase())
217 return null;
218 }
219
220 final paymentMethods = data['paymentMethod'] as String?;
221 final quote =
222 Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods));
223
224 quote.setFiatCurrency = fiatCurrency;
225 quote.setCryptoCurrency = cryptoCurrency;
226
227 return [quote];
228 } else {
229 printV('Moon Pay: Error fetching buy quote: ');
230 return null;
231 }
232 } catch (e) {
233 printV('Moon Pay: Error fetching buy quote: $e');
234 return null;
235 }
236 }
237
238 @override
239 Future<void>? launchProvider(
240 {required BuildContext context,
241 required Quote quote,
242 required double amount,
243 required bool isBuyAction,
244 required String cryptoCurrencyAddress,
245 String? countryCode}) async {
246 final Map<String, String> params = {
247 'theme': themeToMoonPayTheme(_appStore.themeStore.currentTheme),
248 'language': _appStore.settingsStore.languageCode,
249 'colorCode': _appStore.themeStore.currentTheme.isDark
250 ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
251 : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
252 'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name,
253 'baseCurrencyAmount': amount.toStringAsFixed(2),
254 'walletAddress': cryptoCurrencyAddress,
255 'lockAmount': 'false',
256 'showAllCurrencies': 'false',
257 'showWalletAddressForm': 'false',
258 if (isBuyAction)
259 'enabledPaymentMethods': normalizePaymentMethod(quote.paymentType) ??
260 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
261 if (!isBuyAction) 'refundWalletAddress': cryptoCurrencyAddress
262 };
263
264 if (isBuyAction) params['currencyCode'] = quote.cryptoCurrency.name;
265 if (!isBuyAction) params['quoteCurrencyCode'] = quote.cryptoCurrency.name;
266
267 try {
268 final uri = await requestMoonPayUrl(
269 walletAddress: cryptoCurrencyAddress,
270 isBuyAction: isBuyAction,
271 amount: amount.toString(),
272 params: params);
273
274 if (await canLaunchUrl(uri)) {
275 await launchUrl(uri, mode: LaunchMode.externalApplication);
276 } else {
277 throw Exception('Could not launch URL');
278 }
279 } catch (e) {
280 if (context.mounted) {
281 await showDialog<void>(
282 context: context,
283 builder: (BuildContext context) {
284 return AlertWithOneAction(
285 alertTitle: 'MoonPay',
286 alertContent: 'The MoonPay service is currently unavailable: $e',
287 buttonText: S.of(context).ok,
288 buttonAction: () => Navigator.of(context).pop(),
289 );
290 },
291 );
292 }
293 }
294 }
295
296 Future<Uri> requestMoonPayUrl({
297 required String walletAddress,
298 required bool isBuyAction,
299 required Map<String, String> params,
300 String? amount,
301 }) async {
302 if (_apiKey.isNotEmpty) params["apiKey"] = _apiKey;
303
304 final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl;
305 final originalUri = Uri.https(baseUrl, "", params);
306
307 final query = await getMoonpaySignedQuery("?${originalUri.query}");
308 return Uri.parse(query);
309 }
310
311 Future<Order> findOrderById(String id) async {
312 final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey;
313 final uri = Uri.parse(url);
314 final response = await ProxyWrapper().get(clearnetUri: uri);
315
316 if (response.statusCode != 200) {
317 throw BuyException(title: providerDescription, content: 'Transaction $id is not found!');
318 }
319
320 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
321 final status = responseJSON['status'] as String;
322 final state = TradeState.deserialize(raw: status);
323 final createdAtRaw = responseJSON['createdAt'] as String;
324 final createdAt = DateTime.parse(createdAtRaw).toLocal();
325 final amount = responseJSON['quoteCurrencyAmount'] as double;
326
327 return Order(
328 id: id,
329 source: OrderSourceDescription.buy,
330 buyProvider: BuyProviderDescription.moonPay,
331 transferId: id,
332 state: state,
333 createdAt: createdAt,
334 amount: amount.toString(),
335 receiveAddress: wallet.walletAddresses.address,
336 walletId: wallet.id);
337 }
338
339 String _normalizeCurrency(CryptoCurrency currency) {
340 if (currency.tag == 'POLY') {
341 return '${currency.title.toLowerCase()}_polygon';
342 }
343
344 if (currency.tag == 'TRX') {
345 return '${currency.title.toLowerCase()}_trx';
346 }
347
348 return currency.toString().toLowerCase();
349 }
350
351 String? normalizePaymentMethod(PaymentType paymentMethod) {
352 switch (paymentMethod) {
353 case PaymentType.creditCard:
354 return 'credit_debit_card';
355 case PaymentType.debitCard:
356 return 'credit_debit_card';
357 case PaymentType.ach:
358 return 'ach_bank_transfer';
359 case PaymentType.applePay:
360 return 'apple_pay';
361 case PaymentType.googlePay:
362 return 'google_pay';
363 case PaymentType.sepa:
364 return 'sepa_bank_transfer';
365 case PaymentType.paypal:
366 return 'paypal';
367 case PaymentType.sepaOpenBankingPayment:
368 return 'sepa_open_banking_payment';
369 case PaymentType.gbpOpenBankingPayment:
370 return 'gbp_open_banking_payment';
371 case PaymentType.lowCostAch:
372 return 'low_cost_ach';
373 case PaymentType.mobileWallet:
374 return 'mobile_wallet';
375 case PaymentType.pixInstantPayment:
376 return 'pix_instant_payment';
377 case PaymentType.yellowCardBankTransfer:
378 return 'yellow_card_bank_transfer';
379 case PaymentType.fiatBalance:
380 return 'fiat_balance';
381 case PaymentType.revolutPay:
382 return 'revolut_pay';
383 default:
384 return null;
385 }
386 }
387
388 PaymentType _getPaymentTypeByString(String? paymentMethod) {
389 switch (paymentMethod) {
390 case 'ach_bank_transfer':
391 return PaymentType.ach;
392 case 'apple_pay':
393 return PaymentType.applePay;
394 case 'credit_debit_card':
395 return PaymentType.creditCard;
396 case 'fiat_balance':
397 return PaymentType.fiatBalance;
398 case 'gbp_open_banking_payment':
399 return PaymentType.gbpOpenBankingPayment;
400 case 'google_pay':
401 return PaymentType.googlePay;
402 case 'low_cost_ach':
403 return PaymentType.lowCostAch;
404 case 'mobile_wallet':
405 return PaymentType.mobileWallet;
406 case 'paypal':
407 return PaymentType.paypal;
408 case 'pix_instant_payment':
409 return PaymentType.pixInstantPayment;
410 case 'sepa_bank_transfer':
411 return PaymentType.sepa;
412 case 'sepa_open_banking_payment':
413 return PaymentType.sepaOpenBankingPayment;
414 case 'yellow_card_bank_transfer':
415 return PaymentType.yellowCardBankTransfer;
416 case 'revolut_pay':
417 return PaymentType.revolutPay;
418 default:
419 return PaymentType.unknown;
420 }
421 }
422 }