dev
dart 417 lines 13.8 KB
Raw
1 import 'dart:convert';
2
3 import 'package:cake_wallet/.secrets.g.dart' as secrets;
4 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
5 import 'package:cake_wallet/exchange/limits.dart';
6 import 'package:cake_wallet/exchange/provider/exchange_provider.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 SwapTradeExchangeProvider extends ExchangeProvider {
18 SwapTradeExchangeProvider();
19
20 static final markup = secrets.swapTradeExchangeMarkup;
21
22 static const apiAuthority = 'api.swaptrade.io';
23 static const getRate = '/api/swap/get-rate';
24 static const getCoins = '/api/swap/get-coins';
25 static const createOrder = '/api/swap/create-order';
26 static const order = '/api/swap/order';
27
28 @override
29 String get title => 'SwapTrade';
30
31 @override
32 bool get isAvailable => true;
33
34 @override
35 bool get isEnabled => true;
36
37 @override
38 bool get supportsFixedRate => false;
39
40 @override
41 bool get supportsMemoOrDestinationTag => false;
42
43 @override
44 ExchangeProviderDescription get description => ExchangeProviderDescription.swapTrade;
45
46 static const _headers = <String, String>{'Content-Type': 'application/json'};
47
48 @override
49 Future<bool> checkIsAvailable() async => true;
50
51 @override
52 Future<Limits?> fetchLimits({
53 required CryptoCurrency from,
54 required CryptoCurrency to,
55 required bool isFixedRateMode,
56 }) async {
57 try {
58 final uri = Uri.https(apiAuthority, getCoins);
59 final response = await ProxyWrapper().get(clearnetUri: uri);
60
61 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
62
63 if (response.statusCode != 200)
64 throw Exception('Unexpected http status: ${response.statusCode}');
65
66 final coinsInfoRaw = responseJSON['data'];
67 final coinsInfo = coinsInfoRaw is List<dynamic> ? coinsInfoRaw : <dynamic>[];
68
69 final normalized = _normalizeCurrency(from);
70 final coin = coinsInfo.cast<Map<String, dynamic>>().firstWhere(
71 (c) => (c['id']?.toString().toUpperCase() ?? '') == normalized,
72 orElse: () => <String, dynamic>{},
73 );
74
75 if (coin.isEmpty) {
76 // Currency not supported by SwapTrade (e.g. USDC, DOGE).
77 return null;
78 }
79
80 final min = double.tryParse(coin['min']?.toString() ?? '') ?? 0.0;
81 final max = double.tryParse(coin['max']?.toString() ?? '') ?? 0.0;
82 if (max == 0) return null;
83 return Limits(min: min, max: max);
84 } catch (e) {
85 printV(e.toString());
86 return null;
87 }
88 }
89
90 @override
91 Future<double> fetchRate(
92 {required CryptoCurrency from,
93 required CryptoCurrency to,
94 required double amount,
95 required bool isFixedRateMode,
96 required bool isReceiveAmount}) async {
97 try {
98 if (amount == 0) return 0.0;
99 if (isFixedRateMode && !supportsFixedRate) {
100 return 0.0;
101 }
102 if (from == CryptoCurrency.btcln || to == CryptoCurrency.btcln) return 0;
103
104 final params = <String, dynamic>{};
105 final body = <String, String>{
106 'coin_send': _normalizeCurrency(from),
107 'coin_receive': _normalizeCurrency(to),
108 'amount': amount.toString(),
109 'ref': 'cake',
110 };
111
112 final uri = Uri.https(apiAuthority, getRate, params);
113 final response = await ProxyWrapper().post(
114 clearnetUri: uri,
115 body: json.encode(body),
116 headers: _headers,
117 );
118
119 final responseBody = json.decode(response.body) as Map<String, dynamic>;
120
121 if (response.statusCode != 200) {
122 ExchangeProviderLogger.logError(
123 provider: description,
124 function: 'fetchRate',
125 error: Exception('Unexpected http status: ${response.statusCode}'),
126 stackTrace: StackTrace.current,
127 requestData: {
128 'from': from.title,
129 'to': to.title,
130 'amount': amount,
131 'isFixedRateMode': isFixedRateMode,
132 'isReceiveAmount': isReceiveAmount,
133 'body': body,
134 'url': uri.toString(),
135 },
136 );
137 throw Exception('Unexpected http status: ${response.statusCode}');
138 }
139
140 final data = responseBody['data'] as Map<String, dynamic>;
141 double rate = double.parse(data['price'].toString());
142 final calculatedRate = rate > 0
143 ? isFixedRateMode
144 ? amount / rate
145 : rate / amount
146 : 0.0;
147
148 ExchangeProviderLogger.logSuccess(
149 provider: description,
150 function: 'fetchRate',
151 requestData: {
152 'from': from.title,
153 'to': to.title,
154 'amount': amount,
155 'isFixedRateMode': isFixedRateMode,
156 'isReceiveAmount': isReceiveAmount,
157 'body': body,
158 'url': uri.toString(),
159 },
160 responseData: {
161 'rate': rate,
162 'calculatedRate': calculatedRate,
163 'statusCode': response.statusCode,
164 'responseBody': responseBody,
165 },
166 );
167
168 return calculatedRate;
169 } catch (e, s) {
170 ExchangeProviderLogger.logError(
171 provider: description,
172 function: 'fetchRate',
173 error: e,
174 stackTrace: s,
175 requestData: {
176 'from': from.title,
177 'to': to.title,
178 'amount': amount,
179 'isFixedRateMode': isFixedRateMode,
180 'isReceiveAmount': isReceiveAmount,
181 },
182 );
183 printV("error fetching rate: ${e.toString()}");
184 return 0.0;
185 }
186 }
187
188 @override
189 Future<Trade> createTrade({
190 required TradeRequest request,
191 required bool isFixedRateMode,
192 required bool isSendAll,
193 }) async {
194 try {
195 final params = <String, dynamic>{};
196 var body = <String, dynamic>{
197 'coin_send': _normalizeCurrency(request.fromCurrency),
198 'coin_send_network': _networkFor(request.fromCurrency),
199 'coin_receive': _normalizeCurrency(request.toCurrency),
200 'coin_receive_network': _networkFor(request.toCurrency),
201 'amount_send': request.fromAmount,
202 'recipient': request.toAddress,
203 'ref': 'cake',
204 'markup': double.tryParse(markup.toString()) ?? 0,
205 'refund_address': request.refundAddress,
206 };
207
208 final uri = Uri.https(apiAuthority, createOrder, params);
209 final response = await ProxyWrapper().post(
210 clearnetUri: uri,
211 body: json.encode(body),
212 headers: _headers,
213 );
214
215 final responseBody = json.decode(response.body) as Map<String, dynamic>;
216
217 if (response.statusCode == 400 || responseBody["success"] == false) {
218 final List<dynamic> errorsList = responseBody['errors'] as List? ?? [];
219 final error = errorsList.isNotEmpty
220 ? (errorsList[0]['msg'] as String?) ?? responseBody.toString()
221 : responseBody.toString();
222
223 ExchangeProviderLogger.logError(
224 provider: description,
225 function: 'createTrade',
226 error: TradeNotCreatedException(description, description: error),
227 stackTrace: StackTrace.current,
228 requestData: {
229 'from': request.fromCurrency.title,
230 'to': request.toCurrency.title,
231 'fromAmount': request.fromAmount,
232 'toAmount': request.toAmount,
233 'toAddress': request.toAddress,
234 'refundAddress': request.refundAddress,
235 'isFixedRateMode': isFixedRateMode,
236 'isSendAll': isSendAll,
237 'body': body,
238 'url': uri.toString(),
239 },
240 );
241
242 throw TradeNotCreatedException(description, description: error);
243 }
244
245 if (response.statusCode != 200) {
246 ExchangeProviderLogger.logError(
247 provider: description,
248 function: 'createTrade',
249 error: Exception('Unexpected http status: ${response.statusCode}'),
250 stackTrace: StackTrace.current,
251 requestData: {
252 'from': request.fromCurrency.title,
253 'to': request.toCurrency.title,
254 'fromAmount': request.fromAmount,
255 'toAmount': request.toAmount,
256 'toAddress': request.toAddress,
257 'refundAddress': request.refundAddress,
258 'isFixedRateMode': isFixedRateMode,
259 'isSendAll': isSendAll,
260 'body': body,
261 'url': uri.toString(),
262 },
263 );
264 throw Exception('Unexpected http status: ${response.statusCode}');
265 }
266
267 final responseData = responseBody['data'] as Map<String, dynamic>;
268 final receiveAmount = responseData["amount_receive"]?.toString();
269
270 ExchangeProviderLogger.logSuccess(
271 provider: description,
272 function: 'createTrade',
273 requestData: {
274 'from': request.fromCurrency.title,
275 'to': request.toCurrency.title,
276 'fromAmount': request.fromAmount,
277 'toAmount': request.toAmount,
278 'toAddress': request.toAddress,
279 'refundAddress': request.refundAddress,
280 'isFixedRateMode': isFixedRateMode,
281 'isSendAll': isSendAll,
282 'body': body,
283 'url': uri.toString(),
284 },
285 responseData: {
286 'id': responseData["order_id"] as String,
287 'inputAddress': responseData["server_address"] as String,
288 'receiveAmount': receiveAmount,
289 'statusCode': response.statusCode,
290 'responseBody': responseBody,
291 },
292 );
293
294 return Trade(
295 id: responseData["order_id"] as String,
296 inputAddress: responseData["server_address"] as String,
297 amount: request.fromAmount,
298 receiveAmount: receiveAmount ?? request.toAmount,
299 from: request.fromCurrency,
300 to: request.toCurrency,
301 provider: description,
302 createdAt: DateTime.now(),
303 state: TradeState.created,
304 payoutAddress: request.toAddress,
305 isSendAll: isSendAll,
306 );
307 } catch (e, s) {
308 ExchangeProviderLogger.logError(
309 provider: description,
310 function: 'createTrade',
311 error: e,
312 stackTrace: s,
313 requestData: {
314 'from': request.fromCurrency.title,
315 'to': request.toCurrency.title,
316 'fromAmount': request.fromAmount,
317 'toAmount': request.toAmount,
318 'toAddress': request.toAddress,
319 'refundAddress': request.refundAddress,
320 'isFixedRateMode': isFixedRateMode,
321 'isSendAll': isSendAll,
322 },
323 );
324 printV("error creating trade: ${e.toString()}");
325 throw TradeNotCreatedException(description, description: e.toString());
326 }
327 }
328
329 @override
330 Future<Trade> findTradeById({required String id}) async {
331 try {
332 final params = <String, dynamic>{};
333 var body = <String, dynamic>{
334 'order_id': id,
335 };
336
337 final uri = Uri.https(apiAuthority, order, params);
338 final response = await ProxyWrapper().post(
339 clearnetUri: uri,
340 body: json.encode(body),
341 headers: _headers,
342 );
343
344 final responseBody = json.decode(response.body) as Map<String, dynamic>;
345
346 if (response.statusCode == 400 || responseBody["success"] == false) {
347 final error = responseBody['errors'][0]['msg'] as String;
348 throw TradeNotCreatedException(description, description: error);
349 }
350
351 if (response.statusCode != 200)
352 throw Exception('Unexpected http status: ${response.statusCode}');
353
354 final responseData = responseBody['data'] as Map<String, dynamic>;
355 final fromCurrency = responseData['coin_send'] as String;
356 final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency);
357 final toCurrency = responseData['coin_receive'] as String;
358 final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency);
359 final inputAddress = responseData['server_address'] as String;
360 final payoutAddress = responseData['recipient'] as String;
361 final status = responseData['status'] as String;
362 final state = TradeState.deserialize(raw: status);
363 final response_id = responseData['order_id'] as String;
364 final expectedSendAmount = responseData['amount_send'] as String;
365 final expectedReceiveAmount = responseData['amount_receive'] as String;
366 final memo = responseData['memo'] as String?;
367 final createdAt = responseData['created_at'] as String?;
368
369 return Trade(
370 id: response_id,
371 from: from,
372 to: to,
373 provider: description,
374 inputAddress: inputAddress,
375 amount: expectedSendAmount,
376 payoutAddress: payoutAddress,
377 state: state,
378 receiveAmount: expectedReceiveAmount,
379 memo: memo,
380 createdAt: DateTime.tryParse(createdAt ?? ''),
381 );
382 } catch (e) {
383 printV("error getting trade: ${e.toString()}");
384 throw TradeNotFoundException(
385 id,
386 provider: description,
387 description: e.toString(),
388 );
389 }
390 }
391
392 String _normalizeCurrency(CryptoCurrency currency) {
393 switch (currency) {
394 default:
395 return currency.title.toUpperCase();
396 }
397 }
398
399 String _networkFor(CryptoCurrency currency) {
400 final network = switch (currency) {
401 CryptoCurrency.eth => 'ETH',
402 CryptoCurrency.bnb => 'BNB_BSC',
403 CryptoCurrency.usdterc20 => 'USDT_ERC20',
404 CryptoCurrency.usdttrc20 => 'TRX_USDT_S2UZ',
405 CryptoCurrency.usdtbsc => 'USDT_BSC',
406 CryptoCurrency.sol => 'SOL',
407 CryptoCurrency.btc => 'BTC',
408 CryptoCurrency.xmr => 'XMR',
409 CryptoCurrency.ltc => 'LTC',
410 CryptoCurrency.ada => 'ADA',
411 CryptoCurrency.bch => 'BCH',
412 CryptoCurrency.zec => 'ZEC',
413 _ => currency.title.toUpperCase(),
414 };
415 return network;
416 }
417 }