2024-09-11 02:14:17 +00:00
|
|
|
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<CryptoCurrency> _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<bool> checkIsAvailable() async => true;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<Limits> 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<double> 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;
|
|
|
|
|
2024-12-02 22:26:33 +00:00
|
|
|
if (amountToGet == 0.0) return 0.0;
|
|
|
|
|
2024-09-11 02:14:17 +00:00
|
|
|
return isFixedRateMode ? amount / amountToGet : amountToGet / amount;
|
|
|
|
} catch (e) {
|
|
|
|
log(e.toString());
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<Trade> 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<String, dynamic>;
|
|
|
|
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<Trade> 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<String, dynamic>;
|
|
|
|
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<Map<String, dynamic>> _getInfo(Map<String, String> 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<String, dynamic>;
|
|
|
|
} 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;
|
|
|
|
}
|