dev
dart 366 lines 12 KB
Raw
1 import 'dart:convert';
2
3 import 'package:cake_wallet/.secrets.g.dart' as secrets;
4 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
5 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
6 import 'package:cake_wallet/exchange/limits.dart';
7 import 'package:cake_wallet/exchange/trade.dart';
8 import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
9 import 'package:cake_wallet/exchange/trade_not_found_exception.dart';
10 import 'package:cake_wallet/exchange/trade_request.dart';
11 import 'package:cake_wallet/exchange/trade_state.dart';
12 import 'package:cake_wallet/utils/device_info.dart';
13 import 'package:cw_core/utils/proxy_wrapper.dart';
14 import 'package:cw_core/crypto_currency.dart';
15 import 'package:cake_wallet/utils/exchange_provider_logger.dart';
16
17 class SimpleSwapExchangeProvider extends ExchangeProvider {
18 SimpleSwapExchangeProvider();
19
20 static final apiKey =
21 DeviceInfo.instance.isMobile ? secrets.simpleSwapApiKey : secrets.simpleSwapApiKeyDesktop;
22 static const apiAuthority = 'api.simpleswap.io';
23 static const getEstimatePath = '/v1/get_estimated';
24 static const rangePath = '/v1/get_ranges';
25 static const getExchangePath = '/v1/get_exchange';
26 static const createExchangePath = '/v1/create_exchange';
27
28 @override
29 String get title => 'SimpleSwap';
30
31 @override
32 bool get isAvailable => false;
33
34 @override
35 bool get isEnabled => false;
36
37 @override
38 bool get supportsFixedRate => false;
39
40 @override
41 ExchangeProviderDescription get description => ExchangeProviderDescription.simpleSwap;
42
43 @override
44 Future<bool> checkIsAvailable() async {
45 final uri = Uri.https(apiAuthority, getEstimatePath, <String, String>{'api_key': apiKey});
46 final response = await ProxyWrapper().get(clearnetUri: uri);
47
48 return !(response.statusCode == 403);
49 }
50
51 @override
52 Future<Limits?> fetchLimits(
53 {required CryptoCurrency from,
54 required CryptoCurrency to,
55 required bool isFixedRateMode}) async {
56 final params = <String, dynamic>{
57 'api_key': apiKey,
58 'fixed': isFixedRateMode.toString(),
59 'currency_from': _normalizeCurrency(from),
60 'currency_to': _normalizeCurrency(to),
61 };
62 final uri = Uri.https(apiAuthority, rangePath, params);
63
64 final response = await ProxyWrapper().get(clearnetUri: uri);
65
66 if (response.statusCode == 500) {
67 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
68 final error = responseJSON['message'] as String;
69
70 throw Exception('$error');
71 }
72
73 if (response.statusCode != 200) {
74 throw Exception('Unexpected http status: ${response.statusCode}');
75 }
76
77 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
78 final min = double.tryParse(responseJSON['min'] as String? ?? '');
79 final max = double.tryParse(responseJSON['max'] as String? ?? '');
80
81 return Limits(min: min, max: max);
82 }
83
84 @override
85 Future<double> fetchRate(
86 {required CryptoCurrency from,
87 required CryptoCurrency to,
88 required double amount,
89 required bool isFixedRateMode,
90 required bool isReceiveAmount}) async {
91 try {
92 if (amount == 0) return 0.0;
93
94 final params = {
95 'api_key': apiKey,
96 'currency_from': _normalizeCurrency(from),
97 'currency_to': _normalizeCurrency(to),
98 'amount': amount.toString(),
99 'fixed': isFixedRateMode.toString()
100 };
101 final uri = Uri.https(apiAuthority, getEstimatePath, params);
102 final response = await ProxyWrapper().get(clearnetUri: uri);
103
104 if (response.body == "null") {
105 ExchangeProviderLogger.logError(
106 provider: description,
107 function: 'fetchRate',
108 error: Exception('Null response body'),
109 stackTrace: StackTrace.current,
110 requestData: {
111 'from': from.title,
112 'to': to.title,
113 'amount': amount,
114 'isFixedRateMode': isFixedRateMode,
115 'isReceiveAmount': isReceiveAmount,
116 'params': params,
117 'url': uri.toString(),
118 },
119 );
120 return 0.00;
121 }
122
123 final data = json.decode(response.body) as String;
124 final rate = double.parse(data) / amount;
125
126 ExchangeProviderLogger.logSuccess(
127 provider: description,
128 function: 'fetchRate',
129 requestData: {
130 'from': from.title,
131 'to': to.title,
132 'amount': amount,
133 'isFixedRateMode': isFixedRateMode,
134 'isReceiveAmount': isReceiveAmount,
135 'params': params,
136 'url': uri.toString(),
137 },
138 responseData: {
139 'data': data,
140 'rate': rate,
141 'statusCode': response.statusCode,
142 'responseBody': response.body,
143 },
144 );
145
146 return rate;
147 } catch (e, s) {
148 ExchangeProviderLogger.logError(
149 provider: description,
150 function: 'fetchRate',
151 error: e,
152 stackTrace: s,
153 requestData: {
154 'from': from.title,
155 'to': to.title,
156 'amount': amount,
157 'isFixedRateMode': isFixedRateMode,
158 'isReceiveAmount': isReceiveAmount,
159 },
160 );
161 return 0.00;
162 }
163 }
164
165 @override
166 Future<Trade> createTrade({
167 required TradeRequest request,
168 required bool isFixedRateMode,
169 required bool isSendAll,
170 }) async {
171 final headers = {'Content-Type': 'application/json'};
172 final params = {'api_key': apiKey};
173 final body = <String, dynamic>{
174 "currency_from": _normalizeCurrency(request.fromCurrency),
175 "currency_to": _normalizeCurrency(request.toCurrency),
176 "amount": request.fromAmount,
177 "fixed": isFixedRateMode,
178 "user_refund_address": _normalizeAddress(request.refundAddress),
179 "address_to": _normalizeAddress(request.toAddress),
180 if (request.toAddressExtraId.isNotEmpty) "extra_id_to": request.toAddressExtraId,
181 };
182 final uri = Uri.https(apiAuthority, createExchangePath, params);
183
184 final response = await ProxyWrapper().post(
185 clearnetUri: uri,
186 headers: headers,
187 body: json.encode(body),
188 );
189
190 if (response.statusCode != 200 && response.statusCode != 201) {
191 if (response.statusCode == 400) {
192 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
193 final error = responseJSON['message'] as String;
194
195 ExchangeProviderLogger.logError(
196 provider: description,
197 function: 'createTrade',
198 error: TradeNotCreatedException(description, description: error),
199 stackTrace: StackTrace.current,
200 requestData: {
201 'from': request.fromCurrency.title,
202 'to': request.toCurrency.title,
203 'fromAmount': request.fromAmount,
204 'toAmount': request.toAmount,
205 'toAddress': request.toAddress,
206 'refundAddress': request.refundAddress,
207 'isFixedRateMode': isFixedRateMode,
208 'isSendAll': isSendAll,
209 'body': body,
210 'url': uri.toString(),
211 },
212 );
213
214 throw TradeNotCreatedException(description, description: error);
215 }
216
217 ExchangeProviderLogger.logError(
218 provider: description,
219 function: 'createTrade',
220 error: TradeNotCreatedException(description),
221 stackTrace: StackTrace.current,
222 requestData: {
223 'from': request.fromCurrency.title,
224 'to': request.toCurrency.title,
225 'fromAmount': request.fromAmount,
226 'toAmount': request.toAmount,
227 'toAddress': request.toAddress,
228 'refundAddress': request.refundAddress,
229 'isFixedRateMode': isFixedRateMode,
230 'isSendAll': isSendAll,
231 'body': body,
232 'url': uri.toString(),
233 },
234 );
235
236 throw TradeNotCreatedException(description);
237 }
238
239 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
240 final id = responseJSON['id'] as String;
241 final inputAddress = responseJSON['address_from'] as String;
242 final payoutAddress = responseJSON['address_to'] as String;
243 final settleAddress = responseJSON['user_refund_address'] as String;
244 final extraId = responseJSON['extra_id_from'] as String?;
245 final receiveAmount = responseJSON['amount_to'] as String?;
246
247 ExchangeProviderLogger.logSuccess(
248 provider: description,
249 function: 'createTrade',
250 requestData: {
251 'from': request.fromCurrency.title,
252 'to': request.toCurrency.title,
253 'fromAmount': request.fromAmount,
254 'toAmount': request.toAmount,
255 'toAddress': request.toAddress,
256 'refundAddress': request.refundAddress,
257 'isFixedRateMode': isFixedRateMode,
258 'isSendAll': isSendAll,
259 'body': body,
260 'url': uri.toString(),
261 },
262 responseData: {
263 'id': id,
264 'inputAddress': inputAddress,
265 'payoutAddress': payoutAddress,
266 'settleAddress': settleAddress,
267 'extraId': extraId,
268 'receiveAmount': receiveAmount,
269 'statusCode': response.statusCode,
270 'responseJSON': responseJSON,
271 },
272 );
273
274 return Trade(
275 id: id,
276 provider: description,
277 from: request.fromCurrency,
278 to: request.toCurrency,
279 inputAddress: inputAddress,
280 refundAddress: settleAddress,
281 extraId: extraId,
282 state: TradeState.created,
283 amount: request.fromAmount,
284 receiveAmount: receiveAmount ?? request.toAmount,
285 payoutAddress: payoutAddress,
286 createdAt: DateTime.now(),
287 isSendAll: isSendAll,
288 toAddressExtraId: request.toAddressExtraId,
289 );
290 }
291
292 @override
293 Future<Trade> findTradeById({required String id}) async {
294 final params = {'api_key': apiKey, 'id': id};
295 final uri = Uri.https(apiAuthority, getExchangePath, params);
296 final response = await ProxyWrapper().get(clearnetUri: uri);
297
298 if (response.statusCode == 404) {
299 throw TradeNotFoundException(id, provider: description);
300 }
301
302 if (response.statusCode == 400) {
303 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
304 final error = responseJSON['message'] as String;
305
306 throw TradeNotFoundException(id, provider: description, description: error);
307 }
308
309 if (response.statusCode != 200) {
310 throw Exception('Unexpected http status: ${response.statusCode}');
311 }
312
313 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
314 final fromCurrency = responseJSON['currency_from'] as String;
315 final toCurrency = responseJSON['currency_to'] as String;
316 final inputAddress = responseJSON['address_from'] as String;
317 final expectedSendAmount = responseJSON['expected_amount'].toString();
318 final extraId = responseJSON['extra_id_from'] as String?;
319 final status = responseJSON['status'] as String;
320 final payoutAddress = responseJSON['address_to'] as String;
321
322 final fromParsed = CryptoCurrency.safeParseCurrencyFromString(fromCurrency);
323 final toParsed = CryptoCurrency.safeParseCurrencyFromString(toCurrency);
324 return Trade(
325 id: id,
326 from: fromParsed,
327 to: toParsed,
328 extraId: extraId,
329 provider: description,
330 inputAddress: inputAddress,
331 amount: expectedSendAmount,
332 state: TradeState.deserialize(raw: status),
333 payoutAddress: payoutAddress,
334 );
335 }
336
337 static String _normalizeCurrency(CryptoCurrency currency) {
338 switch (currency) {
339 case CryptoCurrency.zec:
340 return 'zec';
341 case CryptoCurrency.bnb:
342 return currency.tag!.toLowerCase();
343 case CryptoCurrency.usdterc20:
344 return 'usdterc20';
345 case CryptoCurrency.usdttrc20:
346 return 'usdttrc20';
347 case CryptoCurrency.usdcpoly:
348 return 'usdcpoly';
349 case CryptoCurrency.usdtPoly:
350 return 'usdtpoly';
351 case CryptoCurrency.usdcEPoly:
352 return 'usdcepoly';
353 case CryptoCurrency.usdcsol:
354 return 'usdcspl';
355 case CryptoCurrency.matic:
356 return 'pol';
357 case CryptoCurrency.maticpoly:
358 return 'matic';
359 default:
360 return currency.title.toLowerCase();
361 }
362 }
363
364 String _normalizeAddress(String address) =>
365 address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address;
366 }