Exolix integration (#1080)

* Add Exolix exchange integration * update tx payload * remove import * Improve mapping * Additional fixes * fix apiBaseUrl * Update trade_details_view_model.dart * Update exolix_exchange_provider.dart * Fix status URL * Fix fetch rates API error handling update limits API to use a valid amount and validate on success status code --------- Co-authored-by: Justin Ehrenhofer <justin.ehrenhofer@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Procyon Lotor committed Sep 28, 2023 at 19:20 UTC 9eb6867ab98dbb3a5fec64e0c940c716cf1fd43c
14 files changed +404 -5
.github/workflows/pr_test_build.yml
+1
@@ -128,6 +128,7 @@ jobs:
128 echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart
129 echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart
130 echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart
131 + echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart
132 echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart
133 echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
134
assets/images/exolix.png
Binary files /dev/null and b/assets/images/exolix.png differ
lib/exchange/exchange_provider_description.dart
+6 -1
@@ -24,7 +24,10 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
24 static const trocador =
25 ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png');
26
27 - static const all = ExchangeProviderDescription(title: 'All trades', raw: 6, image: '');
27 + static const exolix =
28 + ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png');
29 +
30 + static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: '');
31
32 static ExchangeProviderDescription deserialize({required int raw}) {
33 switch (raw) {
@@ -41,6 +44,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
44 case 5:
45 return trocador;
46 case 6:
47 + return exolix;
48 + case 7:
49 return all;
50 default:
51 throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
lib/exchange/exolix/exolix_exchange_provider.dart new
+294
@@ -0,0 +1,294 @@
1 +import 'dart:convert';
2 +import 'package:cake_wallet/exchange/trade_not_found_exeption.dart';
3 +import 'package:http/http.dart';
4 +import 'package:cake_wallet/.secrets.g.dart' as secrets;
5 +import 'package:cw_core/crypto_currency.dart';
6 +import 'package:cake_wallet/exchange/exchange_pair.dart';
7 +import 'package:cake_wallet/exchange/exchange_provider.dart';
8 +import 'package:cake_wallet/exchange/limits.dart';
9 +import 'package:cake_wallet/exchange/trade.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/exolix/exolix_request.dart';
13 +import 'package:cake_wallet/exchange/exchange_provider_description.dart';
14 +
15 +class ExolixExchangeProvider extends ExchangeProvider {
16 + ExolixExchangeProvider() : super(pairList: _supportedPairs());
17 +
18 + static final apiKey = secrets.exolixApiKey;
19 + static const apiBaseUrl = 'exolix.com';
20 + static const transactionsPath = '/api/v2/transactions';
21 + static const ratePath = '/api/v2/rate';
22 +
23 + static const List<CryptoCurrency> _notSupported = [
24 + CryptoCurrency.usdt,
25 + CryptoCurrency.xhv,
26 + CryptoCurrency.btt,
27 + CryptoCurrency.firo,
28 + CryptoCurrency.zaddr,
29 + CryptoCurrency.xvg,
30 + CryptoCurrency.kmd,
31 + CryptoCurrency.paxg,
32 + CryptoCurrency.rune,
33 + CryptoCurrency.scrt,
34 + CryptoCurrency.btcln,
35 + CryptoCurrency.cro,
36 + CryptoCurrency.ftm,
37 + CryptoCurrency.frax,
38 + CryptoCurrency.gusd,
39 + CryptoCurrency.gtc,
40 + CryptoCurrency.weth,
41 + ];
42 +
43 + static List<ExchangePair> _supportedPairs() {
44 + final supportedCurrencies =
45 + CryptoCurrency.all.where((element) => !_notSupported.contains(element)).toList();
46 +
47 + return supportedCurrencies
48 + .map((i) => supportedCurrencies.map((k) => ExchangePair(from: i, to: k, reverse: true)))
49 + .expand((i) => i)
50 + .toList();
51 + }
52 +
53 + @override
54 + String get title => 'Exolix';
55 +
56 + @override
57 + bool get isAvailable => true;
58 +
59 + @override
60 + bool get isEnabled => true;
61 +
62 + @override
63 + bool get supportsFixedRate => true;
64 +
65 + @override
66 + ExchangeProviderDescription get description => ExchangeProviderDescription.exolix;
67 +
68 + @override
69 + Future<bool> checkIsAvailable() async => true;
70 +
71 + static String getRateType(bool isFixedRate) => isFixedRate ? 'fixed' : 'float';
72 +
73 + @override
74 + Future<Limits> fetchLimits(
75 + {required CryptoCurrency from,
76 + required CryptoCurrency to,
77 + required bool isFixedRateMode}) async {
78 + final params = <String, String>{
79 + 'rateType': getRateType(isFixedRateMode),
80 + 'amount': '1',
81 + };
82 + if (isFixedRateMode) {
83 + params['coinFrom'] = _normalizeCurrency(to);
84 + params['coinTo'] = _normalizeCurrency(from);
85 + params['networkFrom'] = _networkFor(to);
86 + params['networkTo'] = _networkFor(from);
87 + } else {
88 + params['coinFrom'] = _normalizeCurrency(from);
89 + params['coinTo'] = _normalizeCurrency(to);
90 + params['networkFrom'] = _networkFor(from);
91 + params['networkTo'] = _networkFor(to);
92 + }
93 + final uri = Uri.https(apiBaseUrl, ratePath, params);
94 + final response = await get(uri);
95 +
96 + if (response.statusCode != 200) {
97 + throw Exception('Unexpected http status: ${response.statusCode}');
98 + }
99 +
100 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
101 + return Limits(min: responseJSON['minAmount'] as double?);
102 + }
103 +
104 + @override
105 + Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
106 + final _request = request as ExolixRequest;
107 +
108 + final headers = {'Content-Type': 'application/json'};
109 + final body = <String, dynamic>{
110 + 'coinFrom': _normalizeCurrency(_request.from),
111 + 'coinTo': _normalizeCurrency(_request.to),
112 + 'networkFrom': _networkFor(_request.from),
113 + 'networkTo': _networkFor(_request.to),
114 + 'withdrawalAddress': _request.address,
115 + 'refundAddress': _request.refundAddress,
116 + 'rateType': getRateType(isFixedRateMode),
117 + 'apiToken': apiKey,
118 + };
119 +
120 + if (isFixedRateMode) {
121 + body['withdrawalAmount'] = _request.toAmount;
122 + } else {
123 + body['amount'] = _request.fromAmount;
124 + }
125 +
126 + final uri = Uri.https(apiBaseUrl, transactionsPath);
127 + final response = await post(uri, headers: headers, body: json.encode(body));
128 +
129 + if (response.statusCode == 400) {
130 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
131 + final errors = responseJSON['errors'] as Map<String, String>;
132 + final errorMessage = errors.values.join(', ');
133 + throw Exception(errorMessage);
134 + }
135 +
136 + if (response.statusCode != 200 && response.statusCode != 201) {
137 + throw Exception('Unexpected http status: ${response.statusCode}');
138 + }
139 +
140 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
141 + final id = responseJSON['id'] as String;
142 + final inputAddress = responseJSON['depositAddress'] as String;
143 + final refundAddress = responseJSON['refundAddress'] as String?;
144 + final extraId = responseJSON['depositExtraId'] as String?;
145 + final payoutAddress = responseJSON['withdrawalAddress'] as String;
146 + final amount = responseJSON['amount'].toString();
147 +
148 + return Trade(
149 + id: id,
150 + from: _request.from,
151 + to: _request.to,
152 + provider: description,
153 + inputAddress: inputAddress,
154 + refundAddress: refundAddress,
155 + extraId: extraId,
156 + createdAt: DateTime.now(),
157 + amount: amount,
158 + state: TradeState.created,
159 + payoutAddress: payoutAddress);
160 + }
161 +
162 + @override
163 + Future<Trade> findTradeById({required String id}) async {
164 + final findTradeByIdPath = transactionsPath + '/$id';
165 + final uri = Uri.https(apiBaseUrl, findTradeByIdPath);
166 + final response = await get(uri);
167 +
168 + if (response.statusCode == 404) {
169 + throw TradeNotFoundException(id, provider: description);
170 + }
171 +
172 + if (response.statusCode == 400) {
173 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
174 + final errors = responseJSON['errors'] as Map<String, String>;
175 + final errorMessage = errors.values.join(', ');
176 +
177 + throw TradeNotFoundException(id, provider: description, description: errorMessage);
178 + }
179 +
180 + if (response.statusCode != 200) {
181 + throw Exception('Unexpected http status: ${response.statusCode}');
182 + }
183 +
184 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
185 + final coinFrom = responseJSON['coinFrom']['coinCode'] as String;
186 + final from = CryptoCurrency.fromString(coinFrom);
187 + final coinTo = responseJSON['coinTo']['coinCode'] as String;
188 + final to = CryptoCurrency.fromString(coinTo);
189 + final inputAddress = responseJSON['depositAddress'] as String;
190 + final amount = responseJSON['amount'].toString();
191 + final status = responseJSON['status'] as String;
192 + final state = TradeState.deserialize(raw: _prepareStatus(status));
193 + final extraId = responseJSON['depositExtraId'] as String?;
194 + final outputTransaction = responseJSON['hashOut']['hash'] as String?;
195 + final payoutAddress = responseJSON['withdrawalAddress'] as String;
196 +
197 + return Trade(
198 + id: id,
199 + from: from,
200 + to: to,
201 + provider: description,
202 + inputAddress: inputAddress,
203 + amount: amount,
204 + state: state,
205 + extraId: extraId,
206 + outputTransaction: outputTransaction,
207 + payoutAddress: payoutAddress);
208 + }
209 +
210 + @override
211 + Future<double> fetchRate(
212 + {required CryptoCurrency from,
213 + required CryptoCurrency to,
214 + required double amount,
215 + required bool isFixedRateMode,
216 + required bool isReceiveAmount}) async {
217 + try {
218 + if (amount == 0) {
219 + return 0.0;
220 + }
221 +
222 + final params = <String, String>{
223 + 'coinFrom': _normalizeCurrency(from),
224 + 'coinTo': _normalizeCurrency(to),
225 + 'networkFrom': _networkFor(from),
226 + 'networkTo': _networkFor(to),
227 + 'rateType': getRateType(isFixedRateMode),
228 + };
229 +
230 + if (isReceiveAmount) {
231 + params['withdrawalAmount'] = amount.toString();
232 + } else {
233 + params['amount'] = amount.toString();
234 + }
235 +
236 + final uri = Uri.https(apiBaseUrl, ratePath, params);
237 + final response = await get(uri);
238 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
239 +
240 + if (response.statusCode != 200) {
241 + final message = responseJSON['message'] as String?;
242 + throw Exception(message);
243 + }
244 +
245 + final rate = responseJSON['rate'] as double;
246 +
247 + return rate;
248 + } catch (e) {
249 + print(e.toString());
250 + return 0.0;
251 + }
252 + }
253 +
254 + String _prepareStatus(String status) {
255 + switch (status) {
256 + case 'deleted':
257 + case 'error':
258 + return 'overdue';
259 + default:
260 + return status;
261 + }
262 + }
263 +
264 + String _networkFor(CryptoCurrency currency) {
265 + switch (currency) {
266 + case CryptoCurrency.arb:
267 + return 'ARBITRUM';
268 + default:
269 + return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title;
270 + }
271 + }
272 +
273 + String _normalizeCurrency(CryptoCurrency currency) {
274 + switch (currency) {
275 + case CryptoCurrency.nano:
276 + return 'XNO';
277 + case CryptoCurrency.bttc:
278 + return 'BTT';
279 + case CryptoCurrency.zec:
280 + return 'ZEC';
281 + default:
282 + return currency.title;
283 + }
284 + }
285 +
286 + String _normalizeTag(String tag) {
287 + switch (tag) {
288 + case 'POLY':
289 + return 'Polygon';
290 + default:
291 + return tag;
292 + }
293 + }
294 +}
lib/exchange/exolix/exolix_request.dart new
+20
@@ -0,0 +1,20 @@
1 +import 'package:flutter/foundation.dart';
2 +import 'package:cw_core/crypto_currency.dart';
3 +import 'package:cake_wallet/exchange/trade_request.dart';
4 +
5 +class ExolixRequest extends TradeRequest {
6 + ExolixRequest(
7 + {required this.from,
8 + required this.to,
9 + required this.address,
10 + required this.fromAmount,
11 + required this.toAmount,
12 + required this.refundAddress});
13 +
14 + CryptoCurrency from;
15 + CryptoCurrency to;
16 + String address;
17 + String fromAmount;
18 + String toAmount;
19 + String refundAddress;
20 +}
lib/exchange/trade_state.dart
+27
@@ -35,6 +35,15 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
35 static const completed = TradeState(raw: 'completed', title: 'Completed');
36 static const settling = TradeState(raw: 'settling', title: 'Settlement in progress');
37 static const settled = TradeState(raw: 'settled', title: 'Settlement completed');
38 + static const wait = TradeState(raw: 'wait', title: 'Waiting');
39 + static const overdue = TradeState(raw: 'overdue', title: 'Overdue');
40 + static const refund = TradeState(raw: 'refund', title: 'Refund');
41 + static const refunded = TradeState(raw: 'refunded', title: 'Refunded');
42 + static const confirmation = TradeState(raw: 'confirmation', title: 'Confirmation');
43 + static const confirmed = TradeState(raw: 'confirmed', title: 'Confirmed');
44 + static const exchanging = TradeState(raw: 'exchanging', title: 'Exchanging');
45 + static const sending = TradeState(raw: 'sending', title: 'Sending');
46 + static const success = TradeState(raw: 'success', title: 'Success');
47 static TradeState deserialize({required String raw}) {
48 switch (raw) {
49 case 'pending':
@@ -77,6 +86,24 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
86 return failed;
87 case 'completed':
88 return completed;
89 + case 'wait':
90 + return wait;
91 + case 'overdue':
92 + return overdue;
93 + case 'refund':
94 + return refund;
95 + case 'refunded':
96 + return refunded;
97 + case 'confirmation':
98 + return confirmation;
99 + case 'confirmed':
100 + return confirmed;
101 + case 'exchanging':
102 + return exchanging;
103 + case 'sending':
104 + return sending;
105 + case 'success':
106 + return success;
107 default:
108 throw Exception('Unexpected token: $raw in TradeState deserialize');
109 }
lib/src/screens/dashboard/widgets/trade_row.dart
+3
@@ -94,6 +94,9 @@ class TradeRow extends StatelessWidget {
94 borderRadius: BorderRadius.circular(50),
95 child: Image.asset('assets/images/trocador.png', width: 36, height: 36));
96 break;
97 + case ExchangeProviderDescription.exolix:
98 + image = Image.asset('assets/images/exolix.png', width: 36, height: 36);
99 + break;
100 default:
101 image = null;
102 }
lib/store/dashboard/trade_filter_store.dart
+14 -4
@@ -13,7 +13,8 @@ abstract class TradeFilterStoreBase with Store {
13 displaySideShift = true,
14 displayMorphToken = true,
15 displaySimpleSwap = true,
16 - displayTrocador = true;
16 + displayTrocador = true,
17 + displayExolix = true;
18
19 @observable
20 bool displayXMRTO;
@@ -33,8 +34,11 @@ abstract class TradeFilterStoreBase with Store {
34 @observable
35 bool displayTrocador;
36
37 + @observable
38 + bool displayExolix;
39 +
40 @computed
37 - bool get displayAllTrades => displayChangeNow && displaySideShift && displaySimpleSwap && displayTrocador;
41 + bool get displayAllTrades => displayChangeNow && displaySideShift && displaySimpleSwap && displayTrocador && displayExolix;
42
43 @action
44 void toggleDisplayExchange(ExchangeProviderDescription provider) {
@@ -56,7 +60,10 @@ abstract class TradeFilterStoreBase with Store {
60 break;
61 case ExchangeProviderDescription.trocador:
62 displayTrocador = !displayTrocador;
59 - break;
63 + break;
64 + case ExchangeProviderDescription.exolix:
65 + displayExolix = !displayExolix;
66 + break;
67 case ExchangeProviderDescription.all:
68 if (displayAllTrades) {
69 displayChangeNow = false;
@@ -65,6 +72,7 @@ abstract class TradeFilterStoreBase with Store {
72 displayMorphToken = false;
73 displaySimpleSwap = false;
74 displayTrocador = false;
75 + displayExolix = false;
76 } else {
77 displayChangeNow = true;
78 displaySideShift = true;
@@ -72,6 +80,7 @@ abstract class TradeFilterStoreBase with Store {
80 displayMorphToken = true;
81 displaySimpleSwap = true;
82 displayTrocador = true;
83 + displayExolix = true;
84 }
85 break;
86 }
@@ -98,7 +107,8 @@ abstract class TradeFilterStoreBase with Store {
107 ||(displaySimpleSwap &&
108 item.trade.provider ==
109 ExchangeProviderDescription.simpleSwap)
101 - ||(displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador))
110 + ||(displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador)
111 + ||(displayExolix && item.trade.provider == ExchangeProviderDescription.exolix))
112 .toList()
113 : _trades;
114 }
lib/view_model/dashboard/dashboard_view_model.dart
+5
@@ -99,6 +99,11 @@ abstract class DashboardViewModelBase with Store {
99 caption: ExchangeProviderDescription.trocador.title,
100 onChanged: () => tradeFilterStore
101 .toggleDisplayExchange(ExchangeProviderDescription.trocador)),
102 + FilterItem(
103 + value: () => tradeFilterStore.displayExolix,
104 + caption: ExchangeProviderDescription.exolix.title,
105 + onChanged: () => tradeFilterStore
106 + .toggleDisplayExchange(ExchangeProviderDescription.exolix)),
107 ]
108 },
109 subname = '',
lib/view_model/exchange/exchange_trade_view_model.dart
+4
@@ -1,4 +1,5 @@
1 import 'dart:async';
2 +import 'package:cake_wallet/exchange/exolix/exolix_exchange_provider.dart';
3 import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
4 import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart';
5 import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart';
@@ -53,6 +54,9 @@ abstract class ExchangeTradeViewModelBase with Store {
54 case ExchangeProviderDescription.trocador:
55 _provider = TrocadorExchangeProvider();
56 break;
57 + case ExchangeProviderDescription.exolix:
58 + _provider = ExolixExchangeProvider();
59 + break;
60 }
61
62 _updateItems();
lib/view_model/exchange/exchange_view_model.dart
+14
@@ -6,6 +6,8 @@ import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
6 import 'package:cake_wallet/entities/exchange_api_mode.dart';
7 import 'package:cake_wallet/entities/preferences_key.dart';
8 import 'package:cake_wallet/entities/wallet_contact.dart';
9 +import 'package:cake_wallet/exchange/exolix/exolix_exchange_provider.dart';
10 +import 'package:cake_wallet/exchange/exolix/exolix_request.dart';
11 import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
12 import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart';
13 import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart';
@@ -151,6 +153,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
153 SideShiftExchangeProvider(),
154 SimpleSwapExchangeProvider(),
155 TrocadorExchangeProvider(useTorOnly: _useTorOnly),
156 + ExolixExchangeProvider(),
157 ];
158
159 @observable
@@ -547,6 +550,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
550 amount = isFixedRateMode ? receiveAmount : depositAmount;
551 }
552
553 + if (provider is ExolixExchangeProvider) {
554 + request = ExolixRequest(
555 + from: depositCurrency,
556 + to: receiveCurrency,
557 + fromAmount: depositAmount.replaceAll(',', '.'),
558 + toAmount: receiveAmount.replaceAll(',', '.'),
559 + refundAddress: depositAddress,
560 + address: receiveAddress);
561 + amount = isFixedRateMode ? receiveAmount : depositAmount;
562 + }
563 +
564 amount = amount.replaceAll(',', '.');
565
566 if (limitsState is LimitsLoadedSuccessfully) {
lib/view_model/support_view_model.dart
+5
@@ -53,6 +53,11 @@ abstract class SupportViewModelBase with Store {
53 icon: 'assets/images/simpleSwap.png',
54 linkTitle: 'support@simpleswap.io',
55 link: 'mailto:support@simpleswap.io'),
56 + LinkListItem(
57 + title: 'Exolix',
58 + icon: 'assets/images/exolix.png',
59 + linkTitle: 'support@exolix.com',
60 + link: 'mailto:support@exolix.com'),
61 if (!isMoneroOnly) ... [
62 LinkListItem(
63 title: 'Wyre',
lib/view_model/trade_details_view_model.dart
+10
@@ -2,6 +2,7 @@ import 'dart:async';
2 import 'package:cake_wallet/exchange/changenow/changenow_exchange_provider.dart';
3 import 'package:cake_wallet/exchange/exchange_provider.dart';
4 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
5 +import 'package:cake_wallet/exchange/exolix/exolix_exchange_provider.dart';
6 import 'package:cake_wallet/exchange/morphtoken/morphtoken_exchange_provider.dart';
7 import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
8 import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart';
@@ -54,6 +55,9 @@ abstract class TradeDetailsViewModelBase with Store {
55 case ExchangeProviderDescription.trocador:
56 _provider = TrocadorExchangeProvider();
57 break;
58 + case ExchangeProviderDescription.exolix:
59 + _provider = ExolixExchangeProvider();
60 + break;
61 }
62
63 _updateItems();
@@ -157,6 +161,12 @@ abstract class TradeDetailsViewModelBase with Store {
161 items.add(StandartListItem(
162 title: '${trade.providerName} ${S.current.password}', value: trade.password ?? ''));
163 }
164 +
165 + if (trade.provider == ExchangeProviderDescription.exolix) {
166 + final buildURL = 'https://exolix.com/transaction/${trade.id.toString()}';
167 + items.add(
168 + TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => _launchUrl(buildURL)));
169 + }
170 }
171
172 void _launchUrl(String url) {
tool/utils/secret_key.dart
+1
@@ -32,6 +32,7 @@ class SecretKey {
32 SecretKey('fiatApiKey', () => ''),
33 SecretKey('payfuraApiKey', () => ''),
34 SecretKey('chatwootWebsiteToken', () => ''),
35 + SecretKey('exolixApiKey', () => ''),
36 SecretKey('robinhoodApplicationId', () => ''),
37 SecretKey('robinhoodCIdApiSecret', () => ''),
38 ];