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
];