Fixed rate for changenow.

M committed Jan 26, 2022 at 17:44 UTC 02de3104eb62c1b5f8a361ea3256ca8b1a662dc9
5 files changed +194 -139
lib/exchange/changenow/changenow_exchange_provider.dart
+110 -119
@@ -16,7 +16,8 @@ import 'package:cake_wallet/exchange/trade_not_created_exeption.dart';
16
17 class ChangeNowExchangeProvider extends ExchangeProvider {
18 ChangeNowExchangeProvider()
19 - : super(
19 + : _lastUsedRateId = '',
20 + super(
21 pairList: CryptoCurrency.all
22 .map((i) => CryptoCurrency.all
23 .map((k) => ExchangePair(from: i, to: k, reverse: true))
@@ -24,13 +25,13 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
25 .expand((i) => i)
26 .toList());
27
27 - static const apiUri = 'https://changenow.io/api/v1';
28 static const apiKey = secrets.changeNowApiKey;
29 - static const _exchangeAmountUriSufix = '/exchange-amount/';
30 - static const _transactionsUriSufix = '/transactions/';
31 - static const _minAmountUriSufix = '/min-amount/';
32 - static const _marketInfoUriSufix = '/market-info/';
33 - static const _fixedRateUriSufix = 'fixed-rate/';
29 + static const apiAuthority = 'api.changenow.io';
30 + static const createTradePath = '/v2/exchange';
31 + static const findTradeByIdPath = '/v2/exchange/by-id';
32 + static const estimatedAmountPath = '/v2/exchange/estimated-amount';
33 + static const rangePath = '/v2/exchange/range';
34 + static const apiHeaderKey = 'x-changenow-api-key';
35
36 @override
37 String get title => 'ChangeNOW';
@@ -45,68 +46,74 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
46 @override
47 Future<bool> checkIsAvailable() async => true;
48
49 + String _lastUsedRateId;
50 +
51 + static String getFlow(bool isFixedRate) => isFixedRate ? 'fixed-rate' : 'standard';
52 +
53 @override
54 Future<Limits> fetchLimits({CryptoCurrency from, CryptoCurrency to,
55 bool isFixedRateMode}) async {
51 - final fromTitle = defineCurrencyTitle(from);
52 - final toTitle = defineCurrencyTitle(to);
53 - final symbol = fromTitle + '_' + toTitle;
54 - final url = isFixedRateMode
55 - ? apiUri + _marketInfoUriSufix + _fixedRateUriSufix + apiKey
56 - : apiUri + _minAmountUriSufix + symbol;
57 - final response = await get(url);
58 -
59 - if (isFixedRateMode) {
60 - final responseJSON = json.decode(response.body) as List<dynamic>;
61 -
62 - for (var elem in responseJSON) {
63 - final elemFrom = elem["from"] as String;
64 - final elemTo = elem["to"] as String;
65 -
66 - if ((elemFrom == fromTitle) && (elemTo == toTitle)) {
67 - final min = elem["min"] as double;
68 - final max = elem["max"] as double;
69 -
70 - return Limits(min: min, max: max);
71 - }
72 - }
73 - return Limits(min: 0, max: 0);
74 - } else {
56 + final headers = {apiHeaderKey: apiKey};
57 + final normalizedFrom = normalizeCryptoCurrency(from);
58 + final normalizedTo = normalizeCryptoCurrency(to);
59 + final flow = getFlow(isFixedRateMode);
60 + final params = <String, String>{
61 + 'fromCurrency': normalizedFrom,
62 + 'toCurrency': normalizedTo,
63 + 'flow': flow};
64 + final uri = Uri.https(apiAuthority, rangePath, params);
65 + final response = await get(uri, headers: headers);
66 +
67 + if (response.statusCode == 400) {
68 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
76 - final min = responseJSON['minAmount'] as double;
69 + final error = responseJSON['error'] as String;
70 + final message = responseJSON['message'] as String;
71 + throw Exception('${error}\n$message');
72 + }
73
78 - return Limits(min: min, max: null);
74 + if (response.statusCode != 200) {
75 + return null;
76 }
77 +
78 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
79 + return Limits(
80 + min: responseJSON['minAmount'] as double,
81 + max: responseJSON['maxAmount'] as double);
82 }
83
84 @override
85 Future<Trade> createTrade({TradeRequest request, bool isFixedRateMode}) async {
84 - final url = isFixedRateMode
85 - ? apiUri + _transactionsUriSufix + _fixedRateUriSufix + apiKey
86 - : apiUri + _transactionsUriSufix + apiKey;
86 final _request = request as ChangeNowRequest;
88 - final fromTitle = defineCurrencyTitle(_request.from);
89 - final toTitle = defineCurrencyTitle(_request.to);
90 - final body = {
91 - 'from': fromTitle,
92 - 'to': toTitle,
87 + final headers = {
88 + apiHeaderKey: apiKey,
89 + 'Content-Type': 'application/json'};
90 + final flow = getFlow(isFixedRateMode);
91 + final body = <String, String>{
92 + 'fromCurrency': normalizeCryptoCurrency(_request.from),
93 + 'toCurrency': normalizeCryptoCurrency(_request.to),
94 + 'fromAmount': _request.fromAmount,
95 + 'toAmount': _request.toAmount,
96 'address': _request.address,
94 - 'amount': _request.amount,
97 + 'flow': flow,
98 'refundAddress': _request.refundAddress
99 };
100
98 - final response = await post(url,
99 - headers: {'Content-Type': 'application/json'}, body: json.encode(body));
100 -
101 - if (response.statusCode != 200) {
102 - if (response.statusCode == 400) {
103 - final responseJSON = json.decode(response.body) as Map<String, dynamic>;
104 - final error = responseJSON['message'] as String;
101 + if (isFixedRateMode) {
102 + body['rateId'] = _lastUsedRateId;
103 + }
104
106 - throw TradeNotCreatedException(description, description: error);
107 - }
105 + final uri = Uri.https(apiAuthority, createTradePath);
106 + final response = await post(uri, headers: headers, body: json.encode(body));
107 +
108 + if (response.statusCode == 400) {
109 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
110 + final error = responseJSON['error'] as String;
111 + final message = responseJSON['message'] as String;
112 + throw Exception('${error}\n$message');
113 + }
114
109 - throw TradeNotCreatedException(description);
115 + if (response.statusCode != 200) {
116 + return null;
117 }
118
119 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -124,25 +131,31 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
131 refundAddress: refundAddress,
132 extraId: extraId,
133 createdAt: DateTime.now(),
127 - amount: _request.amount,
134 + amount: _request.fromAmount,
135 state: TradeState.created);
136 }
137
138 @override
139 Future<Trade> findTradeById({@required String id}) async {
133 - final url = apiUri + _transactionsUriSufix + id + '/' + apiKey;
134 - final response = await get(url);
140 + final headers = {apiHeaderKey: apiKey};
141 + final params = <String, String>{'id': id};
142 + final uri = Uri.https(apiAuthority,findTradeByIdPath, params);
143 + final response = await get(uri, headers: headers);
144
136 - if (response.statusCode != 200) {
137 - if (response.statusCode == 400) {
138 - final responseJSON = json.decode(response.body) as Map<String, dynamic>;
139 - final error = responseJSON['message'] as String;
145 + if (response.statusCode == 404) {
146 + throw TradeNotFoundException(id, provider: description);
147 + }
148
141 - throw TradeNotFoundException(id,
142 - provider: description, description: error);
143 - }
149 + if (response.statusCode == 400) {
150 + final responseJSON = json.decode(response.body) as Map<String, dynamic>;
151 + final error = responseJSON['message'] as String;
152
145 - throw TradeNotFoundException(id, provider: description);
153 + throw TradeNotFoundException(id,
154 + provider: description, description: error);
155 + }
156 +
157 + if (response.statusCode != 200) {
158 + return null;
159 }
160
161 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -151,7 +164,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
164 final toCurrency = responseJSON['toCurrency'] as String;
165 final to = CryptoCurrency.fromString(toCurrency);
166 final inputAddress = responseJSON['payinAddress'] as String;
154 - final expectedSendAmount = responseJSON['expectedSendAmount'].toString();
167 + final expectedSendAmount = responseJSON['expectedAmountFrom'].toString();
168 final status = responseJSON['status'] as String;
169 final state = TradeState.deserialize(raw: status);
170 final extraId = responseJSON['payinExtraId'] as String;
@@ -181,68 +194,46 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
194 double amount,
195 bool isFixedRateMode,
196 bool isReceiveAmount}) async {
184 - if (isReceiveAmount && isFixedRateMode) {
185 - final url = apiUri + _marketInfoUriSufix + _fixedRateUriSufix + apiKey;
186 - final response = await get(url);
187 - final responseJSON = json.decode(response.body) as List<dynamic>;
188 - final fromTitle = defineCurrencyTitle(from);
189 - final toTitle = defineCurrencyTitle(to);
190 - var rate = 0.0;
191 - var fee = 0.0;
192 -
193 - for (var elem in responseJSON) {
194 - final elemFrom = elem["from"] as String;
195 - final elemTo = elem["to"] as String;
196 -
197 - if ((elemFrom == toTitle) && (elemTo == fromTitle)) {
198 - rate = elem["rate"] as double;
199 - fee = elem["minerFee"] as double;
200 - break;
201 - }
197 + try {
198 + if (amount == 0) {
199 + return 0.0;
200 }
201
204 - final estimatedAmount = (amount == 0.0)||(rate == 0.0) ? 0.0
205 - : (amount + fee)/rate;
206 -
207 - return estimatedAmount;
208 - } else {
209 - final url = defineUrlForCalculatingAmount(from, to, amount, isFixedRateMode);
210 - final response = await get(url);
202 + final headers = {apiHeaderKey: apiKey};
203 + final isReverse = isReceiveAmount;
204 + final type = isReverse ? 'reverse' : 'direct';
205 + final flow = getFlow(isFixedRateMode);
206 + final params = <String, String>{
207 + 'fromCurrency': isReverse ? normalizeCryptoCurrency(to) : normalizeCryptoCurrency(from),
208 + 'toCurrency': isReverse ? normalizeCryptoCurrency(from) : normalizeCryptoCurrency(to) ,
209 + 'type': type,
210 + 'flow': flow};
211 +
212 + if (isReverse) {
213 + params['toAmount'] = amount.toString();
214 + } else {
215 + params['fromAmount'] = amount.toString();
216 + }
217 +
218 + final uri = Uri.https(apiAuthority, estimatedAmountPath, params);
219 + final response = await get(uri, headers: headers);
220 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
212 - final estimatedAmount = responseJSON['estimatedAmount'] as double;
221 + final fromAmount = double.parse(responseJSON['fromAmount'].toString());
222 + final toAmount = double.parse(responseJSON['toAmount'].toString());
223 + final rateId = responseJSON['rateId'] as String ?? '';
224
214 - return estimatedAmount;
215 - }
216 - }
225 + if (rateId.isNotEmpty) {
226 + _lastUsedRateId = rateId;
227 + }
228
218 - static String defineUrlForCalculatingAmount(
219 - CryptoCurrency from,
220 - CryptoCurrency to,
221 - double amount,
222 - bool isFixedRateMode) {
223 - final fromTitle = defineCurrencyTitle(from);
224 - final toTitle = defineCurrencyTitle(to);
225 -
226 - return isFixedRateMode
227 - ? apiUri +
228 - _exchangeAmountUriSufix +
229 - _fixedRateUriSufix +
230 - amount.toString() +
231 - '/' +
232 - fromTitle +
233 - '_' +
234 - toTitle +
235 - '?api_key=' + apiKey
236 - : apiUri +
237 - _exchangeAmountUriSufix +
238 - amount.toString() +
239 - '/' +
240 - fromTitle +
241 - '_' +
242 - toTitle;
229 + return isReverse ? fromAmount : toAmount;
230 + } catch(e) {
231 + print(e.toString());
232 + return 0.0;
233 + }
234 }
235
245 - static String defineCurrencyTitle(CryptoCurrency currency) {
236 + static String normalizeCryptoCurrency(CryptoCurrency currency) {
237 const bnbTitle = 'bnbmainnet';
238 final currencyTitle = currency == CryptoCurrency.bnb
239 ? bnbTitle : currency.title.toLowerCase();
lib/exchange/changenow/changenow_request.dart
+7 -3
@@ -7,12 +7,16 @@ class ChangeNowRequest extends TradeRequest {
7 {@required this.from,
8 @required this.to,
9 @required this.address,
10 - @required this.amount,
11 - @required this.refundAddress});
10 + @required this.fromAmount,
11 + @required this.toAmount,
12 + @required this.refundAddress,
13 + @required this.isReverse});
14
15 CryptoCurrency from;
16 CryptoCurrency to;
17 String address;
16 - String amount;
18 + String fromAmount;
19 + String toAmount;
20 String refundAddress;
21 + bool isReverse;
22 }
lib/src/screens/exchange/exchange_page.dart
+39 -10
@@ -1,5 +1,6 @@
1 import 'dart:ui';
2 import 'package:cake_wallet/entities/parsed_address.dart';
3 +import 'package:cake_wallet/utils/debounce.dart';
4 import 'package:cw_core/sync_status.dart';
5 import 'package:cw_core/wallet_type.dart';
6 import 'package:cake_wallet/entities/parse_address_from_domain.dart';
@@ -44,6 +45,8 @@ class ExchangePage extends BasePage {
45 final _depositAddressFocus = FocusNode();
46 final _receiveAmountFocus = FocusNode();
47 final _receiveAddressFocus = FocusNode();
48 + final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500));
49 + final _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
50 var _isReactionsSet = false;
51
52 @override
@@ -99,6 +102,7 @@ class ExchangePage extends BasePage {
102 .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
103
104 return KeyboardActions(
105 + disableScroll: true,
106 config: KeyboardActionsConfig(
107 keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
108 keyboardBarColor:
@@ -113,7 +117,6 @@ class ExchangePage extends BasePage {
117 toolbarButtons: [(_) => KeyboardDoneButton()])
118 ]),
119 child: Container(
116 - height: 1,
120 color: Theme.of(context).backgroundColor,
121 child: Form(
122 key: _formKey,
@@ -314,6 +317,21 @@ class ExchangePage extends BasePage {
317 ],
318 ),
319 ),
320 + Padding(
321 + padding: EdgeInsets.only(top: 12, left: 24),
322 + child: Row(
323 + mainAxisAlignment: MainAxisAlignment.start,
324 + children: [
325 + StandardCheckbox(
326 + key: checkBoxKey,
327 + value: exchangeViewModel.isFixedRateMode,
328 + caption: S.of(context).fixed_rate,
329 + onChanged: (value) =>
330 + exchangeViewModel.isFixedRateMode = value,
331 + ),
332 + ],
333 + )
334 + ),
335 Padding(
336 padding: EdgeInsets.only(top: 30, left: 24, bottom: 24),
337 child: Row(
@@ -548,7 +566,9 @@ class ExchangePage extends BasePage {
566 final max = limitsState.limits.max != null
567 ? limitsState.limits.max.toString()
568 : null;
551 - final key = depositKey;
569 + final key = exchangeViewModel.isFixedRateMode
570 + ? receiveKey
571 + : depositKey;
572 key.currentState.changeLimits(min: min, max: max);
573 }
574
@@ -656,8 +676,13 @@ class ExchangePage extends BasePage {
676 max = '...';
677 }
678
659 - depositKey.currentState.changeLimits(min: min, max: max);
660 - receiveKey.currentState.changeLimits(min: null, max: null);
679 + if (exchangeViewModel.isFixedRateMode) {
680 + depositKey.currentState.changeLimits(min: null, max: null);
681 + receiveKey.currentState.changeLimits(min: min, max: max);
682 + } else {
683 + depositKey.currentState.changeLimits(min: min, max: max);
684 + receiveKey.currentState.changeLimits(min: null, max: null);
685 + }
686 });
687
688 depositAddressController.addListener(
@@ -665,9 +690,11 @@ class ExchangePage extends BasePage {
690
691 depositAmountController.addListener(() {
692 if (depositAmountController.text != exchangeViewModel.depositAmount) {
668 - exchangeViewModel.changeDepositAmount(
669 - amount: depositAmountController.text);
670 - exchangeViewModel.isReceiveAmountEntered = false;
693 + _depositAmountDebounce.run(() {
694 + exchangeViewModel.changeDepositAmount(
695 + amount: depositAmountController.text);
696 + exchangeViewModel.isReceiveAmountEntered = false;
697 + });
698 }
699 });
700
@@ -676,9 +703,11 @@ class ExchangePage extends BasePage {
703
704 receiveAmountController.addListener(() {
705 if (receiveAmountController.text != exchangeViewModel.receiveAmount) {
679 - exchangeViewModel.changeReceiveAmount(
680 - amount: receiveAmountController.text);
681 - exchangeViewModel.isReceiveAmountEntered = true;
706 + _receiveAmountDebounce.run(() {
707 + exchangeViewModel.changeReceiveAmount(
708 + amount: receiveAmountController.text);
709 + exchangeViewModel.isReceiveAmountEntered = true;
710 + });
711 }
712 });
713
lib/utils/debounce.dart new
+14
@@ -0,0 +1,14 @@
1 +import 'dart:async';
2 +import 'package:flutter/foundation.dart';
3 +
4 +class Debounce {
5 + Debounce(this.duration);
6 +
7 + final Duration duration;
8 + Timer _timer;
9 +
10 + void run(VoidCallback action) {
11 + _timer?.cancel();
12 + _timer = Timer(duration, action);
13 + }
14 +}
\ No newline at end of file
lib/view_model/exchange/exchange_view_model.dart
+24 -7
@@ -57,10 +57,14 @@ abstract class ExchangeViewModelBase with Store {
57 receiveCurrencies = CryptoCurrency.all
58 .where((cryptoCurrency) => !excludeCurrencies.contains(cryptoCurrency))
59 .toList();
60 - _defineIsReceiveAmountEditable();
60 + isReverse = false;
61 isFixedRateMode = false;
62 isReceiveAmountEntered = false;
63 + _defineIsReceiveAmountEditable();
64 loadLimits();
65 + reaction(
66 + (_) => isFixedRateMode,
67 + (Object _) => _defineIsReceiveAmountEditable());
68 }
69
70 final WalletBase wallet;
@@ -129,6 +133,8 @@ abstract class ExchangeViewModelBase with Store {
133
134 Limits limits;
135
136 + bool isReverse;
137 +
138 NumberFormat _cryptoNumberFormat;
139
140 SettingsStore _settingsStore;
@@ -164,6 +170,7 @@ abstract class ExchangeViewModelBase with Store {
170 @action
171 void changeReceiveAmount({String amount}) {
172 receiveAmount = amount;
173 + isReverse = true;
174
175 if (amount == null || amount.isEmpty) {
176 depositAmount = '';
@@ -190,6 +197,7 @@ abstract class ExchangeViewModelBase with Store {
197 @action
198 void changeDepositAmount({String amount}) {
199 depositAmount = amount;
200 + isReverse = false;
201
202 if (amount == null || amount.isEmpty) {
203 depositAmount = '';
@@ -217,9 +225,15 @@ abstract class ExchangeViewModelBase with Store {
225 limitsState = LimitsIsLoading();
226
227 try {
228 + final from = isFixedRateMode
229 + ? receiveCurrency
230 + : depositCurrency;
231 + final to = isFixedRateMode
232 + ? depositCurrency
233 + : receiveCurrency;
234 limits = await provider.fetchLimits(
221 - from: depositCurrency,
222 - to: receiveCurrency,
235 + from: from,
236 + to: to,
237 isFixedRateMode: isFixedRateMode);
238 limitsState = LimitsLoadedSuccessfully(limits: limits);
239 } catch (e) {
@@ -250,10 +264,12 @@ abstract class ExchangeViewModelBase with Store {
264 request = ChangeNowRequest(
265 from: depositCurrency,
266 to: receiveCurrency,
253 - amount: depositAmount?.replaceAll(',', '.'),
267 + fromAmount: depositAmount?.replaceAll(',', '.'),
268 + toAmount: receiveAmount?.replaceAll(',', '.'),
269 refundAddress: depositAddress,
255 - address: receiveAddress);
256 - amount = depositAmount;
270 + address: receiveAddress,
271 + isReverse: isReverse);
272 + amount = isReverse ? receiveAmount : depositAmount;
273 currency = depositCurrency;
274 }
275
@@ -422,6 +438,7 @@ abstract class ExchangeViewModelBase with Store {
438 } else {
439 isReceiveAmountEditable = false;
440 }*/
425 - isReceiveAmountEditable = false;
441 + //isReceiveAmountEditable = false;
442 + isReceiveAmountEditable = (isFixedRateMode ?? false) && provider is ChangeNowExchangeProvider;
443 }
444 }