import 'dart:convert'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/limits.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class SimpleSwapExchangeProvider extends ExchangeProvider { SimpleSwapExchangeProvider(); static final apiKey = DeviceInfo.instance.isMobile ? secrets.simpleSwapApiKey : secrets.simpleSwapApiKeyDesktop; static const apiAuthority = 'api.simpleswap.io'; static const getEstimatePath = '/v1/get_estimated'; static const rangePath = '/v1/get_ranges'; static const getExchangePath = '/v1/get_exchange'; static const createExchangePath = '/v1/create_exchange'; @override String get title => 'SimpleSwap'; @override bool get isAvailable => false; @override bool get isEnabled => false; @override bool get supportsFixedRate => false; @override ExchangeProviderDescription get description => ExchangeProviderDescription.simpleSwap; @override Future checkIsAvailable() async { final uri = Uri.https(apiAuthority, getEstimatePath, {'api_key': apiKey}); final response = await ProxyWrapper().get(clearnetUri: uri); return !(response.statusCode == 403); } @override Future fetchLimits( {required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode}) async { final params = { 'api_key': apiKey, 'fixed': isFixedRateMode.toString(), 'currency_from': _normalizeCurrency(from), 'currency_to': _normalizeCurrency(to), }; final uri = Uri.https(apiAuthority, rangePath, params); final response = await ProxyWrapper().get(clearnetUri: uri); if (response.statusCode == 500) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['message'] as String; throw Exception('$error'); } if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } final responseJSON = json.decode(response.body) as Map; final min = double.tryParse(responseJSON['min'] as String? ?? ''); final max = double.tryParse(responseJSON['max'] as String? ?? ''); return Limits(min: min, max: max); } @override Future fetchRate({ required CryptoCurrency from, required CryptoCurrency to, required double amount, required bool isFixedRateMode, required bool isReceiveAmount }) async { try { if (amount == 0) return 0.0; final params = { 'api_key': apiKey, 'currency_from': _normalizeCurrency(from), 'currency_to': _normalizeCurrency(to), 'amount': amount.toString(), 'fixed': isFixedRateMode.toString() }; final uri = Uri.https(apiAuthority, getEstimatePath, params); final response = await ProxyWrapper().get(clearnetUri: uri); if (response.body == "null") { ExchangeProviderLogger.logError( provider: description, function: 'fetchRate', error: Exception('Null response body'), stackTrace: StackTrace.current, requestData: { 'from': from.title, 'to': to.title, 'amount': amount, 'isFixedRateMode': isFixedRateMode, 'isReceiveAmount': isReceiveAmount, 'params': params, 'url': uri.toString(), }, ); return 0.00; } final data = json.decode(response.body) as String; final rate = double.parse(data) / amount; ExchangeProviderLogger.logSuccess( provider: description, function: 'fetchRate', requestData: { 'from': from.title, 'to': to.title, 'amount': amount, 'isFixedRateMode': isFixedRateMode, 'isReceiveAmount': isReceiveAmount, 'params': params, 'url': uri.toString(), }, responseData: { 'data': data, 'rate': rate, 'statusCode': response.statusCode, 'responseBody': response.body, }, ); return rate; } catch (e, s) { ExchangeProviderLogger.logError( provider: description, function: 'fetchRate', error: e, stackTrace: s, requestData: { 'from': from.title, 'to': to.title, 'amount': amount, 'isFixedRateMode': isFixedRateMode, 'isReceiveAmount': isReceiveAmount, }, ); return 0.00; } } @override Future createTrade({ required TradeRequest request, required bool isFixedRateMode, required bool isSendAll, }) async { final headers = {'Content-Type': 'application/json'}; final params = {'api_key': apiKey}; final body = { "currency_from": _normalizeCurrency(request.fromCurrency), "currency_to": _normalizeCurrency(request.toCurrency), "amount": request.fromAmount, "fixed": isFixedRateMode, "user_refund_address": _normalizeAddress(request.refundAddress), "address_to": _normalizeAddress(request.toAddress) }; final uri = Uri.https(apiAuthority, createExchangePath, params); final response = await ProxyWrapper().post( clearnetUri: uri, headers: headers, body: json.encode(body), ); if (response.statusCode != 200 && response.statusCode != 201) { if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['message'] as String; ExchangeProviderLogger.logError( provider: description, function: 'createTrade', error: TradeNotCreatedException(description, description: error), stackTrace: StackTrace.current, requestData: { 'from': request.fromCurrency.title, 'to': request.toCurrency.title, 'fromAmount': request.fromAmount, 'toAmount': request.toAmount, 'toAddress': request.toAddress, 'refundAddress': request.refundAddress, 'isFixedRateMode': isFixedRateMode, 'isSendAll': isSendAll, 'body': body, 'url': uri.toString(), }, ); throw TradeNotCreatedException(description, description: error); } ExchangeProviderLogger.logError( provider: description, function: 'createTrade', error: TradeNotCreatedException(description), stackTrace: StackTrace.current, requestData: { 'from': request.fromCurrency.title, 'to': request.toCurrency.title, 'fromAmount': request.fromAmount, 'toAmount': request.toAmount, 'toAddress': request.toAddress, 'refundAddress': request.refundAddress, 'isFixedRateMode': isFixedRateMode, 'isSendAll': isSendAll, 'body': body, 'url': uri.toString(), }, ); throw TradeNotCreatedException(description); } final responseJSON = json.decode(response.body) as Map; final id = responseJSON['id'] as String; final inputAddress = responseJSON['address_from'] as String; final payoutAddress = responseJSON['address_to'] as String; final settleAddress = responseJSON['user_refund_address'] as String; final extraId = responseJSON['extra_id_from'] as String?; final receiveAmount = responseJSON['amount_to'] as String?; ExchangeProviderLogger.logSuccess( provider: description, function: 'createTrade', requestData: { 'from': request.fromCurrency.title, 'to': request.toCurrency.title, 'fromAmount': request.fromAmount, 'toAmount': request.toAmount, 'toAddress': request.toAddress, 'refundAddress': request.refundAddress, 'isFixedRateMode': isFixedRateMode, 'isSendAll': isSendAll, 'body': body, 'url': uri.toString(), }, responseData: { 'id': id, 'inputAddress': inputAddress, 'payoutAddress': payoutAddress, 'settleAddress': settleAddress, 'extraId': extraId, 'receiveAmount': receiveAmount, 'statusCode': response.statusCode, 'responseJSON': responseJSON, }, ); return Trade( id: id, provider: description, from: request.fromCurrency, to: request.toCurrency, inputAddress: inputAddress, refundAddress: settleAddress, extraId: extraId, state: TradeState.created, amount: request.fromAmount, receiveAmount: receiveAmount ?? request.toAmount, payoutAddress: payoutAddress, createdAt: DateTime.now(), isSendAll: isSendAll, userCurrencyFromRaw: '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}', userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}', ); } @override Future findTradeById({required String id}) async { final params = {'api_key': apiKey, 'id': id}; final uri = Uri.https(apiAuthority, getExchangePath, params); final response = await ProxyWrapper().get(clearnetUri: uri); if (response.statusCode == 404) { throw TradeNotFoundException(id, provider: description); } if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['message'] as String; throw TradeNotFoundException(id, provider: description, description: error); } if (response.statusCode != 200) { throw Exception('Unexpected http status: ${response.statusCode}'); } final responseJSON = json.decode(response.body) as Map; final fromCurrency = responseJSON['currency_from'] as String; final toCurrency = responseJSON['currency_to'] as String; final inputAddress = responseJSON['address_from'] as String; final expectedSendAmount = responseJSON['expected_amount'].toString(); final extraId = responseJSON['extra_id_from'] as String?; final status = responseJSON['status'] as String; final payoutAddress = responseJSON['address_to'] as String; return Trade( id: id, from: CryptoCurrency.safeParseCurrencyFromString(fromCurrency), to: CryptoCurrency.safeParseCurrencyFromString(toCurrency), extraId: extraId, provider: description, inputAddress: inputAddress, amount: expectedSendAmount, state: TradeState.deserialize(raw: status), payoutAddress: payoutAddress, userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_', userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_', ); } static String _normalizeCurrency(CryptoCurrency currency) { switch (currency) { case CryptoCurrency.zec: return 'zec'; case CryptoCurrency.bnb: return currency.tag!.toLowerCase(); case CryptoCurrency.usdterc20: return 'usdterc20'; case CryptoCurrency.usdttrc20: return 'usdttrc20'; case CryptoCurrency.usdcpoly: return 'usdcpoly'; case CryptoCurrency.usdtPoly: return 'usdtpoly'; case CryptoCurrency.usdcEPoly: return 'usdcepoly'; case CryptoCurrency.usdcsol: return 'usdcspl'; case CryptoCurrency.matic: return 'pol'; case CryptoCurrency.maticpoly: return 'matic'; default: return currency.title.toLowerCase(); } } String _normalizeAddress(String address) => address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address; }