Merge pull request #48 from cypherstack/ui-testing

UI testing
This commit is contained in:
Rylee Davis 2022-09-08 14:33:37 -06:00 committed by GitHub
commit 553f68481f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 568 additions and 194 deletions

View file

@ -1,5 +1,6 @@
import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart';
import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
@ -59,6 +60,20 @@ class CachedElectrumX {
"setHash": "", "setHash": "",
"coins": <dynamic>[], "coins": <dynamic>[],
}; };
// try up to 3 times
for (int i = 0; i < 3; i++) {
final result = await getInitialAnonymitySetCache(groupId);
if (result != null) {
set["setHash"] = result["setHash"];
set["blockHash"] = result["blockHash"];
set["coins"] = result["coins"];
Logging.instance.log(
"Populated initial anon set cache for group $groupId",
level: LogLevel.Info);
break;
}
}
} else { } else {
set = Map<String, dynamic>.from(cachedSet); set = Map<String, dynamic>.from(cachedSet);
} }

View file

@ -0,0 +1,163 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/utilities/logger.dart';
enum CNEstimateType { direct, reverse }
enum CNFlowType implements Comparable<CNFlowType> {
standard("standard"),
fixedRate("fixed-rate");
const CNFlowType(this.value);
final String value;
@override
int compareTo(CNFlowType other) => value.compareTo(other.value);
}
class CNExchangeEstimate {
/// Ticker of the currency you want to exchange
final String fromCurrency;
/// Network of the currency you want to exchange
final String fromNetwork;
/// Ticker of the currency you want to receive
final String toCurrency;
/// Network of the currency you want to receive
final String toNetwork;
/// Type of exchange flow. Enum: ["standard", "fixed-rate"]
final CNFlowType flow;
/// Direction of exchange flow. Use "direct" value to set amount for
/// currencyFrom and get amount of currencyTo. Use "reverse" value to set
/// amount for currencyTo and get amount of currencyFrom.
/// Enum: ["direct", "reverse"]
final CNEstimateType type;
/// (Optional) Use rateId for fixed-rate flow. If this field is true, you
/// could use returned field "rateId" in next method for creating transaction
/// to freeze estimated amount that you got in this method. Current estimated
/// amount would be valid until time in field "validUntil"
final String? rateId;
/// Date and time before estimated amount would be freezed in case of using
/// rateId. If you set param "useRateId" to true, you could use returned field
/// "rateId" in next method for creating transaction to freeze estimated
/// amount that you got in this method. Estimated amount would be valid until
/// this date and time
final String? validUntil;
/// Dash-separated min and max estimated time in minutes
final String? transactionSpeedForecast;
/// Some warnings like warnings that transactions on this network
/// take longer or that the currency has moved to another network
final String? warningMessage;
/// Exchange amount of fromCurrency (in case when type=reverse it is an
/// estimated value)
final Decimal fromAmount;
/// Exchange amount of toCurrency (in case when type=direct it is an
/// estimated value)
final Decimal toAmount;
CNExchangeEstimate({
required this.fromCurrency,
required this.fromNetwork,
required this.toCurrency,
required this.toNetwork,
required this.flow,
required this.type,
this.rateId,
this.validUntil,
this.transactionSpeedForecast,
this.warningMessage,
required this.fromAmount,
required this.toAmount,
});
factory CNExchangeEstimate.fromJson(Map<String, dynamic> json) {
try {
final flow = CNFlowType.values
.firstWhere((element) => element.value == json["flow"]);
final type = CNEstimateType.values
.firstWhere((element) => element.name == json["type"]);
return CNExchangeEstimate(
fromCurrency: json["fromCurrency"] as String,
fromNetwork: json["fromNetwork"] as String,
toCurrency: json["toCurrency"] as String,
toNetwork: json["toNetwork"] as String,
flow: flow,
type: type,
rateId: json["rateId"] as String?,
validUntil: json["validUntil"] as String?,
transactionSpeedForecast: json["transactionSpeedForecast"] as String?,
warningMessage: json["warningMessage"] as String?,
fromAmount: Decimal.parse(json["fromAmount"].toString()),
toAmount: Decimal.parse(json["toAmount"].toString()),
);
} catch (e, s) {
Logging.instance
.log("Failed to parse: $json \n$e\n$s", level: LogLevel.Fatal);
rethrow;
}
}
Map<String, dynamic> toJson() {
return {
"fromCurrency": fromCurrency,
"fromNetwork": fromNetwork,
"toCurrency": toCurrency,
"toNetwork": toNetwork,
"flow": flow,
"type": type,
"rateId": rateId,
"validUntil": validUntil,
"transactionSpeedForecast": transactionSpeedForecast,
"warningMessage": warningMessage,
"fromAmount": fromAmount,
"toAmount": toAmount,
};
}
CNExchangeEstimate copyWith({
String? fromCurrency,
String? fromNetwork,
String? toCurrency,
String? toNetwork,
CNFlowType? flow,
CNEstimateType? type,
String? rateId,
String? validUntil,
String? transactionSpeedForecast,
String? warningMessage,
Decimal? fromAmount,
Decimal? toAmount,
}) {
return CNExchangeEstimate(
fromCurrency: fromCurrency ?? this.fromCurrency,
fromNetwork: fromNetwork ?? this.fromNetwork,
toCurrency: toCurrency ?? this.toCurrency,
toNetwork: toNetwork ?? this.toNetwork,
flow: flow ?? this.flow,
type: type ?? this.type,
rateId: rateId ?? this.rateId,
validUntil: validUntil ?? this.validUntil,
transactionSpeedForecast:
transactionSpeedForecast ?? this.transactionSpeedForecast,
warningMessage: warningMessage ?? this.warningMessage,
fromAmount: fromAmount ?? this.fromAmount,
toAmount: toAmount ?? this.toAmount,
);
}
@override
String toString() {
return "EstimatedExchangeAmount: ${toJson()}";
}
}

View file

@ -113,7 +113,7 @@ class EstimatedRateExchangeFormState extends ChangeNotifier {
await _updateMinFromAmount(shouldNotifyListeners: shouldNotifyListeners); await _updateMinFromAmount(shouldNotifyListeners: shouldNotifyListeners);
await updateRate(); await updateRate(shouldNotifyListeners: shouldNotifyListeners);
debugPrint( debugPrint(
"_updated TO: _from=${_from!.ticker} _to=${_to!.ticker} _fromAmount=$_fromAmount _toAmount=$_toAmount rate:$rate"); "_updated TO: _from=${_from!.ticker} _to=${_to!.ticker} _fromAmount=$_fromAmount _toAmount=$_toAmount rate:$rate");
@ -138,7 +138,7 @@ class EstimatedRateExchangeFormState extends ChangeNotifier {
await _updateMinFromAmount(shouldNotifyListeners: shouldNotifyListeners); await _updateMinFromAmount(shouldNotifyListeners: shouldNotifyListeners);
await updateRate(); await updateRate(shouldNotifyListeners: shouldNotifyListeners);
debugPrint( debugPrint(
"_updated FROM: _from=${_from!.ticker} _to=${_to!.ticker} _fromAmount=$_fromAmount _toAmount=$_toAmount rate:$rate"); "_updated FROM: _from=${_from!.ticker} _to=${_to!.ticker} _fromAmount=$_fromAmount _toAmount=$_toAmount rate:$rate");
@ -182,7 +182,7 @@ class EstimatedRateExchangeFormState extends ChangeNotifier {
} }
_fromAmount = newFromAmount; _fromAmount = newFromAmount;
await updateRate(); await updateRate(shouldNotifyListeners: shouldNotifyListeners);
if (shouldNotifyListeners) { if (shouldNotifyListeners) {
notifyListeners(); notifyListeners();
@ -256,7 +256,7 @@ class EstimatedRateExchangeFormState extends ChangeNotifier {
} }
} }
Future<void> updateRate() async { Future<void> updateRate({bool shouldNotifyListeners = false}) async {
rate = null; rate = null;
final amount = _fromAmount; final amount = _fromAmount;
final minAmount = _minFromAmount; final minAmount = _minFromAmount;
@ -275,5 +275,8 @@ class EstimatedRateExchangeFormState extends ChangeNotifier {
_toAmount = amt; _toAmount = amt;
} }
} }
if (shouldNotifyListeners) {
notifyListeners();
}
} }
} }

View file

@ -1,27 +1,44 @@
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart';
import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart'; import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart';
import 'package:stackwallet/services/change_now/change_now.dart';
import 'package:stackwallet/utilities/logger.dart';
class FixedRateExchangeFormState extends ChangeNotifier { class FixedRateExchangeFormState extends ChangeNotifier {
Decimal? _fromAmount; Decimal? _fromAmount;
Decimal? _toAmount; Decimal? _toAmount;
FixedRateMarket? _market; FixedRateMarket? _market;
FixedRateMarket? get market => _market; FixedRateMarket? get market => _market;
CNExchangeEstimate? _estimate;
CNExchangeEstimate? get estimate => _estimate;
Decimal? get rate {
if (_estimate == null) {
return null;
} else {
return (_estimate!.toAmount / _estimate!.fromAmount)
.toDecimal(scaleOnInfinitePrecision: 12);
}
}
Future<void> swap(FixedRateMarket reverseFixedRateMarket) async { Future<void> swap(FixedRateMarket reverseFixedRateMarket) async {
final Decimal? tmp = _fromAmount; final Decimal? tmp = _fromAmount;
_fromAmount = _toAmount; _fromAmount = _toAmount;
_toAmount = tmp; _toAmount = tmp;
await updateMarket(reverseFixedRateMarket, true); await updateMarket(reverseFixedRateMarket, false);
await updateRateEstimate(CNEstimateType.direct);
_toAmount = _estimate?.toAmount ?? Decimal.zero;
notifyListeners();
} }
String get fromAmountString => String get fromAmountString =>
_fromAmount == null ? "-" : _fromAmount!.toStringAsFixed(8); _fromAmount == null ? "" : _fromAmount!.toStringAsFixed(8);
String get toAmountString => String get toAmountString =>
_toAmount == null ? "-" : _toAmount!.toStringAsFixed(8); _toAmount == null ? "" : _toAmount!.toStringAsFixed(8);
Future<void> updateMarket( Future<void> updateMarket(
FixedRateMarket? market, FixedRateMarket? market,
@ -37,7 +54,7 @@ class FixedRateExchangeFormState extends ChangeNotifier {
if (_fromAmount! <= Decimal.zero) { if (_fromAmount! <= Decimal.zero) {
_toAmount = Decimal.zero; _toAmount = Decimal.zero;
} else { } else {
_toAmount = (_fromAmount! * _market!.rate) - _market!.minerFee; await updateRateEstimate(CNEstimateType.direct);
} }
} }
} }
@ -48,10 +65,10 @@ class FixedRateExchangeFormState extends ChangeNotifier {
} }
String get rateDisplayString { String get rateDisplayString {
if (_market == null) { if (_market == null || _estimate == null) {
return "N/A"; return "N/A";
} else { } else {
return "1 ${_market!.from.toUpperCase()} ~${_market!.rate.toStringAsFixed(8)} ${_market!.to.toUpperCase()}"; return "1 ${_estimate!.fromCurrency.toUpperCase()} ~${rate!.toStringAsFixed(8)} ${_estimate!.toCurrency.toUpperCase()}";
} }
} }
@ -78,14 +95,10 @@ class FixedRateExchangeFormState extends ChangeNotifier {
Decimal newToAmount, Decimal newToAmount,
bool shouldNotifyListeners, bool shouldNotifyListeners,
) async { ) async {
if (_market != null) {
_fromAmount = (newToAmount / _market!.rate)
.toDecimal(scaleOnInfinitePrecision: 12) +
_market!.minerFee;
}
_toAmount = newToAmount; _toAmount = newToAmount;
if (shouldNotifyListeners) { if (shouldNotifyListeners) {
await updateRateEstimate(CNEstimateType.reverse);
notifyListeners(); notifyListeners();
} }
} }
@ -94,12 +107,10 @@ class FixedRateExchangeFormState extends ChangeNotifier {
Decimal newFromAmount, Decimal newFromAmount,
bool shouldNotifyListeners, bool shouldNotifyListeners,
) async { ) async {
if (_market != null) {
_toAmount = (newFromAmount * _market!.rate) - _market!.minerFee;
}
_fromAmount = newFromAmount; _fromAmount = newFromAmount;
if (shouldNotifyListeners) { if (shouldNotifyListeners) {
await updateRateEstimate(CNEstimateType.direct);
notifyListeners(); notifyListeners();
} }
} }
@ -115,4 +126,53 @@ class FixedRateExchangeFormState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
} }
Future<void> updateRateEstimate(CNEstimateType direction) async {
if (market != null) {
Decimal? amount;
// set amount based on trade estimate direction
switch (direction) {
case CNEstimateType.direct:
if (_fromAmount != null
// &&
// market!.min >= _fromAmount! &&
// _fromAmount! <= market!.max
) {
amount = _fromAmount!;
}
break;
case CNEstimateType.reverse:
if (_toAmount != null
// &&
// market!.min >= _toAmount! &&
// _toAmount! <= market!.max
) {
amount = _toAmount!;
}
break;
}
if (amount != null && market != null && amount > Decimal.zero) {
final response = await ChangeNow.instance.getEstimatedExchangeAmountV2(
fromTicker: market!.from,
toTicker: market!.to,
fromOrTo: direction,
flow: CNFlowType.fixedRate,
amount: amount,
);
if (response.value != null) {
// update estimate if response succeeded
_estimate = response.value;
_toAmount = _estimate?.toAmount;
_fromAmount = _estimate?.fromAmount;
notifyListeners();
} else if (response.exception != null) {
Logging.instance.log("updateRateEstimate(): ${response.exception}",
level: LogLevel.Warning);
}
}
}
}
} }

View file

@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/exchange/change_now/available_floating_rate_pair.dart'; import 'package:stackwallet/models/exchange/change_now/available_floating_rate_pair.dart';
import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart';
import 'package:stackwallet/models/exchange/change_now/currency.dart'; import 'package:stackwallet/models/exchange/change_now/currency.dart';
import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart'; import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
@ -1011,6 +1012,24 @@ class _ExchangeViewState extends ConsumerState<ExchangeView> {
final to = availableCurrencies.firstWhere( final to = availableCurrencies.firstWhere(
(e) => e.ticker == toTicker); (e) => e.ticker == toTicker);
final newFromAmount =
Decimal.tryParse(_sendController.text);
if (newFromAmount != null) {
await ref
.read(
estimatedRateExchangeFormProvider)
.setFromAmountAndCalculateToAmount(
newFromAmount, false);
} else {
await ref
.read(
estimatedRateExchangeFormProvider)
.setFromAmountAndCalculateToAmount(
Decimal.zero, false);
_receiveController.text = "";
}
await ref await ref
.read(estimatedRateExchangeFormProvider) .read(estimatedRateExchangeFormProvider)
.updateTo(to, false); .updateTo(to, false);
@ -1055,6 +1074,23 @@ class _ExchangeViewState extends ConsumerState<ExchangeView> {
} catch (_) { } catch (_) {
market = null; market = null;
} }
final newFromAmount =
Decimal.tryParse(_sendController.text);
if (newFromAmount != null) {
await ref
.read(fixedRateExchangeFormProvider)
.setFromAmountAndCalculateToAmount(
newFromAmount, false);
} else {
await ref
.read(fixedRateExchangeFormProvider)
.setFromAmountAndCalculateToAmount(
Decimal.zero, false);
_receiveController.text = "";
}
await ref await ref
.read(fixedRateExchangeFormProvider) .read(fixedRateExchangeFormProvider)
.updateMarket(market, false); .updateMarket(market, false);
@ -1233,11 +1269,12 @@ class _ExchangeViewState extends ConsumerState<ExchangeView> {
final response = await ref final response = await ref
.read(changeNowProvider) .read(changeNowProvider)
.getEstimatedFixedRateExchangeAmount( .getEstimatedExchangeAmountV2(
fromTicker: fromTicker, fromTicker: fromTicker,
toTicker: toTicker, toTicker: toTicker,
fromAmount: sendAmount, fromOrTo: CNEstimateType.direct,
useRateId: true, amount: sendAmount,
flow: CNFlowType.fixedRate,
); );
bool? shouldCancel; bool? shouldCancel;
@ -1314,15 +1351,14 @@ class _ExchangeViewState extends ConsumerState<ExchangeView> {
} }
String rate = String rate =
"1 $fromTicker ~${ref.read(fixedRateExchangeFormProvider).market!.rate.toStringAsFixed(8)} $toTicker"; "1 $fromTicker ~${ref.read(fixedRateExchangeFormProvider).rate!.toStringAsFixed(8)} $toTicker";
final model = IncompleteExchangeModel( final model = IncompleteExchangeModel(
sendTicker: fromTicker, sendTicker: fromTicker,
receiveTicker: toTicker, receiveTicker: toTicker,
rateInfo: rate, rateInfo: rate,
sendAmount: sendAmount, sendAmount: sendAmount,
receiveAmount: receiveAmount: response.value!.toAmount,
response.value!.estimatedAmount,
rateId: response.value!.rateId, rateId: response.value!.rateId,
rateType: rateType, rateType: rateType,
); );

View file

@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/exchange/change_now/available_floating_rate_pair.dart'; import 'package:stackwallet/models/exchange/change_now/available_floating_rate_pair.dart';
import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart';
import 'package:stackwallet/models/exchange/change_now/currency.dart'; import 'package:stackwallet/models/exchange/change_now/currency.dart';
import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart'; import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
@ -1436,11 +1437,12 @@ class _WalletInitiatedExchangeViewState
final response = await ref final response = await ref
.read(changeNowProvider) .read(changeNowProvider)
.getEstimatedFixedRateExchangeAmount( .getEstimatedExchangeAmountV2(
fromTicker: fromTicker, fromTicker: fromTicker,
toTicker: toTicker, toTicker: toTicker,
fromAmount: sendAmount, fromOrTo: CNEstimateType.direct,
useRateId: true, amount: sendAmount,
flow: CNFlowType.fixedRate,
); );
bool? shouldCancel; bool? shouldCancel;
@ -1518,15 +1520,14 @@ class _WalletInitiatedExchangeViewState
} }
String rate = String rate =
"1 $fromTicker ~${ref.read(fixedRateExchangeFormProvider).market!.rate.toStringAsFixed(8)} $toTicker"; "1 $fromTicker ~${ref.read(fixedRateExchangeFormProvider).rate!.toStringAsFixed(8)} $toTicker";
final model = IncompleteExchangeModel( final model = IncompleteExchangeModel(
sendTicker: fromTicker, sendTicker: fromTicker,
receiveTicker: toTicker, receiveTicker: toTicker,
rateInfo: rate, rateInfo: rate,
sendAmount: sendAmount, sendAmount: sendAmount,
receiveAmount: receiveAmount: response.value!.toAmount,
response.value!.estimatedAmount,
rateId: response.value!.rateId, rateId: response.value!.rateId,
rateType: rateType, rateType: rateType,
); );

View file

@ -191,7 +191,7 @@ class _TransactionDetailsViewState
), ),
), ),
onPressed: () { onPressed: () {
Navigator.of(context).pop(false); Navigator.of(context).pop(true);
}, },
child: Text( child: Text(
"Continue", "Continue",

View file

@ -6,6 +6,7 @@ import 'package:http/http.dart' as http;
import 'package:stackwallet/external_api_keys.dart'; import 'package:stackwallet/external_api_keys.dart';
import 'package:stackwallet/models/exchange/change_now/available_floating_rate_pair.dart'; import 'package:stackwallet/models/exchange/change_now/available_floating_rate_pair.dart';
import 'package:stackwallet/models/exchange/change_now/change_now_response.dart'; import 'package:stackwallet/models/exchange/change_now/change_now_response.dart';
import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart';
import 'package:stackwallet/models/exchange/change_now/currency.dart'; import 'package:stackwallet/models/exchange/change_now/currency.dart';
import 'package:stackwallet/models/exchange/change_now/estimated_exchange_amount.dart'; import 'package:stackwallet/models/exchange/change_now/estimated_exchange_amount.dart';
import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart';
@ -17,6 +18,7 @@ class ChangeNow {
static const String scheme = "https"; static const String scheme = "https";
static const String authority = "api.changenow.io"; static const String authority = "api.changenow.io";
static const String apiVersion = "/v1"; static const String apiVersion = "/v1";
static const String apiVersionV2 = "/v2";
ChangeNow._(); ChangeNow._();
static final ChangeNow _instance = ChangeNow._(); static final ChangeNow _instance = ChangeNow._();
@ -29,6 +31,10 @@ class ChangeNow {
return Uri.https(authority, apiVersion + path, params); return Uri.https(authority, apiVersion + path, params);
} }
Uri _buildUriV2(String path, Map<String, dynamic>? params) {
return Uri.https(authority, apiVersionV2 + path, params);
}
Future<dynamic> _makeGetRequest(Uri uri) async { Future<dynamic> _makeGetRequest(Uri uri) async {
final client = this.client ?? http.Client(); final client = this.client ?? http.Client();
try { try {
@ -47,6 +53,27 @@ class ChangeNow {
} }
} }
Future<dynamic> _makeGetRequestV2(Uri uri, String apiKey) async {
final client = this.client ?? http.Client();
try {
final response = await client.get(
uri,
headers: {
// 'Content-Type': 'application/json',
'x-changenow-api-key': apiKey,
},
);
final parsed = jsonDecode(response.body);
return parsed;
} catch (e, s) {
Logging.instance
.log("_makeRequestV2($uri) threw: $e\n$s", level: LogLevel.Error);
rethrow;
}
}
Future<dynamic> _makePostRequest( Future<dynamic> _makePostRequest(
Uri uri, Uri uri,
Map<String, String> body, Map<String, String> body,
@ -283,37 +310,109 @@ class ChangeNow {
} }
} }
// old v1 version
/// This API endpoint returns fixed-rate estimated exchange amount of /// This API endpoint returns fixed-rate estimated exchange amount of
/// [toTicker] cryptocurrency to receive for [fromAmount] of [fromTicker] /// [toTicker] cryptocurrency to receive for [fromAmount] of [fromTicker]
Future<ChangeNowResponse<EstimatedExchangeAmount>> // Future<ChangeNowResponse<EstimatedExchangeAmount>>
getEstimatedFixedRateExchangeAmount({ // getEstimatedFixedRateExchangeAmount({
// required String fromTicker,
// required String toTicker,
// required Decimal fromAmount,
// // (Optional) Use rateId for fixed-rate flow. If this field is true, you
// // could use returned field "rateId" in next method for creating transaction
// // to freeze estimated amount that you got in this method. Current estimated
// // amount would be valid until time in field "validUntil"
// bool useRateId = true,
// String? apiKey,
// }) async {
// Map<String, dynamic> params = {
// "api_key": apiKey ?? kChangeNowApiKey,
// "useRateId": useRateId.toString(),
// };
//
// final uri = _buildUri(
// "/exchange-amount/fixed-rate/${fromAmount.toString()}/${fromTicker}_$toTicker",
// params,
// );
//
// try {
// // simple json object is expected here
// final json = await _makeGetRequest(uri);
//
// try {
// final value = EstimatedExchangeAmount.fromJson(
// Map<String, dynamic>.from(json as Map));
// return ChangeNowResponse(value: value);
// } catch (_) {
// return ChangeNowResponse(
// exception: ChangeNowException(
// "Failed to serialize $json",
// ChangeNowExceptionType.serializeResponseError,
// ),
// );
// }
// } catch (e, s) {
// Logging.instance.log(
// "getEstimatedFixedRateExchangeAmount exception: $e\n$s",
// level: LogLevel.Error);
// return ChangeNowResponse(
// exception: ChangeNowException(
// e.toString(),
// ChangeNowExceptionType.generic,
// ),
// );
// }
// }
/// Get estimated amount of [toTicker] cryptocurrency to receive
/// for [fromAmount] of [fromTicker]
Future<ChangeNowResponse<CNExchangeEstimate>> getEstimatedExchangeAmountV2({
required String fromTicker, required String fromTicker,
required String toTicker, required String toTicker,
required Decimal fromAmount, required CNEstimateType fromOrTo,
// (Optional) Use rateId for fixed-rate flow. If this field is true, you required Decimal amount,
// could use returned field "rateId" in next method for creating transaction String? fromNetwork,
// to freeze estimated amount that you got in this method. Current estimated String? toNetwork,
// amount would be valid until time in field "validUntil" CNFlowType flow = CNFlowType.standard,
bool useRateId = true,
String? apiKey, String? apiKey,
}) async { }) async {
Map<String, dynamic> params = { Map<String, dynamic>? params = {
"api_key": apiKey ?? kChangeNowApiKey, "fromCurrency": fromTicker,
"useRateId": useRateId.toString(), "toCurrency": toTicker,
"flow": flow.value,
"type": fromOrTo.name,
}; };
final uri = _buildUri( switch (fromOrTo) {
"/exchange-amount/fixed-rate/${fromAmount.toString()}/${fromTicker}_$toTicker", case CNEstimateType.direct:
params, params["fromAmount"] = amount.toString();
); break;
case CNEstimateType.reverse:
params["toAmount"] = amount.toString();
break;
}
if (fromNetwork != null) {
params["fromNetwork"] = fromNetwork;
}
if (toNetwork != null) {
params["toNetwork"] = toNetwork;
}
if (flow == CNFlowType.fixedRate) {
params["useRateId"] = "true";
}
final uri = _buildUriV2("/exchange/estimated-amount", params);
try { try {
// simple json object is expected here // simple json object is expected here
final json = await _makeGetRequest(uri); final json = await _makeGetRequestV2(uri, apiKey ?? kChangeNowApiKey);
try { try {
final value = EstimatedExchangeAmount.fromJson( final value =
Map<String, dynamic>.from(json as Map)); CNExchangeEstimate.fromJson(Map<String, dynamic>.from(json as Map));
return ChangeNowResponse(value: value); return ChangeNowResponse(value: value);
} catch (_) { } catch (_) {
return ChangeNowResponse( return ChangeNowResponse(
@ -324,8 +423,7 @@ class ChangeNow {
); );
} }
} catch (e, s) { } catch (e, s) {
Logging.instance.log( Logging.instance.log("getEstimatedExchangeAmountV2 exception: $e\n$s",
"getEstimatedFixedRateExchangeAmount exception: $e\n$s",
level: LogLevel.Error); level: LogLevel.Error);
return ChangeNowResponse( return ChangeNowResponse(
exception: ChangeNowException( exception: ChangeNowException(

View file

@ -731,8 +731,10 @@ Future<void> _setTestnetWrapper(bool isTestnet) async {
// setTestnet(isTestnet); // setTestnet(isTestnet);
} }
Future<dynamic> getAnonymity(int groupID) async { Future<Map<String, dynamic>?> getInitialAnonymitySetCache(
Logging.instance.log("getAnonymity", level: LogLevel.Info); String groupID,
) async {
Logging.instance.log("getInitialAnonymitySetCache", level: LogLevel.Info);
final Client client = Client(); final Client client = Client();
try { try {
final uri = Uri.parse("$kStackCommunityNodesEndpoint/getAnonymity"); final uri = Uri.parse("$kStackCommunityNodesEndpoint/getAnonymity");
@ -743,26 +745,22 @@ Future<dynamic> getAnonymity(int groupID) async {
body: jsonEncode({ body: jsonEncode({
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": "0", "id": "0",
'aset': groupID.toString(), 'aset': groupID,
}), }),
); );
// TODO: should the following be removed for security reasons in production?
Logging.instance
.log(anonSetResult.statusCode.toString(), level: LogLevel.Info);
Logging.instance.log(anonSetResult.body.toString(), level: LogLevel.Info);
final response = jsonDecode(anonSetResult.body.toString()); final response = jsonDecode(anonSetResult.body.toString());
if (response['status'] == 'success') { if (response['status'] == 'success') {
final anonResponse = jsonDecode(response['result'] as String); final anonResponse = jsonDecode(response['result'] as String);
Logging.instance.log(anonResponse, level: LogLevel.Info); final setData = Map<String, dynamic>.from(anonResponse["result"] as Map);
return response; return setData;
} else { } else {
return false; return null;
} }
} catch (e, s) { } catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Error); Logging.instance.log("$e $s", level: LogLevel.Error);
return false; return null;
} }
} }

View file

@ -10,7 +10,7 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
import 'cached_electrumx_test.mocks.dart'; import 'cached_electrumx_test.mocks.dart';
import 'sample_data/get_anonymity_set_sample_data.dart'; // import 'sample_data/get_anonymity_set_sample_data.dart';
@GenerateMocks([ElectrumX, Prefs]) @GenerateMocks([ElectrumX, Prefs])
void main() { void main() {
@ -23,36 +23,36 @@ void main() {
await Hive.openBox<dynamic>(DB.instance.boxNameTxCache(coin: Coin.firo)); await Hive.openBox<dynamic>(DB.instance.boxNameTxCache(coin: Coin.firo));
}); });
group("getAnonymitySet", () { group("getAnonymitySet", () {
test("empty set cache call", () async { // test("empty set cache call", () async {
final client = MockElectrumX(); // final client = MockElectrumX();
when( // when(
client.getAnonymitySet( // client.getAnonymitySet(
groupId: "1", // groupId: "1",
blockhash: "", // blockhash: "",
), // ),
).thenAnswer( // ).thenAnswer(
(_) async => GetAnonymitySetSampleData.data, // (_) async => GetAnonymitySetSampleData.data,
); // );
//
final cachedClient = CachedElectrumX( // final cachedClient = CachedElectrumX(
electrumXClient: client, // electrumXClient: client,
port: 0, // port: 0,
failovers: [], // failovers: [],
server: '', // server: '',
useSSL: true, // useSSL: true,
prefs: Prefs.instance); // prefs: Prefs.instance);
//
final result = await cachedClient.getAnonymitySet( // final result = await cachedClient.getAnonymitySet(
groupId: "1", // groupId: "1",
coin: Coin.firo, // coin: Coin.firo,
); // );
//
final expected = // final expected =
Map<String, dynamic>.from(GetAnonymitySetSampleData.data); // Map<String, dynamic>.from(GetAnonymitySetSampleData.data);
expected["setId"] = "1"; // expected["setId"] = "1";
//
expect(result, expected); // expect(result, expected);
}); // });
// //
// test("use and update set cache call", () async { // test("use and update set cache call", () async {
// final storedData = Map.from(GetAnonymitySetSampleData.initialData); // final storedData = Map.from(GetAnonymitySetSampleData.initialData);
@ -91,30 +91,30 @@ void main() {
// fail("This test needs updating"); // fail("This test needs updating");
// }); // });
test("getAnonymitySet throws", () async { // test("getAnonymitySet throws", () async {
final client = MockElectrumX(); // final client = MockElectrumX();
when( // when(
client.getAnonymitySet( // client.getAnonymitySet(
groupId: "1", // groupId: "1",
blockhash: "", // blockhash: "",
), // ),
).thenThrow(Exception()); // ).thenThrow(Exception());
//
final cachedClient = CachedElectrumX( // final cachedClient = CachedElectrumX(
electrumXClient: client, // electrumXClient: client,
port: 0, // port: 0,
failovers: [], // failovers: [],
server: '', // server: '',
useSSL: true, // useSSL: true,
prefs: Prefs.instance); // prefs: Prefs.instance);
//
expect( // expect(
() async => await cachedClient.getAnonymitySet( // () async => await cachedClient.getAnonymitySet(
groupId: "1", // groupId: "1",
coin: Coin.firo, // coin: Coin.firo,
), // ),
throwsA(isA<Exception>())); // throwsA(isA<Exception>()));
}); // });
}); });
test("getTransaction throws", () async { test("getTransaction throws", () async {

View file

@ -339,80 +339,80 @@ void main() {
}); });
}); });
group("getEstimatedFixedRateExchangeAmount", () { // group("getEstimatedFixedRateExchangeAmount", () {
test("getEstimatedFixedRateExchangeAmount succeeds", () async { // test("getEstimatedFixedRateExchangeAmount succeeds", () async {
final client = MockClient(); // final client = MockClient();
ChangeNow.instance.client = client; // ChangeNow.instance.client = client;
//
when(client.get( // when(client.get(
Uri.parse( // Uri.parse(
"https://api.ChangeNow.io/v1/exchange-amount/fixed-rate/10/xmr_btc?api_key=testAPIKEY&useRateId=true"), // "https://api.ChangeNow.io/v1/exchange-amount/fixed-rate/10/xmr_btc?api_key=testAPIKEY&useRateId=true"),
headers: {'Content-Type': 'application/json'}, // headers: {'Content-Type': 'application/json'},
)).thenAnswer((realInvocation) async => // )).thenAnswer((realInvocation) async =>
Response(jsonEncode(estFixedRateExchangeAmountJSON), 200)); // Response(jsonEncode(estFixedRateExchangeAmountJSON), 200));
//
final result = // final result =
await ChangeNow.instance.getEstimatedFixedRateExchangeAmount( // await ChangeNow.instance.getEstimatedFixedRateExchangeAmount(
fromTicker: "xmr", // fromTicker: "xmr",
toTicker: "btc", // toTicker: "btc",
fromAmount: Decimal.fromInt(10), // fromAmount: Decimal.fromInt(10),
apiKey: "testAPIKEY", // apiKey: "testAPIKEY",
); // );
//
expect(result.exception, null); // expect(result.exception, null);
expect(result.value == null, false); // expect(result.value == null, false);
expect(result.value.toString(), // expect(result.value.toString(),
'EstimatedExchangeAmount: {estimatedAmount: 0.07271053, transactionSpeedForecast: 10-60, warningMessage: null, rateId: 1t2W5KBPqhycSJVYpaNZzYWLfMr0kSFe, networkFee: 0.00002408}'); // 'EstimatedExchangeAmount: {estimatedAmount: 0.07271053, transactionSpeedForecast: 10-60, warningMessage: null, rateId: 1t2W5KBPqhycSJVYpaNZzYWLfMr0kSFe, networkFee: 0.00002408}');
}); // });
//
test( // test(
"getEstimatedFixedRateExchangeAmount fails with ChangeNowExceptionType.serializeResponseError", // "getEstimatedFixedRateExchangeAmount fails with ChangeNowExceptionType.serializeResponseError",
() async { // () async {
final client = MockClient(); // final client = MockClient();
ChangeNow.instance.client = client; // ChangeNow.instance.client = client;
//
when(client.get( // when(client.get(
Uri.parse( // Uri.parse(
"https://api.ChangeNow.io/v1/exchange-amount/fixed-rate/10/xmr_btc?api_key=testAPIKEY&useRateId=true"), // "https://api.ChangeNow.io/v1/exchange-amount/fixed-rate/10/xmr_btc?api_key=testAPIKEY&useRateId=true"),
headers: {'Content-Type': 'application/json'}, // headers: {'Content-Type': 'application/json'},
)).thenAnswer((realInvocation) async => Response('{"error": 42}', 200)); // )).thenAnswer((realInvocation) async => Response('{"error": 42}', 200));
//
final result = // final result =
await ChangeNow.instance.getEstimatedFixedRateExchangeAmount( // await ChangeNow.instance.getEstimatedFixedRateExchangeAmount(
fromTicker: "xmr", // fromTicker: "xmr",
toTicker: "btc", // toTicker: "btc",
fromAmount: Decimal.fromInt(10), // fromAmount: Decimal.fromInt(10),
apiKey: "testAPIKEY", // apiKey: "testAPIKEY",
); // );
//
expect(result.exception!.type, // expect(result.exception!.type,
ChangeNowExceptionType.serializeResponseError); // ChangeNowExceptionType.serializeResponseError);
expect(result.value == null, true); // expect(result.value == null, true);
}); // });
//
test("getEstimatedFixedRateExchangeAmount fails for any other reason", // test("getEstimatedFixedRateExchangeAmount fails for any other reason",
() async { // () async {
final client = MockClient(); // final client = MockClient();
ChangeNow.instance.client = client; // ChangeNow.instance.client = client;
//
when(client.get( // when(client.get(
Uri.parse( // Uri.parse(
"https://api.ChangeNow.io/v1/exchange-amount/fixed-rate/10/xmr_btc?api_key=testAPIKEY&useRateId=true"), // "https://api.ChangeNow.io/v1/exchange-amount/fixed-rate/10/xmr_btc?api_key=testAPIKEY&useRateId=true"),
headers: {'Content-Type': 'application/json'}, // headers: {'Content-Type': 'application/json'},
)).thenAnswer((realInvocation) async => Response('', 400)); // )).thenAnswer((realInvocation) async => Response('', 400));
//
final result = // final result =
await ChangeNow.instance.getEstimatedFixedRateExchangeAmount( // await ChangeNow.instance.getEstimatedFixedRateExchangeAmount(
fromTicker: "xmr", // fromTicker: "xmr",
toTicker: "btc", // toTicker: "btc",
fromAmount: Decimal.fromInt(10), // fromAmount: Decimal.fromInt(10),
apiKey: "testAPIKEY", // apiKey: "testAPIKEY",
); // );
//
expect(result.exception!.type, ChangeNowExceptionType.generic); // expect(result.exception!.type, ChangeNowExceptionType.generic);
expect(result.value == null, true); // expect(result.value == null, true);
}); // });
}); // });
group("getAvailableFixedRateMarkets", () { group("getAvailableFixedRateMarkets", () {
test("getAvailableFixedRateMarkets succeeds", () async { test("getAvailableFixedRateMarkets succeeds", () async {