1
import 'dart:convert';
2
+import 'dart:developer';
3
4
import 'package:cake_wallet/.secrets.g.dart' as secrets;
4
-import 'package:cake_wallet/buy/buy_amount.dart';
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/buy/order.dart';
10
+import 'package:cake_wallet/buy/payment_method.dart';
11
+import 'package:cake_wallet/entities/fiat_currency.dart';
12
import 'package:cake_wallet/exchange/trade_state.dart';
13
import 'package:cake_wallet/generated/i18n.dart';
14
import 'package:cake_wallet/palette.dart';
12
-import 'package:cake_wallet/routes.dart';
15
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
16
import 'package:cake_wallet/store/settings_store.dart';
17
import 'package:cake_wallet/themes/theme_base.dart';
16
-import 'package:cake_wallet/utils/device_info.dart';
18
import 'package:cw_core/crypto_currency.dart';
19
import 'package:cw_core/wallet_base.dart';
20
import 'package:cw_core/wallet_type.dart';
40
static const _baseBuyProductUrl = 'buy.moonpay.com';
41
static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
42
static const _apiUrl = 'https://api.moonpay.com';
43
+ static const _baseUrl = 'api.moonpay.com';
44
+ static const _currenciesPath = '/v3/currencies';
45
+ static const _buyQuote = '/buy_quote';
46
+ static const _sellQuote = '/sell_quote';
47
+
48
+ static const _transactionsSuffix = '/v1/transactions';
49
+
50
+ final String baseBuyUrl;
51
+ final String baseSellUrl;
52
53
@override
54
String get providerDescription =>
63
@override
64
String get darkIcon => 'assets/images/moonpay_dark.png';
65
66
+ @override
67
+ bool get isAggregator => false;
68
+
69
+ static String get _apiKey => secrets.moonPayApiKey;
70
+
71
+ String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
72
+
73
+ String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
74
+
75
+ static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
76
+
77
static String themeToMoonPayTheme(ThemeBase theme) {
78
switch (theme.type) {
79
case ThemeType.bright:
84
}
85
}
86
66
- static String get _apiKey => secrets.moonPayApiKey;
67
-
68
- final String baseBuyUrl;
69
- final String baseSellUrl;
70
-
71
- String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
72
-
73
- String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId=';
74
-
75
- static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
76
-
87
Future<String> getMoonpaySignature(String query) async {
88
final uri = Uri.https(_cIdBaseUrl, "/api/moonpay");
89
80
- final response = await post(
81
- uri,
82
- headers: {
83
- 'Content-Type': 'application/json',
84
- 'x-api-key': _exchangeHelperApiKey,
85
- },
86
- body: json.encode({'query': query}),
87
- );
90
+ final response = await post(uri,
91
+ headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey},
92
+ body: json.encode({'query': query}));
93
94
if (response.statusCode == 200) {
95
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
99
}
100
}
101
97
- Future<Uri> requestSellMoonPayUrl({
98
- required CryptoCurrency currency,
99
- required String refundWalletAddress,
100
- required SettingsStore settingsStore,
101
- }) async {
102
- final params = {
103
- 'theme': themeToMoonPayTheme(settingsStore.currentTheme),
104
- 'language': settingsStore.languageCode,
105
- 'colorCode': settingsStore.currentTheme.type == ThemeType.dark
106
- ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
107
- : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
108
- 'defaultCurrencyCode': _normalizeCurrency(currency),
109
- 'refundWalletAddress': refundWalletAddress,
110
- };
102
+ Future<Map<String, dynamic>> fetchFiatCredentials(
103
+ String fiatCurrency, String cryptocurrency, String? paymentMethod) async {
104
+ final params = {'baseCurrencyCode': fiatCurrency.toLowerCase(), 'apiKey': _apiKey};
105
112
- if (_apiKey.isNotEmpty) {
113
- params['apiKey'] = _apiKey;
114
- }
106
+ if (paymentMethod != null) params['paymentMethod'] = paymentMethod;
107
116
- final originalUri = Uri.https(
117
- baseSellUrl,
118
- '',
119
- params,
120
- );
108
+ final path = '$_currenciesPath/${cryptocurrency.toLowerCase()}/limits';
109
+ final url = Uri.https(_baseUrl, path, params);
110
122
- if (isTestEnvironment) {
123
- return originalUri;
111
+ try {
112
+ final response = await get(url, headers: {'accept': 'application/json'});
113
+ if (response.statusCode == 200) {
114
+ return jsonDecode(response.body) as Map<String, dynamic>;
115
+ } else {
116
+ print('MoonPay does not support fiat: $fiatCurrency');
117
+ return {};
118
+ }
119
+ } catch (e) {
120
+ print('MoonPay Error fetching fiat currencies: $e');
121
+ return {};
122
}
123
+ }
124
126
- final signature = await getMoonpaySignature('?${originalUri.query}');
125
+ Future<List<PaymentMethod>> getAvailablePaymentTypes(
126
+ String fiatCurrency, String cryptoCurrency, bool isBuyAction) async {
127
+ final List<PaymentMethod> paymentMethods = [];
128
+
129
+ if (isBuyAction) {
130
+ final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency, cryptoCurrency, null);
131
+ if (fiatBuyCredentials.isNotEmpty) {
132
+ final paymentMethod = fiatBuyCredentials['paymentMethod'] as String?;
133
+ paymentMethods.add(PaymentMethod.fromMoonPayJson(
134
+ fiatBuyCredentials, _getPaymentTypeByString(paymentMethod)));
135
+ return paymentMethods;
136
+ }
137
+ }
138
128
- final query = Map<String, dynamic>.from(originalUri.queryParameters);
129
- query['signature'] = signature;
130
- final signedUri = originalUri.replace(queryParameters: query);
131
- return signedUri;
139
+ return paymentMethods;
140
}
141
134
- // BUY:
135
- static const _currenciesSuffix = '/v3/currencies';
136
- static const _quoteSuffix = '/buy_quote';
137
- static const _transactionsSuffix = '/v1/transactions';
138
- static const _ipAddressSuffix = '/v4/ip_address';
142
+ @override
143
+ Future<List<Quote>?> fetchQuote(
144
+ {required CryptoCurrency cryptoCurrency,
145
+ required FiatCurrency fiatCurrency,
146
+ required double amount,
147
+ required bool isBuyAction,
148
+ required String walletAddress,
149
+ PaymentType? paymentType,
150
+ String? countryCode}) async {
151
+ String? paymentMethod;
152
+
153
+ if (paymentType != null && paymentType != PaymentType.all) {
154
+ paymentMethod = normalizePaymentMethod(paymentType);
155
+ if (paymentMethod == null) paymentMethod = paymentType.name;
156
+ } else {
157
+ paymentMethod = 'credit_debit_card';
158
+ }
159
+
160
+ final action = isBuyAction ? 'buy' : 'sell';
161
+
162
+ final formattedCryptoCurrency = _normalizeCurrency(cryptoCurrency);
163
+ final baseCurrencyCode =
164
+ isBuyAction ? fiatCurrency.name.toLowerCase() : cryptoCurrency.title.toLowerCase();
165
140
- Future<Uri> requestBuyMoonPayUrl({
141
- required CryptoCurrency currency,
142
- required SettingsStore settingsStore,
143
- required String walletAddress,
144
- String? amount,
145
- }) async {
166
final params = {
147
- 'theme': themeToMoonPayTheme(settingsStore.currentTheme),
148
- 'language': settingsStore.languageCode,
149
- 'colorCode': settingsStore.currentTheme.type == ThemeType.dark
167
+ 'baseCurrencyCode': baseCurrencyCode,
168
+ 'baseCurrencyAmount': amount.toString(),
169
+ 'amount': amount.toString(),
170
+ 'paymentMethod': paymentMethod,
171
+ 'areFeesIncluded': 'false',
172
+ 'apiKey': _apiKey
173
+ };
174
+
175
+ log('MoonPay: Fetching $action quote: ${isBuyAction ? formattedCryptoCurrency : fiatCurrency.name.toLowerCase()} -> ${isBuyAction ? baseCurrencyCode : formattedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
176
+
177
+ final quotePath = isBuyAction ? _buyQuote : _sellQuote;
178
+
179
+ final path = '$_currenciesPath/$formattedCryptoCurrency$quotePath';
180
+ final url = Uri.https(_baseUrl, path, params);
181
+ try {
182
+ final response = await get(url);
183
+
184
+ if (response.statusCode == 200) {
185
+ final data = jsonDecode(response.body) as Map<String, dynamic>;
186
+
187
+ // Check if the response is for the correct fiat currency
188
+ if (isBuyAction) {
189
+ final fiatCurrencyCode = data['baseCurrencyCode'] as String?;
190
+ if (fiatCurrencyCode == null || fiatCurrencyCode != fiatCurrency.name.toLowerCase())
191
+ return null;
192
+ } else {
193
+ final quoteCurrency = data['quoteCurrency'] as Map<String, dynamic>?;
194
+ if (quoteCurrency == null || quoteCurrency['code'] != fiatCurrency.name.toLowerCase())
195
+ return null;
196
+ }
197
+
198
+ final paymentMethods = data['paymentMethod'] as String?;
199
+ final quote =
200
+ Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods));
201
+
202
+ quote.setFiatCurrency = fiatCurrency;
203
+ quote.setCryptoCurrency = cryptoCurrency;
204
+
205
+ return [quote];
206
+ } else {
207
+ print('Moon Pay: Error fetching buy quote: ');
208
+ return null;
209
+ }
210
+ } catch (e) {
211
+ print('Moon Pay: Error fetching buy quote: $e');
212
+ return null;
213
+ }
214
+ }
215
+
216
+ @override
217
+ Future<void>? launchProvider(
218
+ {required BuildContext context,
219
+ required Quote quote,
220
+ required double amount,
221
+ required bool isBuyAction,
222
+ required String cryptoCurrencyAddress,
223
+ String? countryCode}) async {
224
+
225
+ final Map<String, String> params = {
226
+ 'theme': themeToMoonPayTheme(_settingsStore.currentTheme),
227
+ 'language': _settingsStore.languageCode,
228
+ 'colorCode': _settingsStore.currentTheme.type == ThemeType.dark
229
? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}'
230
: '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}',
152
- 'baseCurrencyCode': settingsStore.fiatCurrency.title,
153
- 'baseCurrencyAmount': amount ?? '0',
154
- 'currencyCode': _normalizeCurrency(currency),
155
- 'walletAddress': walletAddress,
231
+ 'baseCurrencyCode': isBuyAction ? quote.fiatCurrency.name : quote.cryptoCurrency.name,
232
+ 'baseCurrencyAmount': amount.toString(),
233
+ 'walletAddress': cryptoCurrencyAddress,
234
'lockAmount': 'false',
235
'showAllCurrencies': 'false',
236
'showWalletAddressForm': 'false',
159
- 'enabledPaymentMethods':
160
- 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
237
+ if (isBuyAction)
238
+ 'enabledPaymentMethods': normalizePaymentMethod(quote.paymentType) ??
239
+ 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment',
240
+ if (!isBuyAction) 'refundWalletAddress': cryptoCurrencyAddress
241
};
242
163
- if (_apiKey.isNotEmpty) {
164
- params['apiKey'] = _apiKey;
243
+ if (isBuyAction) params['currencyCode'] = quote.cryptoCurrency.name;
244
+ if (!isBuyAction) params['quoteCurrencyCode'] = quote.cryptoCurrency.name;
245
+
246
+ try {
247
+ {
248
+ final uri = await requestMoonPayUrl(
249
+ walletAddress: cryptoCurrencyAddress,
250
+ settingsStore: _settingsStore,
251
+ isBuyAction: isBuyAction,
252
+ amount: amount.toString(),
253
+ params: params);
254
+
255
+ if (await canLaunchUrl(uri)) {
256
+ await launchUrl(uri, mode: LaunchMode.externalApplication);
257
+ } else {
258
+ throw Exception('Could not launch URL');
259
+ }
260
+ }
261
+ } catch (e) {
262
+ if (context.mounted) {
263
+ await showDialog<void>(
264
+ context: context,
265
+ builder: (BuildContext context) {
266
+ return AlertWithOneAction(
267
+ alertTitle: 'MoonPay',
268
+ alertContent: 'The MoonPay service is currently unavailable: $e',
269
+ buttonText: S.of(context).ok,
270
+ buttonAction: () => Navigator.of(context).pop(),
271
+ );
272
+ },
273
+ );
274
+ }
275
}
276
+ }
277
167
- final originalUri = Uri.https(
168
- baseBuyUrl,
169
- '',
170
- params,
171
- );
278
+ Future<Uri> requestMoonPayUrl({
279
+ required String walletAddress,
280
+ required SettingsStore settingsStore,
281
+ required bool isBuyAction,
282
+ required Map<String, String> params,
283
+ String? amount,
284
+ }) async {
285
+ if (_apiKey.isNotEmpty) params['apiKey'] = _apiKey;
286
173
- if (isTestEnvironment) {
174
- return originalUri;
175
- }
287
+ final baseUrl = isBuyAction ? baseBuyUrl : baseSellUrl;
288
+ final originalUri = Uri.https(baseUrl, '', params);
289
+
290
+ if (isTestEnvironment) return originalUri;
291
292
final signature = await getMoonpaySignature('?${originalUri.query}');
293
final query = Map<String, dynamic>.from(originalUri.queryParameters);
296
return signedUri;
297
}
298
184
- Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
185
- final url = _apiUrl +
186
- _currenciesSuffix +
187
- '/$currencyCode' +
188
- _quoteSuffix +
189
- '/?apiKey=' +
190
- _apiKey +
191
- '&baseCurrencyAmount=' +
192
- amount +
193
- '&baseCurrencyCode=' +
194
- sourceCurrency.toLowerCase();
195
- final uri = Uri.parse(url);
196
- final response = await get(uri);
197
-
198
- if (response.statusCode != 200) {
199
- throw BuyException(title: providerDescription, content: 'Quote is not found!');
200
- }
201
-
202
- final responseJSON = json.decode(response.body) as Map<String, dynamic>;
203
- final sourceAmount = responseJSON['totalAmount'] as double;
204
- final destAmount = responseJSON['quoteCurrencyAmount'] as double;
205
- final minSourceAmount = responseJSON['baseCurrency']['minAmount'] as int;
206
-
207
- return BuyAmount(
208
- sourceAmount: sourceAmount, destAmount: destAmount, minAmount: minSourceAmount);
209
- }
210
-
299
Future<Order> findOrderById(String id) async {
300
final url = _apiUrl + _transactionsSuffix + '/$id' + '?apiKey=' + _apiKey;
301
final uri = Uri.parse(url);
323
walletId: wallet.id);
324
}
325
238
- static Future<bool> onEnabled() async {
239
- final url = _apiUrl + _ipAddressSuffix + '?apiKey=' + _apiKey;
240
- var isBuyEnable = false;
241
- final uri = Uri.parse(url);
242
- final response = await get(uri);
326
+ String _normalizeCurrency(CryptoCurrency currency) {
327
+ if (currency.tag == 'POLY') {
328
+ return '${currency.title.toLowerCase()}_polygon';
329
+ }
330
244
- try {
245
- final responseJSON = json.decode(response.body) as Map<String, dynamic>;
246
- isBuyEnable = responseJSON['isBuyAllowed'] as bool;
247
- } catch (e) {
248
- isBuyEnable = false;
249
- print(e.toString());
331
+ if (currency.tag == 'TRX') {
332
+ return '${currency.title.toLowerCase()}_trx';
333
}
334
252
- return isBuyEnable;
335
+ return currency.toString().toLowerCase();
336
}
337
255
- @override
256
- Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
257
- try {
258
- late final Uri uri;
259
- if (isBuyAction ?? true) {
260
- uri = await requestBuyMoonPayUrl(
261
- currency: wallet.currency,
262
- walletAddress: wallet.walletAddresses.address,
263
- settingsStore: _settingsStore,
264
- );
265
- } else {
266
- uri = await requestSellMoonPayUrl(
267
- currency: wallet.currency,
268
- refundWalletAddress: wallet.walletAddresses.address,
269
- settingsStore: _settingsStore,
270
- );
271
- }
272
-
273
- if (await canLaunchUrl(uri)) {
274
- if (DeviceInfo.instance.isMobile) {
275
- Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
276
- } else {
277
- await launchUrl(uri, mode: LaunchMode.externalApplication);
278
- }
279
- } else {
280
- throw Exception('Could not launch URL');
281
- }
282
- } catch (e) {
283
- if (context.mounted) {
284
- await showDialog<void>(
285
- context: context,
286
- builder: (BuildContext context) {
287
- return AlertWithOneAction(
288
- alertTitle: 'MoonPay',
289
- alertContent: 'The MoonPay service is currently unavailable: $e',
290
- buttonText: S.of(context).ok,
291
- buttonAction: () => Navigator.of(context).pop(),
292
- );
293
- },
294
- );
295
- }
338
+ String? normalizePaymentMethod(PaymentType paymentMethod) {
339
+ switch (paymentMethod) {
340
+ case PaymentType.creditCard:
341
+ return 'credit_debit_card';
342
+ case PaymentType.debitCard:
343
+ return 'credit_debit_card';
344
+ case PaymentType.ach:
345
+ return 'ach_bank_transfer';
346
+ case PaymentType.applePay:
347
+ return 'apple_pay';
348
+ case PaymentType.googlePay:
349
+ return 'google_pay';
350
+ case PaymentType.sepa:
351
+ return 'sepa_bank_transfer';
352
+ case PaymentType.paypal:
353
+ return 'paypal';
354
+ case PaymentType.sepaOpenBankingPayment:
355
+ return 'sepa_open_banking_payment';
356
+ case PaymentType.gbpOpenBankingPayment:
357
+ return 'gbp_open_banking_payment';
358
+ case PaymentType.lowCostAch:
359
+ return 'low_cost_ach';
360
+ case PaymentType.mobileWallet:
361
+ return 'mobile_wallet';
362
+ case PaymentType.pixInstantPayment:
363
+ return 'pix_instant_payment';
364
+ case PaymentType.yellowCardBankTransfer:
365
+ return 'yellow_card_bank_transfer';
366
+ case PaymentType.fiatBalance:
367
+ return 'fiat_balance';
368
+ default:
369
+ return null;
370
}
371
}
372
299
- String _normalizeCurrency(CryptoCurrency currency) {
300
- if (currency == CryptoCurrency.maticpoly) {
301
- return "POL_POLYGON";
302
- } else if (currency == CryptoCurrency.matic) {
303
- return "POL";
373
+ PaymentType _getPaymentTypeByString(String? paymentMethod) {
374
+ switch (paymentMethod) {
375
+ case 'ach_bank_transfer':
376
+ return PaymentType.ach;
377
+ case 'apple_pay':
378
+ return PaymentType.applePay;
379
+ case 'credit_debit_card':
380
+ return PaymentType.creditCard;
381
+ case 'fiat_balance':
382
+ return PaymentType.fiatBalance;
383
+ case 'gbp_open_banking_payment':
384
+ return PaymentType.gbpOpenBankingPayment;
385
+ case 'google_pay':
386
+ return PaymentType.googlePay;
387
+ case 'low_cost_ach':
388
+ return PaymentType.lowCostAch;
389
+ case 'mobile_wallet':
390
+ return PaymentType.mobileWallet;
391
+ case 'paypal':
392
+ return PaymentType.paypal;
393
+ case 'pix_instant_payment':
394
+ return PaymentType.pixInstantPayment;
395
+ case 'sepa_bank_transfer':
396
+ return PaymentType.sepa;
397
+ case 'sepa_open_banking_payment':
398
+ return PaymentType.sepaOpenBankingPayment;
399
+ case 'yellow_card_bank_transfer':
400
+ return PaymentType.yellowCardBankTransfer;
401
+ default:
402
+ return PaymentType.all;
403
}
305
-
306
- return currency.toString().toLowerCase();
404
}
405
}