Cw 613 quantex (#1377)
* save progress * [skip ci] * forgot to add [skip ci] * not sure what exactly I changed but it just works now! ¯\_(ツ)_/¯ * status updates * minor cleanup * minor fix (toUppercase needed) * remove unnecessary apikey + keep original raw values * fix track url for quantex * only increment raw values --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Matthew Fosse committed
May 13, 2024 at 19:07 UTC
4947e231e9afcd38267bab82617130067b1e857b
9 files changed
+301
-5
.github/workflows/pr_test_build.yml
+1
@@ -151,6 +151,7 @@ jobs:
151
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
152
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
153
echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
154
+ echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart
155
echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart
156
echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
157
assets/images/quantex.png
Binary files /dev/null and b/assets/images/quantex.png differ
lib/exchange/exchange_provider_description.dart
+8
-5
@@ -22,10 +22,11 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
22
ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png');
23
static const exolix =
24
ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png');
25
- static const thorChain =
26
- ExchangeProviderDescription(title: 'ThorChain' , raw: 8, image: 'assets/images/thorchain.png');
27
-
25
static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: '');
26
+ static const thorChain =
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
31
static ExchangeProviderDescription deserialize({required int raw}) {
32
switch (raw) {
@@ -43,10 +44,12 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
44
return trocador;
45
case 6:
46
return exolix;
46
- case 8:
47
- return thorChain;
47
case 7:
48
return all;
49
+ case 8:
50
+ return thorChain;
51
+ case 9:
52
+ return quantex;
53
default:
54
throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
55
}
lib/exchange/provider/quantex_exchange_provider.dart
new
+252
@@ -0,0 +1,252 @@
1
+import 'dart:convert';
2
+
3
+import 'package:cake_wallet/.secrets.g.dart' as secrets;
4
+import 'package:cake_wallet/exchange/exchange_provider_description.dart';
5
+import 'package:cake_wallet/exchange/limits.dart';
6
+import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
7
+import 'package:cake_wallet/exchange/trade.dart';
8
+import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
9
+import 'package:cake_wallet/exchange/trade_not_found_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';
15
+
16
+class QuantexExchangeProvider extends ExchangeProvider {
17
+ QuantexExchangeProvider() : super(pairList: supportedPairs(_notSupported));
18
+
19
+ static final List<CryptoCurrency> _notSupported = [
20
+ ...(CryptoCurrency.all
21
+ .where((element) => ![
22
+ CryptoCurrency.btc,
23
+ CryptoCurrency.sol,
24
+ CryptoCurrency.eth,
25
+ CryptoCurrency.ltc,
26
+ CryptoCurrency.ada,
27
+ CryptoCurrency.bch,
28
+ CryptoCurrency.usdt,
29
+ CryptoCurrency.bnb,
30
+ CryptoCurrency.xmr,
31
+ ].contains(element))
32
+ .toList())
33
+ ];
34
+
35
+ static final markup = secrets.quantexExchangeMarkup;
36
+
37
+ static const apiAuthority = 'api.myquantex.com';
38
+ static const getRate = '/api/swap/get-rate';
39
+ static const getCoins = '/api/swap/get-coins';
40
+ static const createOrder = '/api/swap/create-order';
41
+
42
+ @override
43
+ String get title => 'Quantex';
44
+
45
+ @override
46
+ bool get isAvailable => true;
47
+
48
+ @override
49
+ bool get isEnabled => true;
50
+
51
+ @override
52
+ bool get supportsFixedRate => false;
53
+
54
+ @override
55
+ ExchangeProviderDescription get description => ExchangeProviderDescription.quantex;
56
+
57
+ @override
58
+ Future<bool> checkIsAvailable() async => true;
59
+
60
+ @override
61
+ Future<Limits> fetchLimits({
62
+ required CryptoCurrency from,
63
+ required CryptoCurrency to,
64
+ required bool isFixedRateMode,
65
+ }) async {
66
+ try {
67
+ final uri = Uri.https(apiAuthority, getCoins);
68
+ final response = await get(uri);
69
+
70
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
71
+
72
+ if (response.statusCode != 200)
73
+ throw Exception('Unexpected http status: ${response.statusCode}');
74
+
75
+ final coinsInfo = responseJSON['data'] as List<dynamic>;
76
+
77
+ for (var coin in coinsInfo) {
78
+ if (coin['id'].toString().toUpperCase() == _normalizeCurrency(from)) {
79
+ return Limits(
80
+ min: double.parse(coin['min'].toString()),
81
+ max: double.parse(coin['max'].toString()),
82
+ );
83
+ }
84
+ }
85
+
86
+ // coin not found:
87
+ return Limits(min: 0, max: 0);
88
+ } catch (e) {
89
+ print(e.toString());
90
+ return Limits(min: 0, max: 0);
91
+ }
92
+ }
93
+
94
+ @override
95
+ Future<double> fetchRate({
96
+ required CryptoCurrency from,
97
+ required CryptoCurrency to,
98
+ required double amount,
99
+ required bool isFixedRateMode,
100
+ required bool isReceiveAmount,
101
+ }) async {
102
+ try {
103
+ if (amount == 0) return 0.0;
104
+
105
+ final headers = <String, String>{};
106
+ final params = <String, dynamic>{};
107
+ final body = <String, String>{
108
+ 'coin_send': _normalizeCurrency(from),
109
+ 'coin_receive': _normalizeCurrency(to),
110
+ 'ref': 'cake',
111
+ };
112
+
113
+ final uri = Uri.https(apiAuthority, getRate, params);
114
+ final response = await post(uri, body: body, headers: headers);
115
+ final responseBody = json.decode(response.body) as Map<String, dynamic>;
116
+
117
+ if (response.statusCode != 200)
118
+ throw Exception('Unexpected http status: ${response.statusCode}');
119
+
120
+ final data = responseBody['data'] as Map<String, dynamic>;
121
+ double rate = double.parse(data['price'].toString());
122
+ return rate;
123
+ } catch (e) {
124
+ print("error fetching rate: ${e.toString()}");
125
+ return 0.0;
126
+ }
127
+ }
128
+
129
+ @override
130
+ Future<Trade> createTrade({
131
+ required TradeRequest request,
132
+ required bool isFixedRateMode,
133
+ required bool isSendAll,
134
+ }) async {
135
+ try {
136
+ final headers = <String, String>{};
137
+ final params = <String, dynamic>{};
138
+ var body = <String, dynamic>{
139
+ 'coin_send': _normalizeCurrency(request.fromCurrency),
140
+ 'coin_receive': _normalizeCurrency(request.toCurrency),
141
+ 'amount_send': request.fromAmount,
142
+ 'recipient': request.toAddress,
143
+ 'ref': 'cake',
144
+ 'markup': markup,
145
+ };
146
+
147
+ String? fromNetwork = _networkFor(request.fromCurrency);
148
+ String? toNetwork = _networkFor(request.toCurrency);
149
+ if (fromNetwork != null) body['coin_send_network'] = fromNetwork;
150
+ if (toNetwork != null) body['coin_receive_network'] = toNetwork;
151
+
152
+ final uri = Uri.https(apiAuthority, createOrder, params);
153
+ final response = await post(uri, body: body, headers: headers);
154
+ final responseBody = json.decode(response.body) as Map<String, dynamic>;
155
+
156
+ if (response.statusCode == 400 || responseBody["success"] == false) {
157
+ final error = responseBody['errors'][0]['msg'] as String;
158
+ throw TradeNotCreatedException(description, description: error);
159
+ }
160
+
161
+ if (response.statusCode != 200)
162
+ throw Exception('Unexpected http status: ${response.statusCode}');
163
+
164
+ final responseData = responseBody['data'] as Map<String, dynamic>;
165
+
166
+ return Trade(
167
+ id: responseData["order_id"] as String,
168
+ inputAddress: responseData["server_address"] as String,
169
+ amount: request.fromAmount,
170
+ from: request.fromCurrency,
171
+ to: request.toCurrency,
172
+ provider: description,
173
+ createdAt: DateTime.now(),
174
+ state: TradeState.created,
175
+ payoutAddress: request.toAddress,
176
+ isSendAll: isSendAll,
177
+ );
178
+ } catch (e) {
179
+ print("error creating trade: ${e.toString()}");
180
+ throw TradeNotCreatedException(description, description: e.toString());
181
+ }
182
+ }
183
+
184
+ @override
185
+ Future<Trade> findTradeById({required String id}) async {
186
+ try {
187
+ final headers = <String, String>{};
188
+ final params = <String, dynamic>{};
189
+ var body = <String, dynamic>{
190
+ 'order_id': id,
191
+ };
192
+
193
+ final uri = Uri.https(apiAuthority, createOrder, params);
194
+ final response = await post(uri, body: body, headers: headers);
195
+ final responseBody = json.decode(response.body) as Map<String, dynamic>;
196
+
197
+ if (response.statusCode == 400 || responseBody["success"] == false) {
198
+ final error = responseBody['errors'][0]['msg'] as String;
199
+ throw TradeNotCreatedException(description, description: error);
200
+ }
201
+
202
+ if (response.statusCode != 200)
203
+ throw Exception('Unexpected http status: ${response.statusCode}');
204
+
205
+ final responseData = responseBody['data'] as Map<String, dynamic>;
206
+ final fromCurrency = responseData['coin_send'] as String;
207
+ final from = CryptoCurrency.fromString(fromCurrency);
208
+ final toCurrency = responseData['coin_receive'] as String;
209
+ final to = CryptoCurrency.fromString(toCurrency);
210
+ final inputAddress = responseData['server_address'] as String;
211
+ final status = responseData['status'] as String;
212
+ final state = TradeState.deserialize(raw: status);
213
+ final response_id = responseData['order_id'] as String;
214
+ final expectedSendAmount = responseData['amount_send'] as String;
215
+
216
+ return Trade(
217
+ id: response_id,
218
+ from: from,
219
+ to: to,
220
+ provider: description,
221
+ inputAddress: inputAddress,
222
+ amount: expectedSendAmount,
223
+ state: state,
224
+ );
225
+ } catch (e) {
226
+ print("error getting trade: ${e.toString()}");
227
+ throw TradeNotFoundException(
228
+ id,
229
+ provider: description,
230
+ description: e.toString(),
231
+ );
232
+ }
233
+ }
234
+
235
+ String _normalizeCurrency(CryptoCurrency currency) {
236
+ switch (currency) {
237
+ default:
238
+ return currency.title.toUpperCase();
239
+ }
240
+ }
241
+
242
+ String? _networkFor(CryptoCurrency currency) {
243
+ switch (currency) {
244
+ case CryptoCurrency.usdt:
245
+ return "USDT_ERC20";
246
+ case CryptoCurrency.bnb:
247
+ return "BNB_BSC";
248
+ default:
249
+ return null;
250
+ }
251
+ }
252
+}
lib/exchange/trade_state.dart
+27
@@ -28,6 +28,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
28
TradeState(raw: 'waitingAuthorization', title: 'Waiting authorization');
29
static const failed = TradeState(raw: 'failed', title: 'Failed');
30
static const completed = TradeState(raw: 'completed', title: 'Completed');
31
+ static const expired = TradeState(raw: 'expired', title: 'Expired');
32
static const settling = TradeState(raw: 'settling', title: 'Settlement in progress');
33
static const settled = TradeState(raw: 'settled', title: 'Settlement completed');
34
static const wait = TradeState(raw: 'wait', title: 'Waiting');
@@ -39,7 +40,33 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
40
static const exchanging = TradeState(raw: 'exchanging', title: 'Exchanging');
41
static const sending = TradeState(raw: 'sending', title: 'Sending');
42
static const success = TradeState(raw: 'success', title: 'Success');
43
+
44
static TradeState deserialize({required String raw}) {
45
+
46
+ switch (raw) {
47
+ case '1':
48
+ return unpaid;
49
+ case '2':
50
+ return paidUnconfirmed;
51
+ case '3':
52
+ return sending;
53
+ case '4':
54
+ return confirmed;
55
+ case '5':
56
+ case '6':
57
+ return exchanging;
58
+ case '7':
59
+ return sending;
60
+ case '8':
61
+ return complete;
62
+ case '9':
63
+ return expired;
64
+ case '10':
65
+ return underpaid;
66
+ case '11':
67
+ return failed;
68
+ }
69
+
70
switch (raw) {
71
case 'NOT_FOUND':
72
return notFound;
lib/view_model/exchange/exchange_trade_view_model.dart
+4
@@ -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/quantex_exchange_provider.dart';
8
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
9
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
10
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
@@ -48,6 +49,9 @@ abstract class ExchangeTradeViewModelBase with Store {
49
case ExchangeProviderDescription.exolix:
50
_provider = ExolixExchangeProvider();
51
break;
52
+ case ExchangeProviderDescription.quantex:
53
+ _provider = QuantexExchangeProvider();
54
+ break;
55
case ExchangeProviderDescription.thorChain:
56
_provider = ThorChainExchangeProvider(tradesStore: trades);
57
break;
lib/view_model/exchange/exchange_view_model.dart
+2
@@ -30,6 +30,7 @@ import 'package:cake_wallet/exchange/limits_state.dart';
30
import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
31
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
32
import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
33
+import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart';
34
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
35
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
36
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
@@ -157,6 +158,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
158
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
159
ThorChainExchangeProvider(tradesStore: trades),
160
if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
161
+ QuantexExchangeProvider(),
162
];
163
164
@observable
lib/view_model/trade_details_view_model.dart
+6
@@ -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/quantex_exchange_provider.dart';
8
import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
9
import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
10
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
@@ -56,6 +57,9 @@ abstract class TradeDetailsViewModelBase with Store {
57
case ExchangeProviderDescription.thorChain:
58
_provider = ThorChainExchangeProvider(tradesStore: trades);
59
break;
60
+ case ExchangeProviderDescription.quantex:
61
+ _provider = QuantexExchangeProvider();
62
+ break;
63
}
64
65
_updateItems();
@@ -80,6 +84,8 @@ abstract class TradeDetailsViewModelBase with Store {
84
return 'https://exolix.com/transaction/${trade.id}';
85
case ExchangeProviderDescription.thorChain:
86
return 'https://track.ninerealms.com/${trade.id}';
87
+ case ExchangeProviderDescription.quantex:
88
+ return 'https://myquantex.com/send/${trade.id}';
89
}
90
return null;
91
}
tool/utils/secret_key.dart
+1
@@ -38,6 +38,7 @@ class SecretKey {
38
SecretKey('walletConnectProjectId', () => ''),
39
SecretKey('moralisApiKey', () => ''),
40
SecretKey('ankrApiKey', () => ''),
41
+ SecretKey('quantexExchangeMarkup', () => ''),
42
];
43
44
static final evmChainsSecrets = [