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
}