From 7d11d0461f5d32357572b8e8f1753f6c6c402aeb Mon Sep 17 00:00:00 2001 From: Serhii Date: Wed, 11 Sep 2024 05:14:17 +0300 Subject: [PATCH] Integrate LetsExchange exchange provider (#1562) * letsExchange provider * add api key * secrets affiliateId * Update letsexchange_exchange_provider.dart * minor fix [skip ci] * fix network type issue * tracking link [skip ci] * fix data type * normalise bch address --------- Co-authored-by: Omar Hatem --- .github/workflows/pr_test_build_android.yml | 2 + .github/workflows/pr_test_build_linux.yml | 2 + assets/images/letsexchange_icon.svg | 5 + .../exchange_provider_description.dart | 6 +- .../letsexchange_exchange_provider.dart | 292 ++++++++++++++++++ .../stealth_ex_exchange_provider.dart | 2 +- lib/exchange/trade_state.dart | 2 + .../screens/dashboard/widgets/trade_row.dart | 4 +- .../exchange_trade/exchange_confirm_page.dart | 4 +- lib/store/dashboard/trade_filter_store.dart | 11 + lib/utils/image_utill.dart | 60 ++++ .../dashboard/dashboard_view_model.dart | 5 + .../exchange/exchange_view_model.dart | 2 + lib/view_model/trade_details_view_model.dart | 5 + tool/utils/secret_key.dart | 2 + 15 files changed, 400 insertions(+), 4 deletions(-) create mode 100644 assets/images/letsexchange_icon.svg create mode 100644 lib/exchange/provider/letsexchange_exchange_provider.dart create mode 100644 lib/utils/image_utill.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index b60ae1f7e..1b26d87d0 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -168,6 +168,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart + echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index e25bea2ec..db88d7850 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -154,6 +154,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart + echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart diff --git a/assets/images/letsexchange_icon.svg b/assets/images/letsexchange_icon.svg new file mode 100644 index 000000000..104b43a6b --- /dev/null +++ b/assets/images/letsexchange_icon.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index a91288024..9f3723356 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -27,8 +27,10 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png'); static const quantex = ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png'); + static const letsExchange = + ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg'); static const stealthEx = - ExchangeProviderDescription(title: 'StealthEx', raw: 10, image: 'assets/images/stealthex.png'); + ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png'); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -53,6 +55,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< case 9: return quantex; case 10: + return letsExchange; + case 11: return stealthEx; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart new file mode 100644 index 000000000..a11e69796 --- /dev/null +++ b/lib/exchange/provider/letsexchange_exchange_provider.dart @@ -0,0 +1,292 @@ +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; + + 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'; + case 'POLY': + return 'MATIC'; + default: + return currency.tag!; + } + } + return currency.title; + } + + String _normalizeBchAddress(String address) => + address.startsWith('bitcoincash:') ? address.substring(12) : address; +} diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart index 3e05091e6..601735595 100644 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -69,7 +69,7 @@ class StealthExExchangeProvider extends ExchangeProvider { throw Exception('StealthEx fetch limits failed: ${response.body}'); } final responseJSON = json.decode(response.body) as Map; - final min = responseJSON['min_amount'] as double?; + final min = toDouble(responseJSON['min_amount']); final max = responseJSON['max_amount'] as double?; return Limits(min: min, max: max); } catch (e) { diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index e94906763..6d2472a11 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -106,6 +106,7 @@ class TradeState extends EnumerableItem with Serializable { case 'waitingAuthorization': return waitingAuthorization; case 'failed': + case 'error': return failed; case 'completed': return completed; @@ -125,6 +126,7 @@ class TradeState extends EnumerableItem with Serializable { case 'exchanging': return exchanging; case 'sending': + case 'sending_confirmation': return sending; case 'success': case 'done': diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index caccb8047..7c809aa9d 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/utils/image_utill.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; @@ -36,7 +37,8 @@ class TradeRow extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(50), - child: Image.asset(provider.image, width: 36, height: 36)), + child: ImageUtil.getImageFromPath( + imagePath: provider.image, height: 36, width: 36)), SizedBox(width: 12), Expanded( child: Column( diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart index 8070febdf..bf307dce6 100644 --- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart +++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/utils/image_utill.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; @@ -101,7 +102,8 @@ class ExchangeConfirmPage extends BasePage { mainAxisAlignment: MainAxisAlignment.center, children: [ (trade.provider.image?.isNotEmpty ?? false) - ? Image.asset(trade.provider.image, height: 50) + ? ImageUtil.getImageFromPath( + imagePath: trade.provider.image, width: 50) : const SizedBox(), if (!trade.provider.horizontalLogo) Padding( diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index 4ceebedfd..c1e462cd6 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -17,6 +17,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = true, displayExolix = true, displayThorChain = true, + displayLetsExchange = true, displayStealthEx = true; @observable @@ -43,6 +44,9 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayThorChain; + @observable + bool displayLetsExchange; + @observable bool displayStealthEx; @@ -54,6 +58,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador && displayExolix && displayThorChain && + displayLetsExchange && displayStealthEx; @action @@ -83,6 +88,8 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.thorChain: displayThorChain = !displayThorChain; break; + case ExchangeProviderDescription.letsExchange: + displayLetsExchange = !displayLetsExchange; case ExchangeProviderDescription.stealthEx: displayStealthEx = !displayStealthEx; break; @@ -96,6 +103,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = false; displayExolix = false; displayThorChain = false; + displayLetsExchange = false; displayStealthEx = false; } else { displayChangeNow = true; @@ -106,6 +114,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = true; displayExolix = true; displayThorChain = true; + displayLetsExchange = true; displayStealthEx = true; } break; @@ -134,6 +143,8 @@ abstract class TradeFilterStoreBase with Store { (displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) || (displayThorChain && item.trade.provider == ExchangeProviderDescription.thorChain) || + (displayLetsExchange && + item.trade.provider == ExchangeProviderDescription.letsExchange) || (displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx)) .toList() : _trades; diff --git a/lib/utils/image_utill.dart b/lib/utils/image_utill.dart new file mode 100644 index 000000000..a138df23a --- /dev/null +++ b/lib/utils/image_utill.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ImageUtil { + static Widget getImageFromPath({required String imagePath, double? height, double? width}) { + final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https'); + final bool isSvg = imagePath.endsWith('.svg'); + final double _height = height ?? 35; + final double _width = width ?? 35; + + if (isNetworkImage) { + return isSvg + ? SvgPicture.network( + imagePath, + height: _height, + width: _width, + placeholderBuilder: (BuildContext context) => Container( + height: _height, + width: _width, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ) + : Image.network( + imagePath, + height: _height, + width: _width, + loadingBuilder: + (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Container( + height: _height, + width: _width, + child: Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) { + return Container( + height: _height, + width: _width, + ); + }, + ); + } else { + return isSvg + ? SvgPicture.asset(imagePath, height: _height, width: _width) + : Image.asset(imagePath, height: _height, width: _width); + } + } +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index dc96c4461..d69d662e1 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -129,6 +129,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.thorChain.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)), + FilterItem( + value: () => tradeFilterStore.displayLetsExchange, + caption: ExchangeProviderDescription.letsExchange.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.letsExchange)), FilterItem( value: () => tradeFilterStore.displayStealthEx, caption: ExchangeProviderDescription.stealthEx.title, diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index bd9474e39..ca56750f0 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/core/create_trade_result.dart'; +import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; @@ -167,6 +168,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with ThorChainExchangeProvider(tradesStore: trades), if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), QuantexExchangeProvider(), + LetsExchangeExchangeProvider(), StealthExExchangeProvider(), TrocadorExchangeProvider( useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index e71d97ae0..19315f40d 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; @@ -60,6 +61,8 @@ abstract class TradeDetailsViewModelBase with Store { break; case ExchangeProviderDescription.quantex: _provider = QuantexExchangeProvider(); + case ExchangeProviderDescription.letsExchange: + _provider = LetsExchangeExchangeProvider(); break; case ExchangeProviderDescription.stealthEx: _provider = StealthExExchangeProvider(); @@ -90,6 +93,8 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://track.ninerealms.com/${trade.id}'; case ExchangeProviderDescription.quantex: return 'https://myquantex.com/send/${trade.id}'; + case ExchangeProviderDescription.letsExchange: + return 'https://letsexchange.io/?transactionId=${trade.id}'; case ExchangeProviderDescription.stealthEx: return 'https://stealthex.io/exchange/?id=${trade.id}'; } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 0741ab4e7..96787a403 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -43,6 +43,8 @@ class SecretKey { SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('letsExchangeBearerToken', () => ''), + SecretKey('letsExchangeAffiliateId', () => ''), SecretKey('stealthExBearerToken', () => ''), SecretKey('stealthExAdditionalFeePercent', () => ''), ];