import 'dart:convert'; import 'dart:developer'; 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_request.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:http/http.dart' as http; class LetsExchangeExchangeProvider extends ExchangeProvider { LetsExchangeExchangeProvider() : super(pairList: supportedPairs(_notSupported)); static const List _notSupported = []; static const apiKey = secrets.letsExchangeBearerToken; static const _baseUrl = 'api.letsexchange.io'; static const _infoPath = '/api/v1/info'; static const _infoRevertPath = '/api/v1/info-revert'; static const _createTransactionPath = '/api/v1/transaction'; static const _createTransactionRevertPath = '/api/v1/transaction-revert'; static const _getTransactionPath = '/api/v1/transaction'; static const _affiliateId = secrets.letsExchangeAffiliateId; @override String get title => 'LetsExchange'; @override bool get isAvailable => true; @override bool get isEnabled => true; @override bool get supportsFixedRate => true; @override ExchangeProviderDescription get description => ExchangeProviderDescription.letsExchange; @override Future checkIsAvailable() async => true; @override Future fetchLimits( {required CryptoCurrency from, required CryptoCurrency to, required bool isFixedRateMode}) async { final networkFrom = _getNetworkType(from); final networkTo = _getNetworkType(to); try { final params = { 'from': from.title, 'to': to.title, if (networkFrom != null) 'network_from': networkFrom, if (networkTo != null) 'network_to': networkTo, 'amount': '1', 'affiliate_id': _affiliateId }; final responseJSON = await _getInfo(params, isFixedRateMode); final min = double.tryParse(responseJSON['min_amount'] as String); final max = double.tryParse(responseJSON['max_amount'] as String); return Limits(min: min, max: max); } catch (e) { log(e.toString()); throw Exception('Failed to fetch limits'); } } @override Future fetchRate( {required CryptoCurrency from, required CryptoCurrency to, required double amount, required bool isFixedRateMode, required bool isReceiveAmount}) async { final networkFrom = _getNetworkType(from); final networkTo = _getNetworkType(to); try { final params = { 'from': from.title, 'to': to.title, if (networkFrom != null) 'network_from': networkFrom, if (networkTo != null) 'network_to': networkTo, 'amount': amount.toString(), 'affiliate_id': _affiliateId }; final responseJSON = await _getInfo(params, isFixedRateMode); final amountToGet = double.tryParse(responseJSON['amount'] as String) ?? 0.0; if (amountToGet == 0.0) return 0.0; return isFixedRateMode ? amount / amountToGet : amountToGet / amount; } catch (e) { log(e.toString()); return 0.0; } } @override Future createTrade( {required TradeRequest request, required bool isFixedRateMode, required bool isSendAll}) async { final networkFrom = _getNetworkType(request.fromCurrency); final networkTo = _getNetworkType(request.toCurrency); try { final params = { 'from': request.fromCurrency.title, 'to': request.toCurrency.title, if (networkFrom != null) 'network_from': networkFrom, if (networkTo != null) 'network_to': networkTo, 'amount': isFixedRateMode ? request.toAmount.toString() : request.fromAmount.toString(), 'affiliate_id': _affiliateId }; final responseInfoJSON = await _getInfo(params, isFixedRateMode); final rateId = responseInfoJSON['rate_id'] as String; final withdrawalAddress = _normalizeBchAddress(request.toAddress); final returnAddress = _normalizeBchAddress(request.refundAddress); final tradeParams = { 'coin_from': request.fromCurrency.title, 'coin_to': request.toCurrency.title, if (!isFixedRateMode) 'deposit_amount': request.fromAmount.toString(), 'withdrawal': withdrawalAddress, if (isFixedRateMode) 'withdrawal_amount': request.toAmount.toString(), 'withdrawal_extra_id': '', 'return': returnAddress, 'rate_id': rateId, if (networkFrom != null) 'network_from': networkFrom, if (networkTo != null) 'network_to': networkTo, 'affiliate_id': _affiliateId }; final headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': apiKey }; final uri = Uri.https(_baseUrl, isFixedRateMode ? _createTransactionRevertPath : _createTransactionPath, tradeParams); final response = await http.post(uri, headers: headers); if (response.statusCode != 200) { throw Exception('LetsExchange create trade failed: ${response.body}'); } final responseJSON = json.decode(response.body) as Map; final id = responseJSON['transaction_id'] as String; final from = responseJSON['coin_from'] as String; final to = responseJSON['coin_to'] as String; final payoutAddress = responseJSON['withdrawal'] as String; final depositAddress = responseJSON['deposit'] as String; final refundAddress = responseJSON['return'] as String; final depositAmount = responseJSON['deposit_amount'] as String; final receiveAmount = responseJSON['withdrawal_amount'] as String; final status = responseJSON['status'] as String; final createdAtString = responseJSON['created_at'] as String; final expiredAtTimestamp = responseJSON['expired_at'] as int; final createdAt = DateTime.parse(createdAtString); final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000); CryptoCurrency fromCurrency; if (request.fromCurrency.tag != null && request.fromCurrency.title == from) { fromCurrency = request.fromCurrency; } else { fromCurrency = CryptoCurrency.fromString(from); } CryptoCurrency toCurrency; if (request.toCurrency.tag != null && request.toCurrency.title == to) { toCurrency = request.toCurrency; } else { toCurrency = CryptoCurrency.fromString(to); } return Trade( id: id, from: fromCurrency, to: toCurrency, provider: description, inputAddress: depositAddress, payoutAddress: payoutAddress, refundAddress: refundAddress, amount: depositAmount, receiveAmount: receiveAmount, state: TradeState.deserialize(raw: status), createdAt: createdAt, expiredAt: expiredAt, ); } catch (e) { log(e.toString()); throw TradeNotCreatedException(description); } } @override Future findTradeById({required String id}) async { final headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': apiKey }; final url = Uri.https(_baseUrl, '$_getTransactionPath/$id'); final response = await http.get(url, headers: headers); if (response.statusCode != 200) { throw Exception('LetsExchange fetch trade failed: ${response.body}'); } final responseJSON = json.decode(response.body) as Map; final from = responseJSON['coin_from'] as String; final to = responseJSON['coin_to'] as String; final payoutAddress = responseJSON['withdrawal'] as String; final depositAddress = responseJSON['deposit'] as String; final refundAddress = responseJSON['return'] as String; final depositAmount = responseJSON['deposit_amount'] as String; final receiveAmount = responseJSON['withdrawal_amount'] as String; final status = responseJSON['status'] as String; final createdAtString = responseJSON['created_at'] as String; final expiredAtTimestamp = responseJSON['expired_at'] as int; final createdAt = DateTime.parse(createdAtString); final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000); return Trade( id: id, from: CryptoCurrency.fromString(from), to: CryptoCurrency.fromString(to), provider: description, inputAddress: depositAddress, payoutAddress: payoutAddress, refundAddress: refundAddress, amount: depositAmount, receiveAmount: receiveAmount, state: TradeState.deserialize(raw: status), createdAt: createdAt, expiredAt: expiredAt, isRefund: status == 'refund', ); } Future> _getInfo(Map params, bool isFixedRateMode) async { final headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': apiKey }; try { final uri = Uri.https(_baseUrl, isFixedRateMode ? _infoRevertPath : _infoPath, params); final response = await http.post(uri, headers: headers); if (response.statusCode != 200) { throw Exception('LetsExchange fetch info failed: ${response.body}'); } return json.decode(response.body) as Map; } catch (e) { throw Exception('LetsExchange failed to fetch info ${e.toString()}'); } } String? _getNetworkType(CryptoCurrency currency) { if (currency.tag != null && currency.tag!.isNotEmpty) { switch (currency.tag!) { case 'TRX': return 'TRC20'; case 'ETH': return 'ERC20'; case 'BSC': return 'BEP20'; default: return currency.tag!; } } return currency.title; } String _normalizeBchAddress(String address) => address.startsWith('bitcoincash:') ? address.substring(12) : address; }