dev
dart 477 lines 15.1 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:cw_core/utils/proxy_wrapper.dart';
13 import 'package:cw_core/crypto_currency.dart';
14 import 'package:cw_core/utils/print_verbose.dart';
15 import 'package:cake_wallet/utils/exchange_provider_logger.dart';
16
17 class SideShiftExchangeProvider extends ExchangeProvider {
18 SideShiftExchangeProvider();
19
20 static const affiliateId = secrets.sideShiftAffiliateId;
21 static const apiBaseUrl = 'https://sideshift.ai/api';
22 static const rangePath = '/v2/pair';
23 static const orderPath = '/v2/shifts';
24 static const quotePath = '/v2/quotes';
25 static const permissionPath = '/v2/permissions';
26
27 @override
28 String get title => 'SideShift';
29
30 @override
31 bool get isAvailable => true;
32
33 @override
34 bool get isEnabled => true;
35
36 @override
37 bool get supportsFixedRate => true;
38
39 @override
40 ExchangeProviderDescription get description => ExchangeProviderDescription.sideShift;
41
42 @override
43 Future<bool> checkIsAvailable() async {
44 const url = apiBaseUrl + permissionPath;
45 final uri = Uri.parse(url);
46 final response = await ProxyWrapper().get(clearnetUri: uri);
47
48 if (response.statusCode == 500) {
49 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
50 final error = responseJSON['error']['message'] as String;
51
52 throw Exception('$error');
53 }
54
55 if (response.statusCode != 200) return false;
56
57 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
58 return responseJSON['createShift'] as bool;
59 }
60
61 @override
62 Future<Limits?> fetchLimits(
63 {required CryptoCurrency from,
64 required CryptoCurrency to,
65 required bool isFixedRateMode}) async {
66 final fromCurrency = isFixedRateMode ? to : from;
67 final toCurrency = isFixedRateMode ? from : to;
68
69 final fromNetwork = _networkFor(fromCurrency);
70 final toNetwork = _networkFor(toCurrency);
71
72 final url =
73 "$apiBaseUrl$rangePath/${fromCurrency.title.toLowerCase()}-$fromNetwork/${toCurrency.title.toLowerCase()}-$toNetwork";
74
75 final uri = Uri.parse(url);
76 final response = await ProxyWrapper().get(clearnetUri: uri);
77
78 if (response.statusCode == 500) {
79 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
80 final error = responseJSON['error']['message'] as String;
81
82 throw Exception('$error');
83 }
84
85 if (response.statusCode != 200) {
86 throw Exception('Unexpected http status: ${response.statusCode}');
87 }
88
89 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
90 final min = double.tryParse(responseJSON['min'] as String? ?? '');
91 final max = double.tryParse(responseJSON['max'] as String? ?? '');
92
93 if (isFixedRateMode) {
94 final currentRate = double.parse(responseJSON['rate'] as String);
95 return Limits(
96 min: min != null ? (min * currentRate) : null,
97 max: max != null ? (max * currentRate) : null,
98 );
99 }
100
101 return Limits(min: min, max: max);
102 }
103
104 @override
105 Future<double> fetchRate(
106 {required CryptoCurrency from,
107 required CryptoCurrency to,
108 required double amount,
109 required bool isFixedRateMode,
110 required bool isReceiveAmount}) async {
111 try {
112 if (amount == 0) return 0.0;
113
114 final fromCurrency = from.title.toLowerCase();
115 final toCurrency = to.title.toLowerCase();
116 final depositNetwork = _networkFor(from);
117 final settleNetwork = _networkFor(to);
118
119 final url =
120 "$apiBaseUrl$rangePath/$fromCurrency-$depositNetwork/$toCurrency-$settleNetwork?amount=$amount";
121
122 final uri = Uri.parse(url);
123 final response = await ProxyWrapper().get(clearnetUri: uri);
124
125 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
126
127 if (response.statusCode == 500) {
128 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
129 final error = responseJSON['error']['message'] as String;
130
131 ExchangeProviderLogger.logError(
132 provider: description,
133 function: 'fetchRate',
134 error: Exception('SideShift Internal Server Error: $error'),
135 stackTrace: StackTrace.current,
136 requestData: {
137 'from': from.title,
138 'to': to.title,
139 'amount': amount,
140 'isFixedRateMode': isFixedRateMode,
141 'isReceiveAmount': isReceiveAmount,
142 'url': url,
143 },
144 );
145
146 throw Exception('SideShift Internal Server Error: $error');
147 }
148
149 if (response.statusCode != 200) {
150 ExchangeProviderLogger.logError(
151 provider: description,
152 function: 'fetchRate',
153 error: Exception('Unexpected http status: ${response.statusCode}'),
154 stackTrace: StackTrace.current,
155 requestData: {
156 'from': from.title,
157 'to': to.title,
158 'amount': amount,
159 'isFixedRateMode': isFixedRateMode,
160 'isReceiveAmount': isReceiveAmount,
161 'url': url,
162 },
163 );
164
165 throw Exception('Unexpected http status: ${response.statusCode}');
166 }
167
168 final rate = double.parse(responseJSON['rate'] as String);
169
170 ExchangeProviderLogger.logSuccess(
171 provider: description,
172 function: 'fetchRate',
173 requestData: {
174 'from': from.title,
175 'to': to.title,
176 'amount': amount,
177 'isFixedRateMode': isFixedRateMode,
178 'isReceiveAmount': isReceiveAmount,
179 'url': url,
180 },
181 responseData: {
182 'rate': rate,
183 'statusCode': response.statusCode,
184 },
185 );
186
187 return rate;
188 } catch (e, s) {
189 ExchangeProviderLogger.logError(
190 provider: description,
191 function: 'fetchRate',
192 error: e,
193 stackTrace: s,
194 requestData: {
195 'from': from.title,
196 'to': to.title,
197 'amount': amount,
198 'isFixedRateMode': isFixedRateMode,
199 'isReceiveAmount': isReceiveAmount,
200 },
201 );
202 printV(e.toString());
203 return 0.00;
204 }
205 }
206
207 @override
208 Future<Trade> createTrade({
209 required TradeRequest request,
210 required bool isFixedRateMode,
211 required bool isSendAll,
212 }) async {
213 String url = '';
214 final body = {
215 'affiliateId': affiliateId,
216 'settleAddress': request.toAddress,
217 if (request.toAddressExtraId.isNotEmpty) 'settleMemo': request.toAddressExtraId,
218 'refundAddress': request.refundAddress,
219 };
220
221 if (isFixedRateMode) {
222 final quoteId = await _createQuote(request);
223 body['quoteId'] = quoteId;
224
225 url = apiBaseUrl + orderPath + '/fixed';
226 } else {
227 url = apiBaseUrl + orderPath + '/variable';
228 body["depositCoin"] = _normalizeCurrency(request.fromCurrency);
229 body["settleCoin"] = _normalizeCurrency(request.toCurrency);
230 body["settleNetwork"] = _networkFor(request.toCurrency);
231 body["depositNetwork"] = _networkFor(request.fromCurrency);
232 }
233 final headers = {'Content-Type': 'application/json'};
234
235 final uri = Uri.parse(url);
236 final response = await ProxyWrapper().post(
237 clearnetUri: uri,
238 headers: headers,
239 body: json.encode(body),
240 );
241
242 if (response.statusCode != 201) {
243 if (response.statusCode == 400) {
244 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
245 final error = responseJSON['error']['message'] as String;
246
247 ExchangeProviderLogger.logError(
248 provider: description,
249 function: 'createTrade',
250 error: TradeNotCreatedException(description, description: error),
251 stackTrace: StackTrace.current,
252 requestData: {
253 'from': request.fromCurrency.title,
254 'to': request.toCurrency.title,
255 'fromAmount': request.fromAmount,
256 'toAmount': request.toAmount,
257 'toAddress': request.toAddress,
258 'refundAddress': request.refundAddress,
259 'isFixedRateMode': isFixedRateMode,
260 'isSendAll': isSendAll,
261 'url': url,
262 'body': body,
263 },
264 );
265
266 throw TradeNotCreatedException(description, description: error);
267 }
268
269 ExchangeProviderLogger.logError(
270 provider: description,
271 function: 'createTrade',
272 error: TradeNotCreatedException(description),
273 stackTrace: StackTrace.current,
274 requestData: {
275 'from': request.fromCurrency.title,
276 'to': request.toCurrency.title,
277 'fromAmount': request.fromAmount,
278 'toAmount': request.toAmount,
279 'toAddress': request.toAddress,
280 'refundAddress': request.refundAddress,
281 'isFixedRateMode': isFixedRateMode,
282 'isSendAll': isSendAll,
283 'url': url,
284 'body': body,
285 },
286 );
287
288 throw TradeNotCreatedException(description);
289 }
290
291 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
292 final id = responseJSON['id'] as String;
293 final inputAddress = responseJSON['depositAddress'] as String;
294 final settleAddress = responseJSON['settleAddress'] as String;
295 final depositAmount = responseJSON['depositAmount'] as String?;
296 final depositMemo = responseJSON['depositMemo'] as String?;
297
298 ExchangeProviderLogger.logSuccess(
299 provider: description,
300 function: 'createTrade',
301 requestData: {
302 'from': request.fromCurrency.title,
303 'to': request.toCurrency.title,
304 'fromAmount': request.fromAmount,
305 'toAmount': request.toAmount,
306 'toAddress': request.toAddress,
307 'refundAddress': request.refundAddress,
308 'isFixedRateMode': isFixedRateMode,
309 'isSendAll': isSendAll,
310 'url': url,
311 'body': body,
312 },
313 responseData: {
314 'id': id,
315 'inputAddress': inputAddress,
316 'settleAddress': settleAddress,
317 'depositAmount': depositAmount,
318 'depositMemo': depositMemo,
319 'statusCode': response.statusCode,
320 },
321 );
322
323 return Trade(
324 id: id,
325 provider: description,
326 from: request.fromCurrency,
327 to: request.toCurrency,
328 inputAddress: inputAddress,
329 refundAddress: settleAddress,
330 state: TradeState.created,
331 amount: depositAmount ?? request.fromAmount,
332 receiveAmount: request.toAmount,
333 payoutAddress: settleAddress,
334 createdAt: DateTime.now(),
335 isSendAll: isSendAll,
336 extraId: depositMemo,
337 toAddressExtraId: request.toAddressExtraId,
338 );
339 }
340
341 @override
342 Future<Trade> findTradeById({required String id}) async {
343 final url = apiBaseUrl + orderPath + '/' + id;
344 final uri = Uri.parse(url);
345 final response = await ProxyWrapper().get(clearnetUri: uri);
346
347 if (response.statusCode == 404) {
348 throw TradeNotFoundException(id, provider: description);
349 }
350
351 if (response.statusCode == 400) {
352 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
353 final error = responseJSON['error']['message'] as String;
354
355 throw TradeNotFoundException(id, provider: description, description: error);
356 }
357
358 if (response.statusCode != 200) {
359 throw Exception('Unexpected http status: ${response.statusCode}');
360 }
361
362 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
363 final fromCurrency = responseJSON['depositCoin'] as String;
364 final fromNetwork = responseJSON['depositNetwork'] as String?;
365 final toCurrency = responseJSON['settleCoin'] as String;
366 final toNetwork = responseJSON['settleNetwork'] as String?;
367 final inputAddress = responseJSON['depositAddress'] as String;
368 final expectedSendAmount = responseJSON['depositAmount'] as String?;
369 final status = responseJSON['status'] as String?;
370 final settleAddress = responseJSON['settleAddress'] as String;
371 final isVariable = (responseJSON['type'] as String) == 'variable';
372 final expiredAtRaw = responseJSON['expiresAt'] as String;
373 final expiredAt = isVariable ? null : DateTime.tryParse(expiredAtRaw)?.toLocal();
374 final depositMemo = responseJSON['depositMemo'] as String?;
375
376 final fromParsed = CryptoCurrency.safeParseCurrencyFromString(
377 fromCurrency,
378 tag: fromNetwork,
379 );
380 final toParsed = CryptoCurrency.safeParseCurrencyFromString(
381 toCurrency,
382 tag: toNetwork,
383 );
384 return Trade(
385 id: id,
386 from: fromParsed,
387 to: toParsed,
388 provider: description,
389 inputAddress: inputAddress,
390 amount: expectedSendAmount ?? '',
391 state: TradeState.deserialize(raw: status ?? 'created'),
392 expiredAt: expiredAt,
393 payoutAddress: settleAddress,
394 extraId: depositMemo,
395 );
396 }
397
398 Future<String> _createQuote(TradeRequest request) async {
399 final url = apiBaseUrl + quotePath;
400 final headers = {'Content-Type': 'application/json'};
401 final body = {
402 'depositCoin': _normalizeCurrency(request.fromCurrency),
403 'settleCoin': _normalizeCurrency(request.toCurrency),
404 'affiliateId': affiliateId,
405 'settleAmount': request.toAmount,
406 'settleNetwork': _networkFor(request.toCurrency),
407 'depositNetwork': _networkFor(request.fromCurrency),
408 };
409 final uri = Uri.parse(url);
410 final response = await ProxyWrapper().post(
411 clearnetUri: uri,
412 headers: headers,
413 body: json.encode(body),
414 );
415
416 if (response.statusCode != 201) {
417 if (response.statusCode == 400) {
418 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
419 final error = responseJSON['error']['message'] as String;
420
421 throw TradeNotCreatedException(description, description: error);
422 }
423
424 throw TradeNotCreatedException(description);
425 }
426
427 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
428
429 return responseJSON['id'] as String;
430 }
431
432 String _normalizeCurrency(CryptoCurrency currency) {
433 switch (currency) {
434 case CryptoCurrency.usdcEPoly:
435 return 'usdc';
436 default:
437 return currency.title.toLowerCase();
438 }
439 }
440
441 String _networkFor(CryptoCurrency currency) =>
442 currency.tag != null ? _normalizeTag(currency.tag!) : 'mainnet';
443
444 String _normalizeTag(String tag) {
445 switch (tag) {
446 case 'ETH':
447 return 'ethereum';
448 case 'TRX':
449 return 'tron';
450 case 'LN':
451 return 'lightning';
452 case 'POL':
453 return 'polygon';
454 case 'ARB':
455 return 'arbitrum';
456 case 'ZEC':
457 return 'zcash';
458 case 'AVAXC':
459 return 'avax';
460 default:
461 return tag.toLowerCase();
462 }
463 }
464
465 String _normalizeNetworkType(String network) {
466 return switch (network) {
467 'ethereum' => 'ETH',
468 'tron' => 'TRX',
469 'lightning' => 'LN',
470 'polygon' => 'POL',
471 'arbitrum' => 'ARB',
472 'zcash' => 'ZEC',
473 'avax' => 'AVAXC',
474 _ => network,
475 };
476 }
477 }