From 02de3104eb62c1b5f8a361ea3256ca8b1a662dc9 Mon Sep 17 00:00:00 2001 From: M Date: Wed, 26 Jan 2022 17:44:15 +0200 Subject: [PATCH] Fixed rate for changenow. --- .../changenow_exchange_provider.dart | 231 +++++++++--------- lib/exchange/changenow/changenow_request.dart | 10 +- lib/src/screens/exchange/exchange_page.dart | 49 +++- lib/utils/debounce.dart | 14 ++ .../exchange/exchange_view_model.dart | 31 ++- 5 files changed, 195 insertions(+), 140 deletions(-) create mode 100644 lib/utils/debounce.dart diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/changenow/changenow_exchange_provider.dart index 0622609cf..53523b6a7 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/changenow/changenow_exchange_provider.dart @@ -16,7 +16,8 @@ import 'package:cake_wallet/exchange/trade_not_created_exeption.dart'; class ChangeNowExchangeProvider extends ExchangeProvider { ChangeNowExchangeProvider() - : super( + : _lastUsedRateId = '', + super( pairList: CryptoCurrency.all .map((i) => CryptoCurrency.all .map((k) => ExchangePair(from: i, to: k, reverse: true)) @@ -24,13 +25,13 @@ class ChangeNowExchangeProvider extends ExchangeProvider { .expand((i) => i) .toList()); - static const apiUri = 'https://changenow.io/api/v1'; static const apiKey = secrets.changeNowApiKey; - static const _exchangeAmountUriSufix = '/exchange-amount/'; - static const _transactionsUriSufix = '/transactions/'; - static const _minAmountUriSufix = '/min-amount/'; - static const _marketInfoUriSufix = '/market-info/'; - static const _fixedRateUriSufix = 'fixed-rate/'; + static const apiAuthority = 'api.changenow.io'; + static const createTradePath = '/v2/exchange'; + static const findTradeByIdPath = '/v2/exchange/by-id'; + static const estimatedAmountPath = '/v2/exchange/estimated-amount'; + static const rangePath = '/v2/exchange/range'; + static const apiHeaderKey = 'x-changenow-api-key'; @override String get title => 'ChangeNOW'; @@ -45,68 +46,74 @@ class ChangeNowExchangeProvider extends ExchangeProvider { @override Future checkIsAvailable() async => true; + String _lastUsedRateId; + + static String getFlow(bool isFixedRate) => isFixedRate ? 'fixed-rate' : 'standard'; + @override Future fetchLimits({CryptoCurrency from, CryptoCurrency to, bool isFixedRateMode}) async { - final fromTitle = defineCurrencyTitle(from); - final toTitle = defineCurrencyTitle(to); - final symbol = fromTitle + '_' + toTitle; - final url = isFixedRateMode - ? apiUri + _marketInfoUriSufix + _fixedRateUriSufix + apiKey - : apiUri + _minAmountUriSufix + symbol; - final response = await get(url); - - if (isFixedRateMode) { - final responseJSON = json.decode(response.body) as List; - - for (var elem in responseJSON) { - final elemFrom = elem["from"] as String; - final elemTo = elem["to"] as String; - - if ((elemFrom == fromTitle) && (elemTo == toTitle)) { - final min = elem["min"] as double; - final max = elem["max"] as double; - - return Limits(min: min, max: max); - } - } - return Limits(min: 0, max: 0); - } else { + final headers = {apiHeaderKey: apiKey}; + final normalizedFrom = normalizeCryptoCurrency(from); + final normalizedTo = normalizeCryptoCurrency(to); + final flow = getFlow(isFixedRateMode); + final params = { + 'fromCurrency': normalizedFrom, + 'toCurrency': normalizedTo, + 'flow': flow}; + final uri = Uri.https(apiAuthority, rangePath, params); + final response = await get(uri, headers: headers); + + if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; - final min = responseJSON['minAmount'] as double; - - return Limits(min: min, max: null); + final error = responseJSON['error'] as String; + final message = responseJSON['message'] as String; + throw Exception('${error}\n$message'); } + + if (response.statusCode != 200) { + return null; + } + + final responseJSON = json.decode(response.body) as Map; + return Limits( + min: responseJSON['minAmount'] as double, + max: responseJSON['maxAmount'] as double); } @override Future createTrade({TradeRequest request, bool isFixedRateMode}) async { - final url = isFixedRateMode - ? apiUri + _transactionsUriSufix + _fixedRateUriSufix + apiKey - : apiUri + _transactionsUriSufix + apiKey; final _request = request as ChangeNowRequest; - final fromTitle = defineCurrencyTitle(_request.from); - final toTitle = defineCurrencyTitle(_request.to); - final body = { - 'from': fromTitle, - 'to': toTitle, + final headers = { + apiHeaderKey: apiKey, + 'Content-Type': 'application/json'}; + final flow = getFlow(isFixedRateMode); + final body = { + 'fromCurrency': normalizeCryptoCurrency(_request.from), + 'toCurrency': normalizeCryptoCurrency(_request.to), + 'fromAmount': _request.fromAmount, + 'toAmount': _request.toAmount, 'address': _request.address, - 'amount': _request.amount, + 'flow': flow, 'refundAddress': _request.refundAddress }; - final response = await post(url, - headers: {'Content-Type': 'application/json'}, body: json.encode(body)); + if (isFixedRateMode) { + body['rateId'] = _lastUsedRateId; + } + + final uri = Uri.https(apiAuthority, createTradePath); + final response = await post(uri, headers: headers, body: json.encode(body)); + + if (response.statusCode == 400) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['error'] as String; + final message = responseJSON['message'] as String; + throw Exception('${error}\n$message'); + } if (response.statusCode != 200) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['message'] as String; - - throw TradeNotCreatedException(description, description: error); - } - - throw TradeNotCreatedException(description); + return null; } final responseJSON = json.decode(response.body) as Map; @@ -124,25 +131,31 @@ class ChangeNowExchangeProvider extends ExchangeProvider { refundAddress: refundAddress, extraId: extraId, createdAt: DateTime.now(), - amount: _request.amount, + amount: _request.fromAmount, state: TradeState.created); } @override Future findTradeById({@required String id}) async { - final url = apiUri + _transactionsUriSufix + id + '/' + apiKey; - final response = await get(url); + final headers = {apiHeaderKey: apiKey}; + final params = {'id': id}; + final uri = Uri.https(apiAuthority,findTradeByIdPath, params); + final response = await get(uri, headers: headers); + + if (response.statusCode == 404) { + throw TradeNotFoundException(id, provider: description); + } + + if (response.statusCode == 400) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['message'] as String; + + throw TradeNotFoundException(id, + provider: description, description: error); + } if (response.statusCode != 200) { - if (response.statusCode == 400) { - final responseJSON = json.decode(response.body) as Map; - final error = responseJSON['message'] as String; - - throw TradeNotFoundException(id, - provider: description, description: error); - } - - throw TradeNotFoundException(id, provider: description); + return null; } final responseJSON = json.decode(response.body) as Map; @@ -151,7 +164,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final toCurrency = responseJSON['toCurrency'] as String; final to = CryptoCurrency.fromString(toCurrency); final inputAddress = responseJSON['payinAddress'] as String; - final expectedSendAmount = responseJSON['expectedSendAmount'].toString(); + final expectedSendAmount = responseJSON['expectedAmountFrom'].toString(); final status = responseJSON['status'] as String; final state = TradeState.deserialize(raw: status); final extraId = responseJSON['payinExtraId'] as String; @@ -181,68 +194,46 @@ class ChangeNowExchangeProvider extends ExchangeProvider { double amount, bool isFixedRateMode, bool isReceiveAmount}) async { - if (isReceiveAmount && isFixedRateMode) { - final url = apiUri + _marketInfoUriSufix + _fixedRateUriSufix + apiKey; - final response = await get(url); - final responseJSON = json.decode(response.body) as List; - final fromTitle = defineCurrencyTitle(from); - final toTitle = defineCurrencyTitle(to); - var rate = 0.0; - var fee = 0.0; - - for (var elem in responseJSON) { - final elemFrom = elem["from"] as String; - final elemTo = elem["to"] as String; - - if ((elemFrom == toTitle) && (elemTo == fromTitle)) { - rate = elem["rate"] as double; - fee = elem["minerFee"] as double; - break; - } + try { + if (amount == 0) { + return 0.0; } - final estimatedAmount = (amount == 0.0)||(rate == 0.0) ? 0.0 - : (amount + fee)/rate; + final headers = {apiHeaderKey: apiKey}; + final isReverse = isReceiveAmount; + final type = isReverse ? 'reverse' : 'direct'; + final flow = getFlow(isFixedRateMode); + final params = { + 'fromCurrency': isReverse ? normalizeCryptoCurrency(to) : normalizeCryptoCurrency(from), + 'toCurrency': isReverse ? normalizeCryptoCurrency(from) : normalizeCryptoCurrency(to) , + 'type': type, + 'flow': flow}; - return estimatedAmount; - } else { - final url = defineUrlForCalculatingAmount(from, to, amount, isFixedRateMode); - final response = await get(url); + if (isReverse) { + params['toAmount'] = amount.toString(); + } else { + params['fromAmount'] = amount.toString(); + } + + final uri = Uri.https(apiAuthority, estimatedAmountPath, params); + final response = await get(uri, headers: headers); final responseJSON = json.decode(response.body) as Map; - final estimatedAmount = responseJSON['estimatedAmount'] as double; + final fromAmount = double.parse(responseJSON['fromAmount'].toString()); + final toAmount = double.parse(responseJSON['toAmount'].toString()); + final rateId = responseJSON['rateId'] as String ?? ''; - return estimatedAmount; + if (rateId.isNotEmpty) { + _lastUsedRateId = rateId; + } + + return isReverse ? fromAmount : toAmount; + } catch(e) { + print(e.toString()); + return 0.0; } } - static String defineUrlForCalculatingAmount( - CryptoCurrency from, - CryptoCurrency to, - double amount, - bool isFixedRateMode) { - final fromTitle = defineCurrencyTitle(from); - final toTitle = defineCurrencyTitle(to); - - return isFixedRateMode - ? apiUri + - _exchangeAmountUriSufix + - _fixedRateUriSufix + - amount.toString() + - '/' + - fromTitle + - '_' + - toTitle + - '?api_key=' + apiKey - : apiUri + - _exchangeAmountUriSufix + - amount.toString() + - '/' + - fromTitle + - '_' + - toTitle; - } - - static String defineCurrencyTitle(CryptoCurrency currency) { + static String normalizeCryptoCurrency(CryptoCurrency currency) { const bnbTitle = 'bnbmainnet'; final currencyTitle = currency == CryptoCurrency.bnb ? bnbTitle : currency.title.toLowerCase(); diff --git a/lib/exchange/changenow/changenow_request.dart b/lib/exchange/changenow/changenow_request.dart index f27106c40..9adf65e50 100644 --- a/lib/exchange/changenow/changenow_request.dart +++ b/lib/exchange/changenow/changenow_request.dart @@ -7,12 +7,16 @@ class ChangeNowRequest extends TradeRequest { {@required this.from, @required this.to, @required this.address, - @required this.amount, - @required this.refundAddress}); + @required this.fromAmount, + @required this.toAmount, + @required this.refundAddress, + @required this.isReverse}); CryptoCurrency from; CryptoCurrency to; String address; - String amount; + String fromAmount; + String toAmount; String refundAddress; + bool isReverse; } diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index d2a1e3c54..d7c44cee3 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -1,5 +1,6 @@ import 'dart:ui'; import 'package:cake_wallet/entities/parsed_address.dart'; +import 'package:cake_wallet/utils/debounce.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; @@ -44,6 +45,8 @@ class ExchangePage extends BasePage { final _depositAddressFocus = FocusNode(); final _receiveAmountFocus = FocusNode(); final _receiveAddressFocus = FocusNode(); + final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500)); + final _depositAmountDebounce = Debounce(Duration(milliseconds: 500)); var _isReactionsSet = false; @override @@ -99,6 +102,7 @@ class ExchangePage extends BasePage { .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel)); return KeyboardActions( + disableScroll: true, config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, keyboardBarColor: @@ -113,7 +117,6 @@ class ExchangePage extends BasePage { toolbarButtons: [(_) => KeyboardDoneButton()]) ]), child: Container( - height: 1, color: Theme.of(context).backgroundColor, child: Form( key: _formKey, @@ -314,6 +317,21 @@ class ExchangePage extends BasePage { ], ), ), + Padding( + padding: EdgeInsets.only(top: 12, left: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + StandardCheckbox( + key: checkBoxKey, + value: exchangeViewModel.isFixedRateMode, + caption: S.of(context).fixed_rate, + onChanged: (value) => + exchangeViewModel.isFixedRateMode = value, + ), + ], + ) + ), Padding( padding: EdgeInsets.only(top: 30, left: 24, bottom: 24), child: Row( @@ -548,7 +566,9 @@ class ExchangePage extends BasePage { final max = limitsState.limits.max != null ? limitsState.limits.max.toString() : null; - final key = depositKey; + final key = exchangeViewModel.isFixedRateMode + ? receiveKey + : depositKey; key.currentState.changeLimits(min: min, max: max); } @@ -656,8 +676,13 @@ class ExchangePage extends BasePage { max = '...'; } - depositKey.currentState.changeLimits(min: min, max: max); - receiveKey.currentState.changeLimits(min: null, max: null); + if (exchangeViewModel.isFixedRateMode) { + depositKey.currentState.changeLimits(min: null, max: null); + receiveKey.currentState.changeLimits(min: min, max: max); + } else { + depositKey.currentState.changeLimits(min: min, max: max); + receiveKey.currentState.changeLimits(min: null, max: null); + } }); depositAddressController.addListener( @@ -665,9 +690,11 @@ class ExchangePage extends BasePage { depositAmountController.addListener(() { if (depositAmountController.text != exchangeViewModel.depositAmount) { - exchangeViewModel.changeDepositAmount( - amount: depositAmountController.text); - exchangeViewModel.isReceiveAmountEntered = false; + _depositAmountDebounce.run(() { + exchangeViewModel.changeDepositAmount( + amount: depositAmountController.text); + exchangeViewModel.isReceiveAmountEntered = false; + }); } }); @@ -676,9 +703,11 @@ class ExchangePage extends BasePage { receiveAmountController.addListener(() { if (receiveAmountController.text != exchangeViewModel.receiveAmount) { - exchangeViewModel.changeReceiveAmount( - amount: receiveAmountController.text); - exchangeViewModel.isReceiveAmountEntered = true; + _receiveAmountDebounce.run(() { + exchangeViewModel.changeReceiveAmount( + amount: receiveAmountController.text); + exchangeViewModel.isReceiveAmountEntered = true; + }); } }); diff --git a/lib/utils/debounce.dart b/lib/utils/debounce.dart new file mode 100644 index 000000000..fdc66b82d --- /dev/null +++ b/lib/utils/debounce.dart @@ -0,0 +1,14 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; + +class Debounce { + Debounce(this.duration); + + final Duration duration; + Timer _timer; + + void run(VoidCallback action) { + _timer?.cancel(); + _timer = Timer(duration, action); + } +} \ No newline at end of file diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 2fe6b3d53..ce36e875e 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -57,10 +57,14 @@ abstract class ExchangeViewModelBase with Store { receiveCurrencies = CryptoCurrency.all .where((cryptoCurrency) => !excludeCurrencies.contains(cryptoCurrency)) .toList(); - _defineIsReceiveAmountEditable(); + isReverse = false; isFixedRateMode = false; isReceiveAmountEntered = false; + _defineIsReceiveAmountEditable(); loadLimits(); + reaction( + (_) => isFixedRateMode, + (Object _) => _defineIsReceiveAmountEditable()); } final WalletBase wallet; @@ -129,6 +133,8 @@ abstract class ExchangeViewModelBase with Store { Limits limits; + bool isReverse; + NumberFormat _cryptoNumberFormat; SettingsStore _settingsStore; @@ -164,6 +170,7 @@ abstract class ExchangeViewModelBase with Store { @action void changeReceiveAmount({String amount}) { receiveAmount = amount; + isReverse = true; if (amount == null || amount.isEmpty) { depositAmount = ''; @@ -190,6 +197,7 @@ abstract class ExchangeViewModelBase with Store { @action void changeDepositAmount({String amount}) { depositAmount = amount; + isReverse = false; if (amount == null || amount.isEmpty) { depositAmount = ''; @@ -217,9 +225,15 @@ abstract class ExchangeViewModelBase with Store { limitsState = LimitsIsLoading(); try { + final from = isFixedRateMode + ? receiveCurrency + : depositCurrency; + final to = isFixedRateMode + ? depositCurrency + : receiveCurrency; limits = await provider.fetchLimits( - from: depositCurrency, - to: receiveCurrency, + from: from, + to: to, isFixedRateMode: isFixedRateMode); limitsState = LimitsLoadedSuccessfully(limits: limits); } catch (e) { @@ -250,10 +264,12 @@ abstract class ExchangeViewModelBase with Store { request = ChangeNowRequest( from: depositCurrency, to: receiveCurrency, - amount: depositAmount?.replaceAll(',', '.'), + fromAmount: depositAmount?.replaceAll(',', '.'), + toAmount: receiveAmount?.replaceAll(',', '.'), refundAddress: depositAddress, - address: receiveAddress); - amount = depositAmount; + address: receiveAddress, + isReverse: isReverse); + amount = isReverse ? receiveAmount : depositAmount; currency = depositCurrency; } @@ -422,6 +438,7 @@ abstract class ExchangeViewModelBase with Store { } else { isReceiveAmountEditable = false; }*/ - isReceiveAmountEditable = false; + //isReceiveAmountEditable = false; + isReceiveAmountEditable = (isFixedRateMode ?? false) && provider is ChangeNowExchangeProvider; } }