Cw 72 implement sideshift exchange (#332)
* add sideshift exchange provider * add secret key * Fix issues * Fix issues * refactor code * add permission checks to side shift * fix formatting issues
Godwin Asuquo committed
Apr 13, 2022 at 14:28 UTC
6378d052ac18ddfa858690277b425c872c8a708c
10 files changed
+322
-2
assets/images/sideshift.png
Binary files /dev/null and b/assets/images/sideshift.png differ
lib/exchange/exchange_provider_description.dart
+5
@@ -11,6 +11,9 @@ class ExchangeProviderDescription extends EnumerableItem<int>
11
static const morphToken =
12
ExchangeProviderDescription(title: 'MorphToken', raw: 2);
13
14
+ static const sideShift =
15
+ ExchangeProviderDescription(title: 'SideShift', raw: 3);
16
+
17
static ExchangeProviderDescription deserialize({int raw}) {
18
switch (raw) {
19
case 0:
@@ -19,6 +22,8 @@ class ExchangeProviderDescription extends EnumerableItem<int>
22
return changeNow;
23
case 2:
24
return morphToken;
25
+ case 3:
26
+ return sideShift;
27
default:
28
return null;
29
}
lib/exchange/sideshift/sideshift_exchange_provider.dart
new
+265
@@ -0,0 +1,265 @@
1
+import 'dart:convert';
2
+
3
+import 'package:cake_wallet/exchange/exchange_pair.dart';
4
+import 'package:cake_wallet/exchange/exchange_provider.dart';
5
+import 'package:cake_wallet/exchange/exchange_provider_description.dart';
6
+import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart';
7
+import 'package:cake_wallet/exchange/trade_not_created_exeption.dart';
8
+import 'package:cake_wallet/exchange/trade_not_found_exeption.dart';
9
+import 'package:cake_wallet/exchange/trade_state.dart';
10
+import 'package:cake_wallet/.secrets.g.dart' as secrets;
11
+import 'package:cw_core/crypto_currency.dart';
12
+import 'package:cake_wallet/exchange/trade_request.dart';
13
+import 'package:cake_wallet/exchange/trade.dart';
14
+import 'package:cake_wallet/exchange/limits.dart';
15
+import 'package:flutter/foundation.dart';
16
+import 'package:http/http.dart';
17
+
18
+class SideShiftExchangeProvider extends ExchangeProvider {
19
+ SideShiftExchangeProvider()
20
+ : super(
21
+ pairList: CryptoCurrency.all
22
+ .map((i) => CryptoCurrency.all
23
+ .map((k) => ExchangePair(from: i, to: k, reverse: true))
24
+ .where((c) => c != null))
25
+ .expand((i) => i)
26
+ .toList());
27
+
28
+ static const apiKey = secrets.sideShiftApiKey;
29
+ static const affiliateId = secrets.sideShiftAffiliateId;
30
+ static const apiBaseUrl = 'https://sideshift.ai/api';
31
+ static const rangePath = '/v1/pairs';
32
+ static const orderPath = '/v1/orders';
33
+ static const quotePath = '/v1/quotes';
34
+ static const permissionPath = '/v1/permissions';
35
+ static const apiHeaderKey = 'x-sideshift-secret';
36
+
37
+ @override
38
+ ExchangeProviderDescription get description =>
39
+ ExchangeProviderDescription.sideShift;
40
+
41
+ @override
42
+ Future<double> calculateAmount(
43
+ {CryptoCurrency from,
44
+ CryptoCurrency to,
45
+ double amount,
46
+ bool isFixedRateMode,
47
+ bool isReceiveAmount}) async {
48
+ try {
49
+ if (amount == 0) {
50
+ return 0.0;
51
+ }
52
+ final fromCurrency = normalizeCryptoCurrency(from);
53
+ final toCurrency = normalizeCryptoCurrency(to);
54
+ final url =
55
+ apiBaseUrl + rangePath + '/' + fromCurrency + '/' + toCurrency;
56
+ final response = await get(url);
57
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
58
+ final rate = double.parse(responseJSON['rate'] as String);
59
+ final max = double.parse(responseJSON['max'] as String);
60
+
61
+ if (amount > max) return 0.00;
62
+
63
+ final estimatedAmount = rate * amount;
64
+
65
+ return estimatedAmount;
66
+ } catch (_) {
67
+ return 0.00;
68
+ }
69
+ }
70
+
71
+ @override
72
+ Future<bool> checkIsAvailable() async {
73
+ const url = apiBaseUrl + permissionPath;
74
+ final response = await get(url);
75
+
76
+ if (response.statusCode == 500) {
77
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
78
+ final error = responseJSON['error']['message'] as String;
79
+
80
+ throw Exception('$error');
81
+ }
82
+
83
+ if (response.statusCode != 200) {
84
+ return false;
85
+ }
86
+
87
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
88
+ final canCreateOrder = responseJSON['createOrder'] as bool;
89
+ final canCreateQuote = responseJSON['createQuote'] as bool;
90
+ return canCreateOrder && canCreateQuote;
91
+ }
92
+
93
+ @override
94
+ Future<Trade> createTrade(
95
+ {TradeRequest request, bool isFixedRateMode}) async {
96
+ final _request = request as SideShiftRequest;
97
+ final quoteId = await _createQuote(_request);
98
+ final url = apiBaseUrl + orderPath;
99
+ final headers = {apiHeaderKey: apiKey, 'Content-Type': 'application/json'};
100
+ final body = {
101
+ 'type': 'fixed',
102
+ 'quoteId': quoteId,
103
+ 'affiliateId': affiliateId,
104
+ 'settleAddress': _request.settleAddress,
105
+ 'refundAddress': _request.refundAddress
106
+ };
107
+ final response = await post(url, headers: headers, body: json.encode(body));
108
+
109
+ if (response.statusCode != 201) {
110
+ if (response.statusCode == 400) {
111
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
112
+ final error = responseJSON['error']['message'] as String;
113
+
114
+ throw TradeNotCreatedException(description, description: error);
115
+ }
116
+
117
+ throw TradeNotCreatedException(description);
118
+ }
119
+
120
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
121
+ final id = responseJSON['id'] as String;
122
+ final inputAddress = responseJSON['depositAddress']['address'] as String;
123
+ final settleAddress = responseJSON['settleAddress']['address'] as String;
124
+
125
+ return Trade(
126
+ id: id,
127
+ provider: description,
128
+ from: _request.depositMethod,
129
+ to: _request.settleMethod,
130
+ inputAddress: inputAddress,
131
+ refundAddress: settleAddress,
132
+ state: TradeState.created,
133
+ amount: _request.depositAmount,
134
+ createdAt: DateTime.now(),
135
+ );
136
+ }
137
+
138
+ Future<String> _createQuote(SideShiftRequest request) async {
139
+ final url = apiBaseUrl + quotePath;
140
+ final headers = {apiHeaderKey: apiKey, 'Content-Type': 'application/json'};
141
+ final depositMethod = normalizeCryptoCurrency(request.depositMethod);
142
+ final settleMethod = normalizeCryptoCurrency(request.settleMethod);
143
+ final body = {
144
+ 'depositMethod': depositMethod,
145
+ 'settleMethod': settleMethod,
146
+ 'affiliateId': affiliateId,
147
+ 'depositAmount': request.depositAmount,
148
+ };
149
+ final response = await post(url, headers: headers, body: json.encode(body));
150
+
151
+ if (response.statusCode != 201) {
152
+ if (response.statusCode == 400) {
153
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
154
+ final error = responseJSON['error']['message'] as String;
155
+
156
+ throw TradeNotCreatedException(description, description: error);
157
+ }
158
+
159
+ throw TradeNotCreatedException(description);
160
+ }
161
+
162
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
163
+ final quoteId = responseJSON['id'] as String;
164
+
165
+ return quoteId;
166
+ }
167
+
168
+ @override
169
+ Future<Limits> fetchLimits(
170
+ {CryptoCurrency from, CryptoCurrency to, bool isFixedRateMode}) async {
171
+ final fromCurrency = normalizeCryptoCurrency(from);
172
+ final toCurrency = normalizeCryptoCurrency(to);
173
+ final url = apiBaseUrl + rangePath + '/' + fromCurrency + '/' + toCurrency;
174
+ final response = await get(url);
175
+
176
+ if (response.statusCode == 500) {
177
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
178
+ final error = responseJSON['error']['message'] as String;
179
+
180
+ throw Exception('$error');
181
+ }
182
+
183
+ if (response.statusCode != 200) {
184
+ return null;
185
+ }
186
+
187
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
188
+ final min = double.parse(responseJSON['min'] as String);
189
+ final max = double.parse(responseJSON['max'] as String);
190
+
191
+ return Limits(min: min, max: max);
192
+ }
193
+
194
+ @override
195
+ Future<Trade> findTradeById({@required String id}) async {
196
+ final url = apiBaseUrl + orderPath + '/' + id;
197
+ final response = await get(url);
198
+
199
+ if (response.statusCode == 404) {
200
+ throw TradeNotFoundException(id, provider: description);
201
+ }
202
+
203
+ if (response.statusCode == 400) {
204
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
205
+ final error = responseJSON['error']['message'] as String;
206
+
207
+ throw TradeNotFoundException(id,
208
+ provider: description, description: error);
209
+ }
210
+
211
+ if (response.statusCode != 200) {
212
+ return null;
213
+ }
214
+
215
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
216
+ final fromCurrency = responseJSON['depositMethodId'] as String;
217
+ final from = CryptoCurrency.fromString(fromCurrency);
218
+ final toCurrency = responseJSON['settleMethodId'] as String;
219
+ final to = CryptoCurrency.fromString(toCurrency);
220
+ final inputAddress = responseJSON['depositAddress']['address'] as String;
221
+ final expectedSendAmount = responseJSON['depositAmount'].toString();
222
+ final deposits = responseJSON['deposits'] as List;
223
+ TradeState state;
224
+
225
+ if (deposits != null && deposits.isNotEmpty) {
226
+ final status = deposits[0]['status'] as String;
227
+ state = TradeState.deserialize(raw: status);
228
+ }
229
+
230
+ final expiredAtRaw = responseJSON['expiresAtISO'] as String;
231
+ final expiredAt =
232
+ expiredAtRaw != null ? DateTime.parse(expiredAtRaw).toLocal() : null;
233
+
234
+ return Trade(
235
+ id: id,
236
+ from: from,
237
+ to: to,
238
+ provider: description,
239
+ inputAddress: inputAddress,
240
+ amount: expectedSendAmount,
241
+ state: state,
242
+ expiredAt: expiredAt,
243
+ );
244
+ }
245
+
246
+ @override
247
+ bool get isAvailable => true;
248
+
249
+ @override
250
+ String get title => 'SideShift';
251
+
252
+ static String normalizeCryptoCurrency(CryptoCurrency currency) {
253
+ const bnbTitle = 'bsc';
254
+ const usdterc20 = 'usdtErc20';
255
+
256
+ switch (currency) {
257
+ case CryptoCurrency.bnb:
258
+ return bnbTitle;
259
+ case CryptoCurrency.usdterc20:
260
+ return usdterc20;
261
+ default:
262
+ return currency.title.toLowerCase();
263
+ }
264
+ }
265
+}
lib/exchange/sideshift/sideshift_request.dart
new
+17
@@ -0,0 +1,17 @@
1
+import 'package:cake_wallet/exchange/trade_request.dart';
2
+import 'package:cw_core/crypto_currency.dart';
3
+
4
+class SideShiftRequest extends TradeRequest {
5
+ final CryptoCurrency depositMethod;
6
+ final CryptoCurrency settleMethod;
7
+ final String depositAmount;
8
+ final String settleAddress;
9
+ final String refundAddress;
10
+
11
+ SideShiftRequest(
12
+ {this.depositMethod,
13
+ this.settleMethod,
14
+ this.depositAmount,
15
+ this.settleAddress,
16
+ this.refundAddress,});
17
+}
lib/exchange/trade_state.dart
+2
-1
@@ -33,7 +33,8 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
33
TradeState(raw: 'waitingAuthorization', title: 'Waiting authorization');
34
static const failed = TradeState(raw: 'failed', title: 'Failed');
35
static const completed = TradeState(raw: 'completed', title: 'Completed');
36
-
36
+ static const settling = TradeState(raw: 'settling', title: 'Settlement in progress');
37
+ static const settled = TradeState(raw: 'settled', title: 'Settlement completed');
38
static TradeState deserialize({String raw}) {
39
switch (raw) {
40
case 'pending':
lib/src/screens/dashboard/widgets/trade_row.dart
+3
@@ -87,6 +87,9 @@ class TradeRow extends StatelessWidget {
87
case ExchangeProviderDescription.morphToken:
88
image = Image.asset('assets/images/morph.png', height: 36, width: 36);
89
break;
90
+ case ExchangeProviderDescription.sideShift:
91
+ image = Image.asset('assets/images/sideshift.png', width: 36, height: 36);
92
+ break;
93
default:
94
image = null;
95
}
lib/src/screens/exchange/widgets/present_provider_picker.dart
+3
@@ -71,6 +71,9 @@ class PresentProviderPicker extends StatelessWidget {
71
case ExchangeProviderDescription.morphToken:
72
images.add(Image.asset('assets/images/morph_icon.png'));
73
break;
74
+ case ExchangeProviderDescription.sideShift:
75
+ images.add(Image.asset('assets/images/sideshift.png', width: 20));
76
+ break;
77
}
78
}
79
lib/view_model/exchange/exchange_view_model.dart
+15
-1
@@ -1,3 +1,5 @@
1
+import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
2
+import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart';
3
import 'package:cw_core/wallet_base.dart';
4
import 'package:cw_core/crypto_currency.dart';
5
import 'package:cw_core/sync_status.dart';
@@ -33,7 +35,7 @@ abstract class ExchangeViewModelBase with Store {
35
this.tradesStore, this._settingsStore) {
36
const excludeDepositCurrencies = [CryptoCurrency.xhv];
37
const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb, CryptoCurrency.xhv];
36
- providerList = [ChangeNowExchangeProvider()];
38
+ providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider()];
39
_initialPairBasedOnWallet();
40
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
41
isReceiveAddressEnabled = !(receiveCurrency == wallet.currency);
@@ -253,6 +255,18 @@ abstract class ExchangeViewModelBase with Store {
255
String amount;
256
CryptoCurrency currency;
257
258
+ if (provider is SideShiftExchangeProvider) {
259
+ request = SideShiftRequest(
260
+ depositMethod: depositCurrency,
261
+ settleMethod: receiveCurrency,
262
+ depositAmount: depositAmount?.replaceAll(',', '.'),
263
+ settleAddress: receiveAddress,
264
+ refundAddress: depositAddress,
265
+ );
266
+ amount = depositAmount;
267
+ currency = depositCurrency;
268
+ }
269
+
270
if (provider is XMRTOExchangeProvider) {
271
request = XMRTOTradeRequest(
272
from: depositCurrency,
lib/view_model/trade_details_view_model.dart
+10
@@ -3,6 +3,7 @@ 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/morphtoken/morphtoken_exchange_provider.dart';
6
+import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
7
import 'package:cake_wallet/exchange/trade.dart';
8
import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart';
9
import 'package:cake_wallet/utils/date_formatter.dart';
@@ -31,6 +32,9 @@ abstract class TradeDetailsViewModelBase with Store {
32
case ExchangeProviderDescription.morphToken:
33
_provider = MorphTokenExchangeProvider(trades: trades);
34
break;
35
+ case ExchangeProviderDescription.sideShift:
36
+ _provider = SideShiftExchangeProvider();
37
+ break;
38
}
39
40
items = ObservableList<StandartListItem>();
@@ -102,6 +106,12 @@ abstract class TradeDetailsViewModelBase with Store {
106
}));
107
}
108
109
+ if (trade.provider == ExchangeProviderDescription.sideShift) {
110
+ final buildURL = 'https://sideshift.ai/orders/${trade.id.toString()}';
111
+ items.add(TrackTradeListItem(
112
+ title: 'Track', value: buildURL, onTap: () => launch(buildURL)));
113
+ }
114
+
115
if (trade.createdAt != null) {
116
items.add(StandartListItem(
117
title: S.current.trade_details_created_at,
tool/utils/secret_key.dart
+2
@@ -23,6 +23,8 @@ class SecretKey {
23
SecretKey('wyreAccountId', () => ''),
24
SecretKey('moonPayApiKey', () => ''),
25
SecretKey('moonPaySecretKey', () => ''),
26
+ SecretKey('sideShiftAffiliateId', () => ''),
27
+ SecretKey('sideShiftApiKey', () => ''),
28
];
29
30
final String name;