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;