Cw 110 integrate simple swap exchange (#459)
* add support for SimpleSwap exchange * fix edit receive amount field * fixed merge conflicts * Fix issues from code review
Godwin Asuquo committed
Aug 31, 2022 at 18:34 UTC
d9905a7cf36802f706c80f8d69592960e65d1028
15 files changed
+307
-17
android/.settings/org.eclipse.buildship.core.prefs
+1
-1
@@ -1,2 +1,2 @@
1
connection.project.dir=
2
-eclipse.preferences.version=1
2
+eclipse.preferences.version=1
\ No newline at end of file
assets/images/simpleSwap.png
Binary files /dev/null and b/assets/images/simpleSwap.png differ
lib/exchange/exchange_provider_description.dart
+5
@@ -13,6 +13,9 @@ class ExchangeProviderDescription extends EnumerableItem<int>
13
14
static const sideShift =
15
ExchangeProviderDescription(title: 'SideShift', raw: 3);
16
+
17
+ static const simpleSwap =
18
+ ExchangeProviderDescription(title: 'SimpleSwap', raw: 4);
19
20
static ExchangeProviderDescription deserialize({int raw}) {
21
switch (raw) {
@@ -24,6 +27,8 @@ class ExchangeProviderDescription extends EnumerableItem<int>
27
return morphToken;
28
case 3:
29
return sideShift;
30
+ case 4:
31
+ return simpleSwap;
32
default:
33
return null;
34
}
lib/exchange/sideshift/sideshift_exchange_provider.dart
+7
-7
@@ -47,8 +47,8 @@ class SideShiftExchangeProvider extends ExchangeProvider {
47
if (amount == 0) {
48
return 0.0;
49
}
50
- final fromCurrency = normalizeCryptoCurrency(from);
51
- final toCurrency = normalizeCryptoCurrency(to);
50
+ final fromCurrency = _normalizeCryptoCurrency(from);
51
+ final toCurrency = _normalizeCryptoCurrency(to);
52
final url =
53
apiBaseUrl + rangePath + '/' + fromCurrency + '/' + toCurrency;
54
final response = await get(url);
@@ -136,8 +136,8 @@ class SideShiftExchangeProvider extends ExchangeProvider {
136
Future<String> _createQuote(SideShiftRequest request) async {
137
final url = apiBaseUrl + quotePath;
138
final headers = {'Content-Type': 'application/json'};
139
- final depositMethod = normalizeCryptoCurrency(request.depositMethod);
140
- final settleMethod = normalizeCryptoCurrency(request.settleMethod);
139
+ final depositMethod = _normalizeCryptoCurrency(request.depositMethod);
140
+ final settleMethod = _normalizeCryptoCurrency(request.settleMethod);
141
final body = {
142
'depositMethod': depositMethod,
143
'settleMethod': settleMethod,
@@ -166,8 +166,8 @@ class SideShiftExchangeProvider extends ExchangeProvider {
166
@override
167
Future<Limits> fetchLimits(
168
{CryptoCurrency from, CryptoCurrency to, bool isFixedRateMode}) async {
169
- final fromCurrency = normalizeCryptoCurrency(from);
170
- final toCurrency = normalizeCryptoCurrency(to);
169
+ final fromCurrency = _normalizeCryptoCurrency(from);
170
+ final toCurrency = _normalizeCryptoCurrency(to);
171
final url = apiBaseUrl + rangePath + '/' + fromCurrency + '/' + toCurrency;
172
final response = await get(url);
173
@@ -247,7 +247,7 @@ class SideShiftExchangeProvider extends ExchangeProvider {
247
@override
248
String get title => 'SideShift';
249
250
- static String normalizeCryptoCurrency(CryptoCurrency currency) {
250
+ static String _normalizeCryptoCurrency(CryptoCurrency currency) {
251
switch (currency) {
252
case CryptoCurrency.zaddr:
253
return 'zaddr';
lib/exchange/simpleswap/simpleswap_exchange_provider.dart
new
+217
@@ -0,0 +1,217 @@
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/simpleswap/simpleswap_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:cw_core/crypto_currency.dart';
11
+import 'package:cake_wallet/exchange/trade_request.dart';
12
+import 'package:cake_wallet/exchange/trade.dart';
13
+import 'package:cake_wallet/exchange/limits.dart';
14
+import 'package:cake_wallet/.secrets.g.dart' as secrets;
15
+import 'package:http/http.dart';
16
+
17
+class SimpleSwapExchangeProvider extends ExchangeProvider {
18
+ SimpleSwapExchangeProvider()
19
+ : super(
20
+ pairList: CryptoCurrency.all
21
+ .map((i) =>
22
+ CryptoCurrency.all.map((k) => ExchangePair(from: i, to: k, reverse: true)).where((c) => c != null))
23
+ .expand((i) => i)
24
+ .toList());
25
+
26
+ static const apiAuthority = 'api.simpleswap.io';
27
+ static const getEstimatePath = '/v1/get_estimated';
28
+ static const rangePath = '/v1/get_ranges';
29
+ static const getExchangePath = '/v1/get_exchange';
30
+ static const createExchangePath = '/v1/create_exchange';
31
+ static const apiKey = secrets.simpleSwapApiKey;
32
+
33
+ @override
34
+ ExchangeProviderDescription get description =>
35
+ ExchangeProviderDescription.simpleSwap;
36
+
37
+ @override
38
+ Future<double> calculateAmount(
39
+ {CryptoCurrency from, CryptoCurrency to, double amount, bool isFixedRateMode, bool isReceiveAmount}) async {
40
+ try {
41
+ if (amount == 0) {
42
+ return 0.0;
43
+ }
44
+ final fromCurrency = _normalizeCryptoCurrency(from);
45
+ final toCurrency = _normalizeCryptoCurrency(to);
46
+ final params = <String, String>{
47
+ 'api_key': apiKey,
48
+ 'currency_from': fromCurrency,
49
+ 'currency_to': toCurrency,
50
+ 'amount': amount.toString(),
51
+ 'fixed': isFixedRateMode.toString()
52
+ };
53
+ final uri = Uri.https(apiAuthority, getEstimatePath, params);
54
+
55
+ final response = await get(uri);
56
+
57
+ if (response.body == null) return 0.00;
58
+ final data = json.decode(response.body) as String;
59
+
60
+ return double.parse(data);
61
+ } catch (_) {
62
+ return 0.00;
63
+ }
64
+ }
65
+
66
+ @override
67
+ Future<bool> checkIsAvailable() async {
68
+ final uri = Uri.https(apiAuthority, getEstimatePath, <String, String>{'api_key': apiKey});
69
+ final response = await get(uri);
70
+
71
+ return !(response.statusCode == 403);
72
+ }
73
+
74
+ @override
75
+ Future<Trade> createTrade({TradeRequest request, bool isFixedRateMode}) async {
76
+ final _request = request as SimpleSwapRequest;
77
+ final headers = {
78
+ 'Content-Type': 'application/json'};
79
+ final params = <String, String>{
80
+ 'api_key': apiKey,
81
+ };
82
+ final body = <String, dynamic>{
83
+ "currency_from": _normalizeCryptoCurrency(_request.from),
84
+ "currency_to": _normalizeCryptoCurrency(_request.to),
85
+ "amount": _request.amount,
86
+ "fixed": isFixedRateMode,
87
+ "user_refund_address": _request.refundAddress,
88
+ "address_to": _request.address
89
+ };
90
+ final uri = Uri.https(apiAuthority, createExchangePath, params);
91
+
92
+ final response = await post(uri, headers: headers, body: json.encode(body));
93
+
94
+ if (response.statusCode != 200) {
95
+ if (response.statusCode == 400) {
96
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
97
+ final error = responseJSON['message'] as String;
98
+
99
+ throw TradeNotCreatedException(description, description: error);
100
+ }
101
+
102
+ throw TradeNotCreatedException(description);
103
+ }
104
+
105
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
106
+ final id = responseJSON['id'] as String;
107
+ final inputAddress = responseJSON['address_from'] as String;
108
+ final settleAddress = responseJSON['user_refund_address'] as String;
109
+
110
+ return Trade(
111
+ id: id,
112
+ provider: description,
113
+ from: _request.from,
114
+ to: _request.to,
115
+ inputAddress: inputAddress,
116
+ refundAddress: settleAddress,
117
+ state: TradeState.created,
118
+ amount: _request.amount,
119
+ createdAt: DateTime.now(),
120
+ );
121
+ }
122
+
123
+ @override
124
+ Future<Limits> fetchLimits({CryptoCurrency from, CryptoCurrency to, bool isFixedRateMode}) async {
125
+ final fromCurrency = _normalizeCryptoCurrency(from);
126
+ final toCurrency = _normalizeCryptoCurrency(to);
127
+ final params = <String, dynamic>{
128
+ 'api_key': apiKey,
129
+ 'fixed': isFixedRateMode.toString(),
130
+ 'currency_from': fromCurrency,
131
+ 'currency_to': toCurrency,
132
+ };
133
+ final uri = Uri.https(apiAuthority, rangePath, params);
134
+
135
+ final response = await get(uri);
136
+
137
+ if (response.statusCode == 500) {
138
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
139
+ final error = responseJSON['message'] as String;
140
+
141
+ throw Exception('$error');
142
+ }
143
+
144
+ if (response.statusCode != 200) {
145
+ return null;
146
+ }
147
+
148
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
149
+ final min = responseJSON['min'] != null ? double.tryParse(responseJSON['min'] as String) : null;
150
+ final max = responseJSON['max'] != null ? double.parse(responseJSON['max'] as String) : null;
151
+
152
+ return Limits(min: min, max: max);
153
+ }
154
+
155
+ @override
156
+ Future<Trade> findTradeById({String id}) async {
157
+ final params = {'api_key': apiKey, 'id': id};
158
+ final uri = Uri.https(apiAuthority, getExchangePath, params);
159
+ final response = await get(uri);
160
+
161
+ if (response.statusCode == 404) {
162
+ throw TradeNotFoundException(id, provider: description);
163
+ }
164
+
165
+ if (response.statusCode == 400) {
166
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
167
+ final error = responseJSON['message'] as String;
168
+
169
+ throw TradeNotFoundException(id, provider: description, description: error);
170
+ }
171
+
172
+ if (response.statusCode != 200) {
173
+ return null;
174
+ }
175
+
176
+ final responseJSON = json.decode(response.body) as Map<String, dynamic>;
177
+ final fromCurrency = responseJSON['currency_from'] as String;
178
+ final from = CryptoCurrency.fromString(fromCurrency);
179
+ final toCurrency = responseJSON['currency_to'] as String;
180
+ final to = CryptoCurrency.fromString(toCurrency);
181
+ final inputAddress = responseJSON['address_from'] as String;
182
+ final expectedSendAmount = responseJSON['expected_amount'].toString();
183
+ final status = responseJSON['status'] as String;
184
+ final state = TradeState.deserialize(raw: status);
185
+
186
+ return Trade(
187
+ id: id,
188
+ from: from,
189
+ to: to,
190
+ provider: description,
191
+ inputAddress: inputAddress,
192
+ amount: expectedSendAmount,
193
+ state: state,
194
+ );
195
+ }
196
+
197
+ @override
198
+ bool get isAvailable => true;
199
+
200
+ @override
201
+ String get title => 'SimpleSwap';
202
+
203
+ static String _normalizeCryptoCurrency(CryptoCurrency currency) {
204
+ switch (currency) {
205
+ case CryptoCurrency.zaddr:
206
+ return 'zec';
207
+ case CryptoCurrency.zec:
208
+ return 'zec';
209
+ case CryptoCurrency.bnb:
210
+ return currency.tag.toLowerCase();
211
+ case CryptoCurrency.usdterc20:
212
+ return 'usdterc';
213
+ default:
214
+ return currency.title.toLowerCase();
215
+ }
216
+ }
217
+}
lib/exchange/simpleswap/simpleswap_request.dart
new
+20
@@ -0,0 +1,20 @@
1
+import 'package:cake_wallet/exchange/trade_request.dart';
2
+import 'package:cw_core/crypto_currency.dart';
3
+import 'package:flutter/material.dart';
4
+
5
+class SimpleSwapRequest extends TradeRequest {
6
+ SimpleSwapRequest({
7
+ @required this.from,
8
+ @required this.to,
9
+ @required this.address,
10
+ @required this.amount,
11
+ @required this.refundAddress,
12
+ });
13
+
14
+ CryptoCurrency from;
15
+ CryptoCurrency to;
16
+ String address;
17
+ String amount;
18
+ String toAmount;
19
+ String refundAddress;
20
+}
lib/src/screens/dashboard/widgets/trade_row.dart
+3
@@ -90,6 +90,9 @@ class TradeRow extends StatelessWidget {
90
case ExchangeProviderDescription.sideShift:
91
image = Image.asset('assets/images/sideshift.png', width: 36, height: 36);
92
break;
93
+ case ExchangeProviderDescription.simpleSwap:
94
+ image = Image.asset('assets/images/simpleSwap.png', width: 36, height: 36);
95
+ break;
96
default:
97
image = null;
98
}
lib/src/screens/exchange/exchange_page.dart
+2
@@ -744,7 +744,9 @@ class ExchangePage extends BasePage {
744
});
745
746
_receiveAmountFocus.addListener(() {
747
+ if(receiveAmountController.text.isNotEmpty){
748
exchangeViewModel.isFixedRateMode = true;
749
+ }
750
exchangeViewModel.changeReceiveAmount(
751
amount: receiveAmountController.text);
752
});
lib/src/screens/exchange/exchange_template_page.dart
-4
@@ -1,8 +1,6 @@
1
import 'dart:ui';
2
import 'package:cake_wallet/exchange/exchange_provider.dart';
3
-import 'package:cake_wallet/exchange/exchange_template.dart';
3
import 'package:cake_wallet/src/screens/base_page.dart';
5
-import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
4
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
5
import 'package:flutter/cupertino.dart';
6
import 'package:flutter/material.dart';
@@ -19,9 +17,7 @@ import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart';
17
import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart';
18
import 'package:cake_wallet/src/widgets/primary_button.dart';
19
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
22
-import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
20
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
24
-import 'package:cake_wallet/core/address_validator.dart';
21
import 'package:cake_wallet/core/amount_validator.dart';
22
import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
23
lib/src/screens/exchange/widgets/present_provider_picker.dart
+3
@@ -74,6 +74,9 @@ class PresentProviderPicker extends StatelessWidget {
74
case ExchangeProviderDescription.sideShift:
75
images.add(Image.asset('assets/images/sideshift.png', width: 20));
76
break;
77
+ case ExchangeProviderDescription.simpleSwap:
78
+ images.add(Image.asset('assets/images/simpleSwap.png', width: 20));
79
+ break;
80
}
81
}
82
lib/store/dashboard/trade_filter_store.dart
+14
-3
@@ -11,7 +11,9 @@ abstract class TradeFilterStoreBase with Store {
11
TradeFilterStoreBase(
12
{this.displayXMRTO = true,
13
this.displayChangeNow = true,
14
- this.displayMorphToken = true});
14
+ this.displayMorphToken = true,
15
+ this.displaySimpleSwap = true,
16
+ });
17
18
@observable
19
bool displayXMRTO;
@@ -22,6 +24,9 @@ abstract class TradeFilterStoreBase with Store {
24
@observable
25
bool displayMorphToken;
26
27
+ @observable
28
+ bool displaySimpleSwap;
29
+
30
@action
31
void toggleDisplayExchange(ExchangeProviderDescription provider) {
32
switch (provider) {
@@ -34,13 +39,16 @@ abstract class TradeFilterStoreBase with Store {
39
case ExchangeProviderDescription.morphToken:
40
displayMorphToken = !displayMorphToken;
41
break;
42
+ case ExchangeProviderDescription.simpleSwap:
43
+ displaySimpleSwap = !displaySimpleSwap;
44
+ break;
45
}
46
}
47
48
List<TradeListItem> filtered({List<TradeListItem> trades, WalletBase wallet}) {
49
final _trades =
50
trades.where((item) => item.trade.walletId == wallet.id).toList();
43
- final needToFilter = !displayChangeNow || !displayXMRTO || !displayMorphToken;
51
+ final needToFilter = !displayChangeNow || !displayXMRTO || !displayMorphToken || !displaySimpleSwap;
52
53
return needToFilter
54
? _trades
@@ -52,7 +60,10 @@ abstract class TradeFilterStoreBase with Store {
60
ExchangeProviderDescription.changeNow) ||
61
(displayMorphToken &&
62
item.trade.provider ==
55
- ExchangeProviderDescription.morphToken))
63
+ ExchangeProviderDescription.morphToken)
64
+ ||(displaySimpleSwap &&
65
+ item.trade.provider ==
66
+ ExchangeProviderDescription.simpleSwap))
67
.toList()
68
: _trades;
69
}
lib/view_model/exchange/exchange_trade_view_model.dart
+8
@@ -1,4 +1,6 @@
1
import 'dart:async';
2
+import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
3
+import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart';
4
import 'package:cw_core/wallet_base.dart';
5
import 'package:cw_core/crypto_currency.dart';
6
import 'package:cake_wallet/exchange/changenow/changenow_exchange_provider.dart';
@@ -37,6 +39,12 @@ abstract class ExchangeTradeViewModelBase with Store {
39
case ExchangeProviderDescription.morphToken:
40
_provider = MorphTokenExchangeProvider(trades: trades);
41
break;
42
+ case ExchangeProviderDescription.sideShift:
43
+ _provider = SideShiftExchangeProvider();
44
+ break;
45
+ case ExchangeProviderDescription.simpleSwap:
46
+ _provider = SimpleSwapExchangeProvider();
47
+ break;
48
}
49
50
items = ObservableList<ExchangeTradeItem>();
lib/view_model/exchange/exchange_view_model.dart
+16
-2
@@ -1,5 +1,7 @@
1
import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
2
import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart';
3
+import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart';
4
+import 'package:cake_wallet/exchange/simpleswap/simpleswap_request.dart';
5
import 'package:cw_core/wallet_base.dart';
6
import 'package:cw_core/crypto_currency.dart';
7
import 'package:cw_core/sync_status.dart';
@@ -35,7 +37,7 @@ abstract class ExchangeViewModelBase with Store {
37
this.tradesStore, this._settingsStore) {
38
const excludeDepositCurrencies = [CryptoCurrency.xhv];
39
const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb, CryptoCurrency.xhv];
38
- providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider()];
40
+ providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider(), SimpleSwapExchangeProvider()];
41
_initialPairBasedOnWallet();
42
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
43
isReceiveAddressEnabled = !(receiveCurrency == wallet.currency);
@@ -267,6 +269,18 @@ abstract class ExchangeViewModelBase with Store {
269
currency = depositCurrency;
270
}
271
272
+ if (provider is SimpleSwapExchangeProvider) {
273
+ request = SimpleSwapRequest(
274
+ from: depositCurrency,
275
+ to: receiveCurrency,
276
+ amount: depositAmount?.replaceAll(',', '.'),
277
+ address: receiveAddress,
278
+ refundAddress: depositAddress,
279
+ );
280
+ amount = depositAmount;
281
+ currency = depositCurrency;
282
+ }
283
+
284
if (provider is XMRTOExchangeProvider) {
285
request = XMRTOTradeRequest(
286
from: depositCurrency,
@@ -459,6 +473,6 @@ abstract class ExchangeViewModelBase with Store {
473
isReceiveAmountEditable = false;
474
}*/
475
//isReceiveAmountEditable = false;
462
- isReceiveAmountEditable = provider is ChangeNowExchangeProvider;
476
+ isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider;
477
}
478
}
lib/view_model/trade_details_view_model.dart
+10
@@ -4,6 +4,7 @@ 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/simpleswap/simpleswap_exchange_provider.dart';
8
import 'package:cake_wallet/exchange/trade.dart';
9
import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart';
10
import 'package:cake_wallet/utils/date_formatter.dart';
@@ -35,6 +36,9 @@ abstract class TradeDetailsViewModelBase with Store {
36
case ExchangeProviderDescription.sideShift:
37
_provider = SideShiftExchangeProvider();
38
break;
39
+ case ExchangeProviderDescription.simpleSwap:
40
+ _provider = SimpleSwapExchangeProvider();
41
+ break;
42
}
43
44
items = ObservableList<StandartListItem>();
@@ -112,6 +116,12 @@ abstract class TradeDetailsViewModelBase with Store {
116
title: 'Track', value: buildURL, onTap: () => launch(buildURL)));
117
}
118
119
+ if (trade.provider == ExchangeProviderDescription.simpleSwap) {
120
+ final buildURL = 'https://simpleswap.io/exchange?id=${trade.id.toString()}';
121
+ items.add(TrackTradeListItem(
122
+ title: 'Track', value: buildURL, onTap: () => launch(buildURL)));
123
+ }
124
+
125
if (trade.createdAt != null) {
126
items.add(StandartListItem(
127
title: S.current.trade_details_created_at,
tool/utils/secret_key.dart
+1
@@ -25,6 +25,7 @@ class SecretKey {
25
SecretKey('moonPaySecretKey', () => ''),
26
SecretKey('sideShiftAffiliateId', () => ''),
27
SecretKey('sideShiftApiKey', () => ''),
28
+ SecretKey('simpleSwapApiKey', () => '')
29
];
30
31
final String name;