dev
dart 437 lines 14.8 KB
Raw
1 import 'dart:convert';
2 import 'dart:developer';
3
4 import 'package:cake_wallet/buy/buy_provider.dart';
5 import 'package:cake_wallet/buy/buy_quote.dart';
6 import 'package:cake_wallet/buy/pairs_utils.dart';
7 import 'package:cake_wallet/buy/payment_method.dart';
8 import 'package:cake_wallet/entities/fiat_currency.dart';
9 import 'package:cake_wallet/generated/i18n.dart';
10 import 'package:cake_wallet/routes.dart';
11 import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
12 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
13 import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
14 import 'package:cake_wallet/utils/show_pop_up.dart';
15 import 'package:cw_core/crypto_currency.dart';
16 import 'package:cw_core/utils/print_verbose.dart';
17 import 'package:cw_core/utils/proxy_wrapper.dart';
18 import 'package:cw_core/wallet_base.dart';
19 import 'package:cw_core/wallet_type.dart';
20 import 'package:flutter/material.dart';
21 import 'package:url_launcher/url_launcher.dart';
22
23 class DFXBuyProvider extends BuyProvider {
24 DFXBuyProvider({
25 required WalletBase wallet,
26 bool isTestEnvironment = false,
27 HardwareWalletViewModel? hardwareWalletVM,
28 }) : super(
29 wallet: wallet,
30 isTestEnvironment: isTestEnvironment,
31 hardwareWalletVM: hardwareWalletVM,
32 supportedCryptoList: supportedCryptoToFiatPairs(
33 notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat),
34 supportedFiatList: supportedFiatToCryptoPairs(
35 notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto),
36 );
37
38 static const _baseUrl = 'api.dfx.swiss';
39
40 // static const _signMessagePath = '/v1/auth/signMessage';
41 static const _authPath = '/v1/auth';
42 static const walletName = 'CakeWallet';
43
44 static final List<CryptoCurrency> _supportedCrypto = [
45 CryptoCurrency.xmr,
46 CryptoCurrency.btc,
47 CryptoCurrency.eth,
48 CryptoCurrency.maticpoly,
49 CryptoCurrency.sol,
50 CryptoCurrency.zano,
51 CryptoCurrency.trx,
52 ];
53 static final List<CryptoCurrency> _notSupportedCrypto = CryptoCurrency.all
54 .where((crypto) => !_supportedCrypto.contains(crypto) || ["ETH", "POL"].contains(crypto.tag))
55 .toList();
56
57 static final List<FiatCurrency> _supportedFiat = [FiatCurrency.chf, FiatCurrency.eur];
58 static final List<FiatCurrency> _notSupportedFiat =
59 FiatCurrency.all.where((fiat) => !_supportedFiat.contains(fiat)).toList();
60
61 @override
62 String get title => 'DFX.swiss';
63
64 @override
65 String get providerDescription => S.current.dfx_option_description;
66
67 @override
68 String get lightIcon => 'assets/images/dfx_light.png';
69
70 @override
71 String get darkIcon => 'assets/images/dfx_dark.png';
72
73 @override
74 bool get isAggregator => false;
75
76 String get blockchain {
77 switch (wallet.type) {
78 case WalletType.bitcoin:
79 return 'Bitcoin';
80 case WalletType.zano:
81 return 'Zano';
82 default:
83 return walletTypeToString(wallet.type);
84 }
85 }
86
87 Future<String> getSignMessage(String walletAddress) async =>
88 "By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress";
89
90 // Lets keep this just in case, but we can avoid this API Call
91 // Future<String> getSignMessage() async {
92 // final uri = Uri.https(_baseUrl, _signMessagePath, {'address': walletAddress});
93 //
94 // final response = await http.get(uri, headers: {'accept': 'application/json'});
95 //
96 // if (response.statusCode == 200) {
97 // final responseBody = jsonDecode(response.body);
98 // return responseBody['message'] as String;
99 // } else {
100 // throw Exception(
101 // 'Failed to get sign message. Status: ${response.statusCode} ${response.body}');
102 // }
103 // }
104
105 Future<String> auth(String walletAddress) async {
106 final signMessage = await getSignature(await getSignMessage(walletAddress), walletAddress);
107
108 final requestBody = jsonEncode({
109 'wallet': walletName,
110 'address': walletAddress,
111 'signature': signMessage,
112 });
113
114 final uri = Uri.https(_baseUrl, _authPath);
115 final response = await ProxyWrapper().post(
116 clearnetUri: uri,
117 headers: {'Content-Type': 'application/json'},
118 body: requestBody,
119 );
120
121 if (response.statusCode == 201) {
122 final responseBody = jsonDecode(response.body);
123 return responseBody['accessToken'] as String;
124 } else if (response.statusCode == 403) {
125 final responseBody = jsonDecode(response.body);
126 final message = responseBody['message'] ?? 'Service unavailable in your country';
127 throw Exception(message);
128 } else {
129 throw Exception('Failed to sign up. ${_getErrorMessage(response.statusCode, response.body)}');
130 }
131 }
132
133 Future<String> getSignature(String message, String walletAddress) async {
134 switch (wallet.type) {
135 case WalletType.ethereum:
136 case WalletType.polygon:
137 case WalletType.base:
138 case WalletType.arbitrum:
139 case WalletType.bsc:
140 case WalletType.solana:
141 case WalletType.tron:
142 return wallet.signMessage(message);
143 case WalletType.monero:
144 case WalletType.litecoin:
145 case WalletType.bitcoin:
146 case WalletType.bitcoinCash:
147 case WalletType.zano:
148 return wallet.signMessage(message, address: walletAddress);
149 default:
150 throw Exception("WalletType is not available for DFX ${wallet.type}");
151 }
152 }
153
154 Future<Map<String, dynamic>> fetchFiatCredentials(String fiatCurrency) async {
155 final url = Uri.https(_baseUrl, '/v1/fiat');
156
157 try {
158 final response =
159 await ProxyWrapper().get(clearnetUri: url, headers: {'accept': 'application/json'});
160
161 if (response.statusCode == 200) {
162 final data = jsonDecode(response.body) as List<dynamic>;
163 for (final item in data) {
164 if (item['name'] == fiatCurrency) return item as Map<String, dynamic>;
165 }
166 log('DFX does not support fiat: $fiatCurrency');
167 return {};
168 } else {
169 log('DFX Failed to fetch fiat currencies: ${response.statusCode}');
170 return {};
171 }
172 } catch (e) {
173 printV('DFX Error fetching fiat currencies: $e');
174 return {};
175 }
176 }
177
178 Future<Map<String, dynamic>> fetchAssetCredential(String assetsName) async {
179 final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain});
180
181 try {
182 final response =
183 await ProxyWrapper().get(clearnetUri: url, headers: {'accept': 'application/json'});
184
185 if (response.statusCode == 200) {
186 final responseData = jsonDecode(response.body);
187
188 if (responseData is List && responseData.isNotEmpty) {
189 for (final i in responseData) {
190 if (assetsName.toLowerCase() == i["dexName"].toString().toLowerCase()) {
191 return i as Map<String, dynamic>;
192 }
193 }
194 return responseData.first as Map<String, dynamic>;
195 } else if (responseData is Map<String, dynamic>) {
196 return responseData;
197 } else {
198 log('DFX: Does not support this asset name : ${blockchain}');
199 }
200 } else {
201 log('DFX: Failed to fetch assets: ${response.statusCode}');
202 }
203 } catch (e) {
204 log('DFX: Error fetching assets: $e');
205 }
206 return {};
207 }
208
209 Future<List<PaymentMethod>> getAvailablePaymentTypes(
210 String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
211 final List<PaymentMethod> paymentMethods = [];
212
213 if (isBuyAction) {
214 final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency);
215 if (fiatBuyCredentials.isNotEmpty) {
216 fiatBuyCredentials.forEach((key, value) {
217 if (key == 'limits') {
218 final limits = value as Map<String, dynamic>;
219 limits.forEach((paymentMethodKey, paymentMethodValue) {
220 final min = _toDouble(paymentMethodValue['minVolume']);
221 final max = _toDouble(paymentMethodValue['maxVolume']);
222 if (min != null && max != null && min > 0 && max > 0) {
223 final paymentMethod = PaymentMethod.fromDFX(
224 paymentMethodKey, _getPaymentTypeByString(paymentMethodKey));
225 paymentMethods.add(paymentMethod);
226 }
227 });
228 }
229 });
230 }
231 } else {
232 final assetCredentials = await fetchAssetCredential(cryptoCurrency.title);
233 if (assetCredentials.isNotEmpty) {
234 if (assetCredentials['sellable'] == true) {
235 final availablePaymentTypes = [
236 PaymentType.bankTransfer,
237 PaymentType.creditCard,
238 PaymentType.sepa
239 ];
240 availablePaymentTypes.forEach((element) {
241 final paymentMethod = PaymentMethod.fromDFX(normalizePaymentMethod(element)!, element);
242 paymentMethods.add(paymentMethod);
243 });
244 }
245 }
246 }
247
248 return paymentMethods;
249 }
250
251 @override
252 Future<List<Quote>?> fetchQuote(
253 {required CryptoCurrency cryptoCurrency,
254 required FiatCurrency fiatCurrency,
255 required double amount,
256 required bool isBuyAction,
257 required String walletAddress,
258 PaymentType? paymentType,
259 String? customPaymentMethodType,
260 String? countryCode}) async {
261 /// if buying with any currency other than eur or chf then DFX is not supported
262
263 if (isBuyAction && (fiatCurrency != FiatCurrency.eur && fiatCurrency != FiatCurrency.chf)) {
264 return null;
265 }
266
267 String? paymentMethod;
268 if (paymentType != null && paymentType != PaymentType.all) {
269 paymentMethod = normalizePaymentMethod(paymentType);
270 if (paymentMethod == null) paymentMethod = paymentType.name;
271 } else {
272 paymentMethod = 'Bank';
273 }
274
275 final action = isBuyAction ? 'buy' : 'sell';
276
277 final fiatCredentials = await fetchFiatCredentials(fiatCurrency.name.toString());
278 if (fiatCredentials['id'] == null) return null;
279
280 final assetCredentials = await fetchAssetCredential(cryptoCurrency.title.toString());
281 if (assetCredentials['id'] == null) return null;
282
283 log('DFX: Fetching $action quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
284
285 final url = Uri.https(_baseUrl, '/v1/$action/quote');
286 final headers = {'accept': 'application/json', 'Content-Type': 'application/json'};
287 final body = jsonEncode({
288 'currency': {'id': fiatCredentials['id'] as int},
289 'asset': {'id': assetCredentials['id']},
290 'amount': amount,
291 'targetAmount': 0,
292 'paymentMethod': paymentMethod,
293 'discountCode': ''
294 });
295
296 try {
297 final response = await ProxyWrapper().put(
298 clearnetUri: url,
299 headers: headers,
300 body: body,
301 );
302
303 final responseData = jsonDecode(response.body);
304
305 if (response.statusCode == 200) {
306 if (responseData is Map<String, dynamic>) {
307 final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
308 final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType);
309 quote.setFiatCurrency = fiatCurrency;
310 quote.setCryptoCurrency = cryptoCurrency;
311 return [quote];
312 } else {
313 printV('DFX: Unexpected data type: ${responseData.runtimeType}');
314 return null;
315 }
316 } else {
317 if (responseData is Map<String, dynamic> && responseData.containsKey('message')) {
318 printV('DFX Error: ${responseData['message']}');
319 } else {
320 printV('DFX Failed to fetch buy quote: ${response.statusCode}');
321 }
322 return null;
323 }
324 } catch (e) {
325 printV('DFX Error fetching buy quote: $e');
326 return null;
327 }
328 }
329
330 Future<void>? launchProvider(
331 {required BuildContext context,
332 required Quote quote,
333 required double amount,
334 required bool isBuyAction,
335 required String cryptoCurrencyAddress,
336 String? countryCode}) async {
337 if (wallet.isHardwareWallet) {
338 if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
339 await Navigator.of(context).pushNamed(
340 Routes.connectDevices,
341 arguments: ConnectDevicePageParams(
342 walletType: wallet.walletInfo.type,
343 hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
344 onConnectDevice: (context, hwwVM) {
345 hwwVM.initWallet(wallet);
346 Navigator.of(context).pop();
347 },
348 isReconnect: false,
349 ),
350 );
351
352 // Recheck to handle tap-backs
353 if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
354 return;
355 }
356 } else {
357 hardwareWalletVM!.initWallet(wallet);
358 }
359 }
360
361 try {
362 final actionType = isBuyAction ? '/buy' : '/sell';
363
364 final accessToken = await auth(cryptoCurrencyAddress);
365
366 final uri = Uri.https('app.dfx.swiss', actionType, {
367 'session': accessToken,
368 'lang': 'en',
369 'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(),
370 'blockchain': blockchain,
371 'asset-in': isBuyAction ? quote.fiatCurrency.toString() : quote.cryptoCurrency.toString(),
372 'amount-in': amount.toString()
373 });
374
375 if (await canLaunchUrl(uri)) {
376 await launchUrl(uri, mode: LaunchMode.externalApplication);
377 } else {
378 throw Exception('Could not launch URL');
379 }
380 } catch (e) {
381 await showPopUp<void>(
382 context: context,
383 builder: (context) => AlertWithOneAction(
384 alertTitle: "DFX.swiss",
385 alertContent: '${S.of(context).buy_provider_unavailable}: $e',
386 buttonText: S.of(context).ok,
387 buttonAction: () => Navigator.of(context).pop()),
388 );
389 }
390 }
391
392 String? normalizePaymentMethod(PaymentType paymentMethod) {
393 switch (paymentMethod) {
394 case PaymentType.bankTransfer:
395 return 'Bank';
396 case PaymentType.creditCard:
397 return 'Card';
398 case PaymentType.sepa:
399 return 'Instant';
400 default:
401 return null;
402 }
403 }
404
405 PaymentType _getPaymentTypeByString(String? paymentMethod) {
406 switch (paymentMethod) {
407 case 'Bank':
408 return PaymentType.bankTransfer;
409 case 'Card':
410 return PaymentType.creditCard;
411 case 'Instant':
412 return PaymentType.sepa;
413 default:
414 return PaymentType.unknown;
415 }
416 }
417
418 double? _toDouble(dynamic value) {
419 if (value is int) {
420 return value.toDouble();
421 } else if (value is double) {
422 return value;
423 }
424 return null;
425 }
426
427 String _getErrorMessage(int statusCode, String body) {
428 final responseBody = jsonDecode(body) as Map<String, dynamic>;
429 final message = responseBody['message']?.toString() ?? '';
430
431 if (message.contains("address must match") || message.contains("signature must match")) {
432 return "The wallet type must match the selected currency";
433 }
434
435 return message.isNotEmpty ? message : "Unknown error: ${statusCode}";
436 }
437 }