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 <omarh.ismail1@gmail.com>

Serhii committed Sep 11, 2024 at 05:14 UTC 7d11d0461f5d32357572b8e8f1753f6c6c402aeb
15 files changed +400 -4
.github/workflows/pr_test_build_android.yml
+2
@@ -168,6 +168,8 @@ jobs:
168 echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
169 echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
170 echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
171 + echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
172 + echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
173 echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
174 echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart
175
.github/workflows/pr_test_build_linux.yml
+2
@@ -154,6 +154,8 @@ jobs:
154 echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
155 echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
156 echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
157 + echo "const letsExchangeBearerToken = '${{ secrets.LETS_EXCHANGE_TOKEN }}';" >> lib/.secrets.g.dart
158 + echo "const letsExchangeAffiliateId = '${{ secrets.LETS_EXCHANGE_AFFILIATE_ID }}';" >> lib/.secrets.g.dart
159 echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart
160 echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart
161
assets/images/letsexchange_icon.svg new
+5
@@ -0,0 +1,5 @@
1 +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2 + <path fill-rule="evenodd" clip-rule="evenodd"
3 + d="M16 1.37854C16 0.764286 16.6636 0.379192 17.1969 0.68395L23 4L29.4961 7.71208C29.8077 7.89012 30 8.22147 30 8.58032V16L23.9923 12.567C23.3774 12.2157 22.6226 12.2157 22.0077 12.567L16 16V8V1.37854ZM2 16V8.58032C2 8.22147 2.19229 7.89012 2.50386 7.71208L8.00772 4.56702C8.62259 4.21566 9.37741 4.21566 9.99228 4.56702L16 8L2 16ZM16 30.6215C16 31.2357 15.3364 31.6208 14.8031 31.3161L9 28L2.50386 24.2879C2.19229 24.1099 2 23.7785 2 23.4197V16L8.00772 19.433C8.62259 19.7843 9.37741 19.7843 9.99228 19.433L16 16V24V30.6215ZM22.0077 27.433C22.6226 27.7843 23.3774 27.7843 23.9923 27.433L29.4961 24.2879C29.8077 24.1099 30 23.7785 30 23.4197V16L16 24L22.0077 27.433Z"
4 + fill="#159DFF"></path>
5 +</svg>
\ No newline at end of file
lib/exchange/exchange_provider_description.dart
+5 -1
@@ -27,8 +27,10 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
27 ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png');
28 static const quantex =
29 ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png');
30 + static const letsExchange =
31 + ExchangeProviderDescription(title: 'LetsExchange', raw: 10, image: 'assets/images/letsexchange_icon.svg');
32 static const stealthEx =
31 - ExchangeProviderDescription(title: 'StealthEx', raw: 10, image: 'assets/images/stealthex.png');
33 + ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png');
34
35 static ExchangeProviderDescription deserialize({required int raw}) {
36 switch (raw) {
@@ -53,6 +55,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
55 case 9:
56 return quantex;
57 case 10:
58 + return letsExchange;
59 + case 11:
60 return stealthEx;
61 default:
62 throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
lib/exchange/provider/letsexchange_exchange_provider.dart new
+292
@@ -0,0 +1,292 @@
1 +import 'dart:convert';
2 +import 'dart:developer';
3 +
4 +import 'package:cake_wallet/.secrets.g.dart' as secrets;
5 +import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
6 +import 'package:cake_wallet/exchange/exchange_provider_description.dart';
7 +import 'package:cake_wallet/exchange/limits.dart';
8 +import 'package:cake_wallet/exchange/trade.dart';
9 +import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
10 +import 'package:cake_wallet/exchange/trade_request.dart';
11 +import 'package:cake_wallet/exchange/trade_state.dart';
12 +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
13 +import 'package:cw_core/crypto_currency.dart';
14 +import 'package:http/http.dart' as http;
15 +
16 +class LetsExchangeExchangeProvider extends ExchangeProvider {
17 + LetsExchangeExchangeProvider() : super(pairList: supportedPairs(_notSupported));
18 +
19 + static const List<CryptoCurrency> _notSupported = [];
20 +
21 + static const apiKey = secrets.letsExchangeBearerToken;
22 + static const _baseUrl = 'api.letsexchange.io';
23 + static const _infoPath = '/api/v1/info';
24 + static const _infoRevertPath = '/api/v1/info-revert';
25 + static const _createTransactionPath = '/api/v1/transaction';
26 + static const _createTransactionRevertPath = '/api/v1/transaction-revert';
27 + static const _getTransactionPath = '/api/v1/transaction';
28 +
29 + static const _affiliateId = secrets.letsExchangeAffiliateId;
30 +
31 + @override
32 + String get title => 'LetsExchange';
33 +
34 + @override
35 + bool get isAvailable => true;
36 +
37 + @override
38 + bool get isEnabled => true;
39 +
40 + @override
41 + bool get supportsFixedRate => true;
42 +
43 + @override
44 + ExchangeProviderDescription get description => ExchangeProviderDescription.letsExchange;
45 +
46 + @override
47 + Future<bool> checkIsAvailable() async => true;
48 +
49 + @override
50 + Future<Limits> fetchLimits(
51 + {required CryptoCurrency from,
52 + required CryptoCurrency to,
53 + required bool isFixedRateMode}) async {
54 + final networkFrom = _getNetworkType(from);
55 + final networkTo = _getNetworkType(to);
56 +
57 + try {
58 + final params = {
59 + 'from': from.title,
60 + 'to': to.title,
61 + if (networkFrom != null) 'network_from': networkFrom,
62 + if (networkTo != null) 'network_to': networkTo,
63 + 'amount': '1',
64 + 'affiliate_id': _affiliateId
65 + };
66 +
67 + final responseJSON = await _getInfo(params, isFixedRateMode);
68 + final min = double.tryParse(responseJSON['min_amount'] as String);
69 + final max = double.tryParse(responseJSON['max_amount'] as String);
70 + return Limits(min: min, max: max);
71 + } catch (e) {
72 + log(e.toString());
73 + throw Exception('Failed to fetch limits');
74 + }
75 + }
76 +
77 + @override
78 + Future<double> fetchRate(
79 + {required CryptoCurrency from,
80 + required CryptoCurrency to,
81 + required double amount,
82 + required bool isFixedRateMode,
83 + required bool isReceiveAmount}) async {
84 + final networkFrom = _getNetworkType(from);
85 + final networkTo = _getNetworkType(to);
86 + try {
87 + final params = {
88 + 'from': from.title,
89 + 'to': to.title,
90 + if (networkFrom != null) 'network_from': networkFrom,
91 + if (networkTo != null) 'network_to': networkTo,
92 + 'amount': amount.toString(),
93 + 'affiliate_id': _affiliateId
94 + };
95 +
96 + final responseJSON = await _getInfo(params, isFixedRateMode);
97 +
98 + final amountToGet = double.tryParse(responseJSON['amount'] as String) ?? 0.0;
99 +
100 + return isFixedRateMode ? amount / amountToGet : amountToGet / amount;
101 + } catch (e) {
102 + log(e.toString());
103 + return 0.0;
104 + }
105 + }
106 +
107 + @override
108 + Future<Trade> createTrade(
109 + {required TradeRequest request,
110 + required bool isFixedRateMode,
111 + required bool isSendAll}) async {
112 + final networkFrom = _getNetworkType(request.fromCurrency);
113 + final networkTo = _getNetworkType(request.toCurrency);
114 + try {
115 + final params = {
116 + 'from': request.fromCurrency.title,
117 + 'to': request.toCurrency.title,
118 + if (networkFrom != null) 'network_from': networkFrom,
119 + if (networkTo != null) 'network_to': networkTo,
120 + 'amount': isFixedRateMode ? request.toAmount.toString() : request.fromAmount.toString(),
121 + 'affiliate_id': _affiliateId
122 + };
123 +
124 + final responseInfoJSON = await _getInfo(params, isFixedRateMode);
125 + final rateId = responseInfoJSON['rate_id'] as String;
126 +
127 + final withdrawalAddress = _normalizeBchAddress(request.toAddress);
128 + final returnAddress = _normalizeBchAddress(request.refundAddress);
129 +
130 + final tradeParams = {
131 + 'coin_from': request.fromCurrency.title,
132 + 'coin_to': request.toCurrency.title,
133 + if (!isFixedRateMode) 'deposit_amount': request.fromAmount.toString(),
134 + 'withdrawal': withdrawalAddress,
135 + if (isFixedRateMode) 'withdrawal_amount': request.toAmount.toString(),
136 + 'withdrawal_extra_id': '',
137 + 'return': returnAddress,
138 + 'rate_id': rateId,
139 + if (networkFrom != null) 'network_from': networkFrom,
140 + if (networkTo != null) 'network_to': networkTo,
141 + 'affiliate_id': _affiliateId
142 + };
143 +
144 + final headers = {
145 + 'Content-Type': 'application/json',
146 + 'Accept': 'application/json',
147 + 'Authorization': apiKey
148 + };
149 +
150 + final uri = Uri.https(_baseUrl,
151 + isFixedRateMode ? _createTransactionRevertPath : _createTransactionPath, tradeParams);
152 + final response = await http.post(uri, headers: headers);
153 +
154 + if (response.statusCode != 200) {
155 + throw Exception('LetsExchange create trade failed: ${response.body}');
156 + }
157 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
158 + final id = responseJSON['transaction_id'] as String;
159 + final from = responseJSON['coin_from'] as String;
160 + final to = responseJSON['coin_to'] as String;
161 + final payoutAddress = responseJSON['withdrawal'] as String;
162 + final depositAddress = responseJSON['deposit'] as String;
163 + final refundAddress = responseJSON['return'] as String;
164 + final depositAmount = responseJSON['deposit_amount'] as String;
165 + final receiveAmount = responseJSON['withdrawal_amount'] as String;
166 + final status = responseJSON['status'] as String;
167 + final createdAtString = responseJSON['created_at'] as String;
168 + final expiredAtTimestamp = responseJSON['expired_at'] as int;
169 +
170 + final createdAt = DateTime.parse(createdAtString);
171 + final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000);
172 +
173 + CryptoCurrency fromCurrency;
174 + if (request.fromCurrency.tag != null && request.fromCurrency.title == from) {
175 + fromCurrency = request.fromCurrency;
176 + } else {
177 + fromCurrency = CryptoCurrency.fromString(from);
178 + }
179 +
180 + CryptoCurrency toCurrency;
181 + if (request.toCurrency.tag != null && request.toCurrency.title == to) {
182 + toCurrency = request.toCurrency;
183 + } else {
184 + toCurrency = CryptoCurrency.fromString(to);
185 + }
186 +
187 + return Trade(
188 + id: id,
189 + from: fromCurrency,
190 + to: toCurrency,
191 + provider: description,
192 + inputAddress: depositAddress,
193 + payoutAddress: payoutAddress,
194 + refundAddress: refundAddress,
195 + amount: depositAmount,
196 + receiveAmount: receiveAmount,
197 + state: TradeState.deserialize(raw: status),
198 + createdAt: createdAt,
199 + expiredAt: expiredAt,
200 + );
201 + } catch (e) {
202 + log(e.toString());
203 + throw TradeNotCreatedException(description);
204 + }
205 + }
206 +
207 + @override
208 + Future<Trade> findTradeById({required String id}) async {
209 + final headers = {
210 + 'Content-Type': 'application/json',
211 + 'Accept': 'application/json',
212 + 'Authorization': apiKey
213 + };
214 +
215 + final url = Uri.https(_baseUrl, '$_getTransactionPath/$id');
216 + final response = await http.get(url, headers: headers);
217 +
218 + if (response.statusCode != 200) {
219 + throw Exception('LetsExchange fetch trade failed: ${response.body}');
220 + }
221 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
222 + final from = responseJSON['coin_from'] as String;
223 + final to = responseJSON['coin_to'] as String;
224 + final payoutAddress = responseJSON['withdrawal'] as String;
225 + final depositAddress = responseJSON['deposit'] as String;
226 + final refundAddress = responseJSON['return'] as String;
227 + final depositAmount = responseJSON['deposit_amount'] as String;
228 + final receiveAmount = responseJSON['withdrawal_amount'] as String;
229 + final status = responseJSON['status'] as String;
230 + final createdAtString = responseJSON['created_at'] as String;
231 + final expiredAtTimestamp = responseJSON['expired_at'] as int;
232 +
233 + final createdAt = DateTime.parse(createdAtString);
234 + final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000);
235 +
236 + return Trade(
237 + id: id,
238 + from: CryptoCurrency.fromString(from),
239 + to: CryptoCurrency.fromString(to),
240 + provider: description,
241 + inputAddress: depositAddress,
242 + payoutAddress: payoutAddress,
243 + refundAddress: refundAddress,
244 + amount: depositAmount,
245 + receiveAmount: receiveAmount,
246 + state: TradeState.deserialize(raw: status),
247 + createdAt: createdAt,
248 + expiredAt: expiredAt,
249 + isRefund: status == 'refund',
250 + );
251 + }
252 +
253 + Future<Map<String, dynamic>> _getInfo(Map<String, String> params, bool isFixedRateMode) async {
254 + final headers = {
255 + 'Content-Type': 'application/json',
256 + 'Accept': 'application/json',
257 + 'Authorization': apiKey
258 + };
259 +
260 + try {
261 + final uri = Uri.https(_baseUrl, isFixedRateMode ? _infoRevertPath : _infoPath, params);
262 + final response = await http.post(uri, headers: headers);
263 + if (response.statusCode != 200) {
264 + throw Exception('LetsExchange fetch info failed: ${response.body}');
265 + }
266 + return json.decode(response.body) as Map<String, dynamic>;
267 + } catch (e) {
268 + throw Exception('LetsExchange failed to fetch info ${e.toString()}');
269 + }
270 + }
271 +
272 + String? _getNetworkType(CryptoCurrency currency) {
273 + if (currency.tag != null && currency.tag!.isNotEmpty) {
274 + switch (currency.tag!) {
275 + case 'TRX':
276 + return 'TRC20';
277 + case 'ETH':
278 + return 'ERC20';
279 + case 'BSC':
280 + return 'BEP20';
281 + case 'POLY':
282 + return 'MATIC';
283 + default:
284 + return currency.tag!;
285 + }
286 + }
287 + return currency.title;
288 + }
289 +
290 + String _normalizeBchAddress(String address) =>
291 + address.startsWith('bitcoincash:') ? address.substring(12) : address;
292 +}
lib/exchange/provider/stealth_ex_exchange_provider.dart
+1 -1
@@ -69,7 +69,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
69 throw Exception('StealthEx fetch limits failed: ${response.body}');
70 }
71 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
72 - final min = responseJSON['min_amount'] as double?;
72 + final min = toDouble(responseJSON['min_amount']);
73 final max = responseJSON['max_amount'] as double?;
74 return Limits(min: min, max: max);
75 } catch (e) {
lib/exchange/trade_state.dart
+2
@@ -106,6 +106,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
106 case 'waitingAuthorization':
107 return waitingAuthorization;
108 case 'failed':
109 + case 'error':
110 return failed;
111 case 'completed':
112 return completed;
@@ -125,6 +126,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
126 case 'exchanging':
127 return exchanging;
128 case 'sending':
129 + case 'sending_confirmation':
130 return sending;
131 case 'success':
132 case 'done':
lib/src/screens/dashboard/widgets/trade_row.dart
+3 -1
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
2 +import 'package:cake_wallet/utils/image_utill.dart';
3 import 'package:flutter/material.dart';
4 import 'package:cw_core/crypto_currency.dart';
5 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
@@ -36,7 +37,8 @@ class TradeRow extends StatelessWidget {
37 children: [
38 ClipRRect(
39 borderRadius: BorderRadius.circular(50),
39 - child: Image.asset(provider.image, width: 36, height: 36)),
40 + child: ImageUtil.getImageFromPath(
41 + imagePath: provider.image, height: 36, width: 36)),
42 SizedBox(width: 12),
43 Expanded(
44 child: Column(
lib/src/screens/exchange_trade/exchange_confirm_page.dart
+3 -1
@@ -2,6 +2,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
2 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
3 import 'package:cake_wallet/store/dashboard/trades_store.dart';
4 import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
5 +import 'package:cake_wallet/utils/image_utill.dart';
6 import 'package:cake_wallet/utils/show_bar.dart';
7 import 'package:flutter/material.dart';
8 import 'package:flutter/cupertino.dart';
@@ -101,7 +102,8 @@ class ExchangeConfirmPage extends BasePage {
102 mainAxisAlignment: MainAxisAlignment.center,
103 children: [
104 (trade.provider.image?.isNotEmpty ?? false)
104 - ? Image.asset(trade.provider.image, height: 50)
105 + ? ImageUtil.getImageFromPath(
106 + imagePath: trade.provider.image, width: 50)
107 : const SizedBox(),
108 if (!trade.provider.horizontalLogo)
109 Padding(
lib/store/dashboard/trade_filter_store.dart
+11
@@ -17,6 +17,7 @@ abstract class TradeFilterStoreBase with Store {
17 displayTrocador = true,
18 displayExolix = true,
19 displayThorChain = true,
20 + displayLetsExchange = true,
21 displayStealthEx = true;
22
23 @observable
@@ -43,6 +44,9 @@ abstract class TradeFilterStoreBase with Store {
44 @observable
45 bool displayThorChain;
46
47 + @observable
48 + bool displayLetsExchange;
49 +
50 @observable
51 bool displayStealthEx;
52
@@ -54,6 +58,7 @@ abstract class TradeFilterStoreBase with Store {
58 displayTrocador &&
59 displayExolix &&
60 displayThorChain &&
61 + displayLetsExchange &&
62 displayStealthEx;
63
64 @action
@@ -83,6 +88,8 @@ abstract class TradeFilterStoreBase with Store {
88 case ExchangeProviderDescription.thorChain:
89 displayThorChain = !displayThorChain;
90 break;
91 + case ExchangeProviderDescription.letsExchange:
92 + displayLetsExchange = !displayLetsExchange;
93 case ExchangeProviderDescription.stealthEx:
94 displayStealthEx = !displayStealthEx;
95 break;
@@ -96,6 +103,7 @@ abstract class TradeFilterStoreBase with Store {
103 displayTrocador = false;
104 displayExolix = false;
105 displayThorChain = false;
106 + displayLetsExchange = false;
107 displayStealthEx = false;
108 } else {
109 displayChangeNow = true;
@@ -106,6 +114,7 @@ abstract class TradeFilterStoreBase with Store {
114 displayTrocador = true;
115 displayExolix = true;
116 displayThorChain = true;
117 + displayLetsExchange = true;
118 displayStealthEx = true;
119 }
120 break;
@@ -134,6 +143,8 @@ abstract class TradeFilterStoreBase with Store {
143 (displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) ||
144 (displayThorChain &&
145 item.trade.provider == ExchangeProviderDescription.thorChain) ||
146 + (displayLetsExchange &&
147 + item.trade.provider == ExchangeProviderDescription.letsExchange) ||
148 (displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx))
149 .toList()
150 : _trades;
lib/utils/image_utill.dart new
+60
@@ -0,0 +1,60 @@
1 +import 'package:flutter/material.dart';
2 +import 'package:flutter_svg/svg.dart';
3 +
4 +class ImageUtil {
5 + static Widget getImageFromPath({required String imagePath, double? height, double? width}) {
6 + final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
7 + final bool isSvg = imagePath.endsWith('.svg');
8 + final double _height = height ?? 35;
9 + final double _width = width ?? 35;
10 +
11 + if (isNetworkImage) {
12 + return isSvg
13 + ? SvgPicture.network(
14 + imagePath,
15 + height: _height,
16 + width: _width,
17 + placeholderBuilder: (BuildContext context) => Container(
18 + height: _height,
19 + width: _width,
20 + child: Center(
21 + child: CircularProgressIndicator(),
22 + ),
23 + ),
24 + )
25 + : Image.network(
26 + imagePath,
27 + height: _height,
28 + width: _width,
29 + loadingBuilder:
30 + (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
31 + if (loadingProgress == null) {
32 + return child;
33 + }
34 + return Container(
35 + height: _height,
36 + width: _width,
37 + child: Center(
38 + child: CircularProgressIndicator(
39 + value: loadingProgress.expectedTotalBytes != null
40 + ? loadingProgress.cumulativeBytesLoaded /
41 + loadingProgress.expectedTotalBytes!
42 + : null,
43 + ),
44 + ),
45 + );
46 + },
47 + errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
48 + return Container(
49 + height: _height,
50 + width: _width,
51 + );
52 + },
53 + );
54 + } else {
55 + return isSvg
56 + ? SvgPicture.asset(imagePath, height: _height, width: _width)
57 + : Image.asset(imagePath, height: _height, width: _width);
58 + }
59 + }
60 +}
lib/view_model/dashboard/dashboard_view_model.dart
+5
@@ -129,6 +129,11 @@ abstract class DashboardViewModelBase with Store {
129 caption: ExchangeProviderDescription.thorChain.title,
130 onChanged: () =>
131 tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)),
132 + FilterItem(
133 + value: () => tradeFilterStore.displayLetsExchange,
134 + caption: ExchangeProviderDescription.letsExchange.title,
135 + onChanged: () =>
136 + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.letsExchange)),
137 FilterItem(
138 value: () => tradeFilterStore.displayStealthEx,
139 caption: ExchangeProviderDescription.stealthEx.title,
lib/view_model/exchange/exchange_view_model.dart
+2
@@ -4,6 +4,7 @@ import 'dart:convert';
4
5 import 'package:bitcoin_base/bitcoin_base.dart';
6 import 'package:cake_wallet/core/create_trade_result.dart';
7 +import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
8 import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
9 import 'package:cw_core/crypto_currency.dart';
10 import 'package:cw_core/sync_status.dart';
@@ -167,6 +168,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
168 ThorChainExchangeProvider(tradesStore: trades),
169 if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
170 QuantexExchangeProvider(),
171 + LetsExchangeExchangeProvider(),
172 StealthExExchangeProvider(),
173 TrocadorExchangeProvider(
174 useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
lib/view_model/trade_details_view_model.dart
+5
@@ -4,6 +4,7 @@ import 'package:cake_wallet/exchange/exchange_provider_description.dart';
4 import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
5 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
6 import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
7 +import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
8 import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart';
9 import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
10 import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
@@ -60,6 +61,8 @@ abstract class TradeDetailsViewModelBase with Store {
61 break;
62 case ExchangeProviderDescription.quantex:
63 _provider = QuantexExchangeProvider();
64 + case ExchangeProviderDescription.letsExchange:
65 + _provider = LetsExchangeExchangeProvider();
66 break;
67 case ExchangeProviderDescription.stealthEx:
68 _provider = StealthExExchangeProvider();
@@ -90,6 +93,8 @@ abstract class TradeDetailsViewModelBase with Store {
93 return 'https://track.ninerealms.com/${trade.id}';
94 case ExchangeProviderDescription.quantex:
95 return 'https://myquantex.com/send/${trade.id}';
96 + case ExchangeProviderDescription.letsExchange:
97 + return 'https://letsexchange.io/?transactionId=${trade.id}';
98 case ExchangeProviderDescription.stealthEx:
99 return 'https://stealthex.io/exchange/?id=${trade.id}';
100 }
tool/utils/secret_key.dart
+2
@@ -43,6 +43,8 @@ class SecretKey {
43 SecretKey('cakePayApiKey', () => ''),
44 SecretKey('CSRFToken', () => ''),
45 SecretKey('authorization', () => ''),
46 + SecretKey('letsExchangeBearerToken', () => ''),
47 + SecretKey('letsExchangeAffiliateId', () => ''),
48 SecretKey('stealthExBearerToken', () => ''),
49 SecretKey('stealthExAdditionalFeePercent', () => ''),
50 ];