Cw 807 add extra fields for xrp and xlm exchanges (#1869)

* show extra id for xrp/xlm Stealth * extra id for xrp/xlm LetsExchange * extra id xrp/xml for Sideshift and Trocador * fix Exolix min amount issue * fix extra id ui

Serhii committed Dec 13, 2024 at 01:24 UTC 489a409beace6fac7368645f2dc02d5923a05b23
11 files changed +102 -27
lib/exchange/provider/exolix_exchange_provider.dart
+39 -10
@@ -60,15 +60,17 @@ class ExolixExchangeProvider extends ExchangeProvider {
60 Future<bool> checkIsAvailable() async => true;
61
62 @override
63 - Future<Limits> fetchLimits(
64 - {required CryptoCurrency from,
65 - required CryptoCurrency to,
66 - required bool isFixedRateMode}) async {
63 + Future<Limits> fetchLimits({
64 + required CryptoCurrency from,
65 + required CryptoCurrency to,
66 + required bool isFixedRateMode,
67 + }) async {
68 final params = <String, String>{
69 'rateType': _getRateType(isFixedRateMode),
70 'amount': '1',
71 'apiToken': apiKey,
72 };
73 +
74 if (isFixedRateMode) {
75 params['coinFrom'] = _normalizeCurrency(to);
76 params['coinTo'] = _normalizeCurrency(from);
@@ -80,14 +82,30 @@ class ExolixExchangeProvider extends ExchangeProvider {
82 params['networkFrom'] = _networkFor(from);
83 params['networkTo'] = _networkFor(to);
84 }
83 - final uri = Uri.https(apiBaseUrl, ratePath, params);
84 - final response = await get(uri);
85
86 - if (response.statusCode != 200)
87 - throw Exception('Unexpected http status: ${response.statusCode}');
86 + // Maximum of 2 attempts to fetch limits
87 + for (int i = 0; i < 2; i++) {
88 + final uri = Uri.https(apiBaseUrl, ratePath, params);
89 + final response = await get(uri);
90
89 - final responseJSON = json.decode(response.body) as Map<String, dynamic>;
90 - return Limits(min: responseJSON['minAmount'] as double?);
91 + if (response.statusCode == 200) {
92 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
93 + final minAmount = responseJSON['minAmount'];
94 + final maxAmount = responseJSON['maxAmount'];
95 + return Limits(min: _toDouble(minAmount), max: _toDouble(maxAmount));
96 + } else if (response.statusCode == 422) {
97 + final errorResponse = json.decode(response.body) as Map<String, dynamic>;
98 + if (errorResponse.containsKey('minAmount')) {
99 + params['amount'] = errorResponse['minAmount'].toString();
100 + continue;
101 + }
102 + throw Exception('Error 422: ${errorResponse['message'] ?? 'Unknown error'}');
103 + } else {
104 + throw Exception('Unexpected HTTP status: ${response.statusCode}');
105 + }
106 + }
107 +
108 + throw Exception('Failed to fetch limits after retrying.');
109 }
110
111 @override
@@ -279,4 +297,15 @@ class ExolixExchangeProvider extends ExchangeProvider {
297
298 String _normalizeAddress(String address) =>
299 address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address;
300 +
301 + static double? _toDouble(dynamic value) {
302 + if (value is int) {
303 + return value.toDouble();
304 + } else if (value is double) {
305 + return value;
306 + } else if (value is String) {
307 + return double.tryParse(value);
308 + }
309 + return null;
310 + }
311 }
lib/exchange/provider/letsexchange_exchange_provider.dart
+4
@@ -168,6 +168,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
168 final status = responseJSON['status'] as String;
169 final createdAtString = responseJSON['created_at'] as String;
170 final expiredAtTimestamp = responseJSON['expired_at'] as int;
171 + final extraId = responseJSON['deposit_extra_id'] as String?;
172
173 final createdAt = DateTime.parse(createdAtString);
174 final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000);
@@ -199,6 +200,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
200 state: TradeState.deserialize(raw: status),
201 createdAt: createdAt,
202 expiredAt: expiredAt,
203 + extraId: extraId,
204 );
205 } catch (e) {
206 log(e.toString());
@@ -231,6 +233,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
233 final status = responseJSON['status'] as String;
234 final createdAtString = responseJSON['created_at'] as String;
235 final expiredAtTimestamp = responseJSON['expired_at'] as int;
236 + final extraId = responseJSON['deposit_extra_id'] as String?;
237
238 final createdAt = DateTime.parse(createdAtString);
239 final expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtTimestamp * 1000);
@@ -249,6 +252,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
252 createdAt: createdAt,
253 expiredAt: expiredAt,
254 isRefund: status == 'refund',
255 + extraId: extraId,
256 );
257 }
258
lib/exchange/provider/sideshift_exchange_provider.dart
+5 -1
@@ -203,6 +203,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
203 final inputAddress = responseJSON['depositAddress'] as String;
204 final settleAddress = responseJSON['settleAddress'] as String;
205 final depositAmount = responseJSON['depositAmount'] as String?;
206 + final depositMemo = responseJSON['depositMemo'] as String?;
207
208 return Trade(
209 id: id,
@@ -217,6 +218,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
218 payoutAddress: settleAddress,
219 createdAt: DateTime.now(),
220 isSendAll: isSendAll,
221 + extraId: depositMemo
222 );
223 }
224
@@ -251,6 +253,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
253 final isVariable = (responseJSON['type'] as String) == 'variable';
254 final expiredAtRaw = responseJSON['expiresAt'] as String;
255 final expiredAt = isVariable ? null : DateTime.tryParse(expiredAtRaw)?.toLocal();
256 + final depositMemo = responseJSON['depositMemo'] as String?;
257
258 return Trade(
259 id: id,
@@ -261,7 +264,8 @@ class SideShiftExchangeProvider extends ExchangeProvider {
264 amount: expectedSendAmount ?? '',
265 state: TradeState.deserialize(raw: status ?? 'created'),
266 expiredAt: expiredAt,
264 - payoutAddress: settleAddress);
267 + payoutAddress: settleAddress,
268 + extraId: depositMemo);
269 }
270
271 Future<String> _createQuote(TradeRequest request) async {
lib/exchange/provider/stealth_ex_exchange_provider.dart
+4
@@ -154,6 +154,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
154 final receiveAmount = toDouble(withdrawal['amount']);
155 final status = responseJSON['status'] as String;
156 final createdAtString = responseJSON['created_at'] as String;
157 + final extraId = deposit['extra_id'] as String?;
158
159 final createdAt = DateTime.parse(createdAtString);
160 final expiredAt = validUntil != null
@@ -188,6 +189,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
189 state: TradeState.deserialize(raw: status),
190 createdAt: createdAt,
191 expiredAt: expiredAt,
192 + extraId: extraId,
193 );
194 } catch (e) {
195 log(e.toString());
@@ -220,6 +222,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
222 final status = responseJSON['status'] as String;
223 final createdAtString = responseJSON['created_at'] as String;
224 final createdAt = DateTime.parse(createdAtString);
225 + final extraId = deposit['extra_id'] as String?;
226
227 return Trade(
228 id: respId,
@@ -234,6 +237,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
237 state: TradeState.deserialize(raw: status),
238 createdAt: createdAt,
239 isRefund: status == 'refunded',
240 + extraId: extraId,
241 );
242 }
243
lib/exchange/provider/trocador_exchange_provider.dart
+4 -1
@@ -171,7 +171,8 @@ class TrocadorExchangeProvider extends ExchangeProvider {
171 if (!isFixedRateMode) 'amount_from': request.fromAmount,
172 if (isFixedRateMode) 'amount_to': request.toAmount,
173 'address': request.toAddress,
174 - 'refund': request.refundAddress
174 + 'refund': request.refundAddress,
175 + 'refund_memo' : '0',
176 };
177
178 if (isFixedRateMode) {
@@ -262,6 +263,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
263 final password = responseJSON['password'] as String;
264 final providerId = responseJSON['id_provider'] as String;
265 final providerName = responseJSON['provider'] as String;
266 + final addressProviderMemo = responseJSON['address_provider_memo'] as String?;
267
268 return Trade(
269 id: id,
@@ -277,6 +279,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
279 password: password,
280 providerId: providerId,
281 providerName: providerName,
282 + extraId: addressProviderMemo,
283 );
284 });
285 }
lib/exchange/trade.dart
+2
@@ -143,6 +143,7 @@ class Trade extends HiveObject {
143 isRefund: map['isRefund'] as bool?,
144 isSendAll: map['isSendAll'] as bool?,
145 router: map['router'] as String?,
146 + extraId: map['extra_id'] as String?,
147 );
148 }
149
@@ -162,6 +163,7 @@ class Trade extends HiveObject {
163 'isRefund': isRefund,
164 'isSendAll': isSendAll,
165 'router': router,
166 + 'extra_id': extraId,
167 };
168 }
169
lib/src/screens/trade_details/trade_details_list_card.dart
+14 -2
@@ -8,7 +8,8 @@ class TradeDetailsListCardItem extends StandartListItem {
8 {required this.id,
9 required this.createdAt,
10 required this.pair,
11 - required this.onTap})
11 + required this.onTap,
12 + this.extraId})
13 : super(title: '', value: '');
14
15 factory TradeDetailsListCardItem.tradeDetails(
@@ -16,9 +17,19 @@ class TradeDetailsListCardItem extends StandartListItem {
17 required String createdAt,
18 required CryptoCurrency from,
19 required CryptoCurrency to,
19 - required void Function(BuildContext) onTap}) {
20 + required void Function(BuildContext) onTap,
21 + String? extraId}) {
22 +
23 +
24 + final extraIdTitle = from == CryptoCurrency.xrp
25 + ? S.current.destination_tag
26 + : from == CryptoCurrency.xlm
27 + ? S.current.memo
28 + : S.current.extra_id;
29 +
30 return TradeDetailsListCardItem(
31 id: '${S.current.trade_details_id} ${formatAsText(id)}',
32 + extraId: extraId != null ? '$extraIdTitle $extraId' : null,
33 createdAt: formatAsText(createdAt),
34 pair: '${formatAsText(from)} → ${formatAsText(to)}',
35 onTap: onTap);
@@ -27,6 +38,7 @@ class TradeDetailsListCardItem extends StandartListItem {
38 final String id;
39 final String createdAt;
40 final String pair;
41 + final String? extraId;
42 final void Function(BuildContext) onTap;
43
44 static String formatAsText<T>(T value) => value?.toString() ?? '';
lib/src/screens/trade_details/trade_details_page.dart
+1
@@ -69,6 +69,7 @@ class TradeDetailsPageBodyState extends State<TradeDetailsPageBody> {
69 if (item is TradeDetailsListCardItem)
70 return TradeDetailsStandardListCard(
71 id: item.id,
72 + extraId: item.extraId,
73 create: item.createdAt,
74 pair: item.pair,
75 currentTheme: tradeDetailsViewModel.settingsStore.currentTheme.type,
lib/src/widgets/standard_list_card.dart
+12
@@ -6,12 +6,14 @@ import 'package:cake_wallet/themes/theme_base.dart';
6 class TradeDetailsStandardListCard extends StatelessWidget {
7 TradeDetailsStandardListCard(
8 {required this.id,
9 + this.extraId,
10 required this.create,
11 required this.pair,
12 required this.onTap,
13 required this.currentTheme});
14
15 final String id;
16 + final String? extraId;
17 final String create;
18 final String pair;
19 final ThemeType currentTheme;
@@ -57,6 +59,16 @@ class TradeDetailsStandardListCard extends StatelessWidget {
59 SizedBox(
60 height: 8,
61 ),
62 + if (extraId != null && extraId!.isNotEmpty)
63 + Padding(
64 + padding: const EdgeInsets.only(bottom: 8.0),
65 + child: Text(extraId!,
66 + style: TextStyle(
67 + fontSize: 16,
68 + fontFamily: 'Lato',
69 + fontWeight: FontWeight.w400,
70 + color: textColor)),
71 + ),
72 Text(create,
73 style: TextStyle(
74 fontSize: 12,
lib/view_model/exchange/exchange_trade_view_model.dart
+16 -13
@@ -146,7 +146,7 @@ abstract class ExchangeTradeViewModelBase with Store {
146
147 void _updateItems() {
148 final tagFrom =
149 - tradesStore.trade!.from.tag != null ? '${tradesStore.trade!.from.tag}' + ' ' : '';
149 + tradesStore.trade!.from.tag != null ? '${tradesStore.trade!.from.tag}' + ' ' : '';
150 final tagTo = tradesStore.trade!.to.tag != null ? '${tradesStore.trade!.to.tag}' + ' ' : '';
151 items.clear();
152
@@ -159,16 +159,6 @@ abstract class ExchangeTradeViewModelBase with Store {
159 ),
160 );
161
162 - if (trade.extraId != null) {
163 - final title = trade.from == CryptoCurrency.xrp
164 - ? S.current.destination_tag
165 - : trade.from == CryptoCurrency.xlm
166 - ? S.current.memo
167 - : S.current.extra_id;
168 -
169 - items.add(ExchangeTradeItem(title: title, data: '${trade.extraId}', isCopied: false));
170 - }
171 -
162 items.addAll([
163 ExchangeTradeItem(
164 title: S.current.amount,
@@ -176,7 +166,7 @@ abstract class ExchangeTradeViewModelBase with Store {
166 isCopied: true,
167 ),
168 ExchangeTradeItem(
179 - title: S.current.estimated_receive_amount +':',
169 + title: S.current.estimated_receive_amount + ':',
170 data: '${tradesStore.trade?.receiveAmount} ${trade.to}',
171 isCopied: true,
172 ),
@@ -185,12 +175,25 @@ abstract class ExchangeTradeViewModelBase with Store {
175 data: trade.inputAddress ?? '',
176 isCopied: true,
177 ),
178 + ]);
179 +
180 + if (trade.extraId != null) {
181 + final title = trade.from == CryptoCurrency.xrp
182 + ? S.current.destination_tag
183 + : trade.from == CryptoCurrency.xlm
184 + ? S.current.memo
185 + : S.current.extra_id;
186 +
187 + items.add(ExchangeTradeItem(title: title, data: '${trade.extraId}', isCopied: true));
188 + }
189 +
190 + items.add(
191 ExchangeTradeItem(
192 title: S.current.arrive_in_this_address('${tradesStore.trade!.to}', tagTo) + ':',
193 data: trade.payoutAddress ?? '',
194 isCopied: true,
195 ),
193 - ]);
196 + );
197 }
198
199 static bool _checkIfCanSend(TradesStore tradesStore, WalletBase wallet) {
lib/view_model/trade_details_view_model.dart
+1
@@ -153,6 +153,7 @@ abstract class TradeDetailsViewModelBase with Store {
153
154 items.add(TradeDetailsListCardItem.tradeDetails(
155 id: trade.id,
156 + extraId: trade.extraId,
157 createdAt: trade.createdAt != null ? dateFormat.format(trade.createdAt!) : '',
158 from: trade.from,
159 to: trade.to,