WIP exchange process refactor to handle multiple sub providers per exchange provider

This commit is contained in:
julian 2023-05-01 16:26:12 -06:00
parent 34f7d80051
commit e81521e374
14 changed files with 468 additions and 957 deletions

View file

@ -13,6 +13,12 @@ class ExchangeFormState extends ChangeNotifier {
Exchange? _exchange;
Exchange get exchange => _exchange ??= Exchange.defaultExchange;
String? _providerName;
// default to exchange name that isn't trocador
String get providerName => _providerName ??= Exchange.defaultExchange.name;
String get combinedExchangeId => "${exchange.name} ($providerName)";
ExchangeRateType _exchangeRateType = ExchangeRateType.estimated;
ExchangeRateType get exchangeRateType => _exchangeRateType;
set exchangeRateType(ExchangeRateType exchangeRateType) {
@ -20,8 +26,8 @@ class ExchangeFormState extends ChangeNotifier {
//
}
Estimate? _estimate;
Estimate? get estimate => _estimate;
List<Estimate> _estimates = [];
List<Estimate> get estimates => _estimates;
bool _reversed = false;
bool get reversed => _reversed;
@ -149,10 +155,12 @@ class ExchangeFormState extends ChangeNotifier {
Future<void> updateExchange({
required Exchange exchange,
required String providerName,
required bool shouldUpdateData,
required bool shouldNotifyListeners,
}) async {
_exchange = exchange;
_providerName = providerName;
if (shouldUpdateData) {
await _updateRangesAndEstimate(
shouldNotifyListeners: false,
@ -173,6 +181,7 @@ class ExchangeFormState extends ChangeNotifier {
required bool shouldNotifyListeners,
}) {
_exchange = null;
_providerName = null;
_reversed = false;
_rate = null;
_sendAmount = null;
@ -445,7 +454,7 @@ class ExchangeFormState extends ChangeNotifier {
);
return;
}
final response = await exchange.getEstimate(
final response = await exchange.getEstimates(
sendCurrency!.ticker,
receiveCurrency!.ticker,
amount,
@ -461,12 +470,16 @@ class ExchangeFormState extends ChangeNotifier {
return;
}
_estimate = response.value!;
_estimates = response.value!;
if (reversed) {
_sendAmount = _estimate!.estimatedAmount;
_sendAmount = _estimates
.firstWhere((e) => e.exchangeProvider == providerName)
.estimatedAmount;
} else {
_receiveAmount = _estimate!.estimatedAmount;
_receiveAmount = _estimates
.firstWhere((e) => e.exchangeProvider == providerName)
.estimatedAmount;
}
_rate =
@ -518,7 +531,7 @@ class ExchangeFormState extends ChangeNotifier {
"\n\t reversed: $reversed,"
"\n\t sendAmount: $sendAmount,"
"\n\t receiveAmount: $receiveAmount,"
"\n\t estimate: $estimate,"
"\n\t estimates: $estimates,"
"\n\t minSendAmount: $minSendAmount,"
"\n\t maxSendAmount: $maxSendAmount,"
"\n\t minReceiveAmount: $minReceiveAmount,"

View file

@ -7,7 +7,8 @@ class Estimate {
final bool reversed;
final String? warningMessage;
final String? rateId;
final String? exchangeProvider;
final String exchangeProvider;
final String? kycRating;
Estimate({
required this.estimatedAmount,
@ -15,10 +16,15 @@ class Estimate {
required this.reversed,
this.warningMessage,
this.rateId,
this.exchangeProvider,
required this.exchangeProvider,
this.kycRating,
});
factory Estimate.fromMap(Map<String, dynamic> map) {
factory Estimate.fromMap(
Map<String, dynamic> map, {
required String exchangeProvider,
String? kycRating,
}) {
try {
return Estimate(
estimatedAmount: Decimal.parse(map["estimatedAmount"] as String),
@ -26,6 +32,8 @@ class Estimate {
reversed: map["reversed"] as bool,
warningMessage: map["warningMessage"] as String?,
rateId: map["rateId"] as String?,
exchangeProvider: exchangeProvider,
kycRating: kycRating,
);
} catch (e, s) {
Logging.instance.log("Estimate.fromMap(): $e\n$s", level: LogLevel.Error);
@ -41,6 +49,7 @@ class Estimate {
"warningMessage": warningMessage,
"rateId": rateId,
"exchangeProvider": exchangeProvider,
"kycRating": kycRating,
};
}

View file

@ -409,7 +409,11 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
final fromTicker = ref.read(exchangeFormStateProvider).fromTicker ?? "";
final toTicker = ref.read(exchangeFormStateProvider).toTicker ?? "";
final sendAmount = ref.read(exchangeFormStateProvider).sendAmount!;
final estimate = ref.read(exchangeFormStateProvider).estimate!;
final estimate = ref.read(exchangeFormStateProvider).estimates.firstWhere(
(e) =>
e.exchangeProvider ==
ref.read(exchangeFormStateProvider).providerName,
);
if (rateType == ExchangeRateType.fixed && toTicker.toUpperCase() == "WOW") {
await showDialog<void>(

View file

@ -0,0 +1,306 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/exceptions/exchange/pair_unavailable_exception.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/providers/exchange/exchange_form_state_provider.dart';
import 'package:stackwallet/providers/global/locale_provider.dart';
import 'package:stackwallet/services/exchange/exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/animated_text.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/exchange/trocador/trocador_kyc_info_button.dart';
import 'package:stackwallet/widgets/exchange/trocador/trocador_rating_type_enum.dart';
class ExchangeProviderOption extends ConsumerStatefulWidget {
const ExchangeProviderOption({
Key? key,
required this.exchange,
required this.exchangeProvider,
required this.fixedRate,
required this.reversed,
}) : super(key: key);
final Exchange exchange;
final String exchangeProvider;
final bool fixedRate;
final bool reversed;
@override
ConsumerState<ExchangeProviderOption> createState() =>
_ExchangeProviderOptionState();
}
class _ExchangeProviderOptionState
extends ConsumerState<ExchangeProviderOption> {
final isDesktop = Util.isDesktop;
late final String _id;
@override
void initState() {
_id = "${widget.exchange.name} (${widget.exchangeProvider})";
super.initState();
}
@override
Widget build(BuildContext context) {
final sendCurrency = ref
.watch(exchangeFormStateProvider.select((value) => value.sendCurrency));
final receivingCurrency = ref.watch(
exchangeFormStateProvider.select((value) => value.receiveCurrency));
final fromAmount = ref
.watch(exchangeFormStateProvider.select((value) => value.sendAmount));
final toAmount = ref.watch(
exchangeFormStateProvider.select((value) => value.receiveAmount));
final selected = ref.watch(exchangeFormStateProvider
.select((value) => value.combinedExchangeId)) ==
_id;
return ConditionalParent(
condition: isDesktop,
builder: (child) => MouseRegion(
cursor: SystemMouseCursors.click,
child: child,
),
child: GestureDetector(
onTap: () {
if (!selected) {
showLoading(
whileFuture: ref.read(exchangeFormStateProvider).updateExchange(
exchange: widget.exchange,
shouldUpdateData: true,
shouldNotifyListeners: true,
providerName: widget.exchangeProvider,
),
context: context,
message: "Updating rates",
isDesktop: isDesktop,
);
}
},
child: Container(
color: Colors.transparent,
child: Padding(
padding:
isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Padding(
padding: EdgeInsets.only(top: isDesktop ? 20.0 : 15.0),
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: _id,
groupValue: ref.watch(exchangeFormStateProvider
.select((value) => value.combinedExchangeId)),
onChanged: (_) {
if (!selected) {
ref.read(exchangeFormStateProvider).updateExchange(
exchange: widget.exchange,
shouldUpdateData: true,
shouldNotifyListeners: true,
providerName: widget.exchangeProvider,
);
}
},
),
),
),
const SizedBox(
width: 14,
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: SizedBox(
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
child: SvgPicture.asset(
Assets.exchange.getIconFor(
exchangeName: widget.exchange.name,
),
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
),
),
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.exchangeProvider,
style: STextStyles.titleBold12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark2,
),
),
if (sendCurrency != null &&
receivingCurrency != null &&
toAmount != null &&
toAmount > Decimal.zero &&
fromAmount != null &&
fromAmount > Decimal.zero)
FutureBuilder(
future: widget.exchange.getEstimates(
sendCurrency.ticker,
receivingCurrency.ticker,
widget.reversed ? toAmount : fromAmount,
widget.fixedRate,
widget.reversed,
),
builder: (context,
AsyncSnapshot<ExchangeResponse<List<Estimate>>>
snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
final estimates = snapshot.data?.value;
if (estimates != null &&
estimates
.where((e) =>
e.exchangeProvider ==
widget.exchangeProvider)
.isNotEmpty) {
final estimate = estimates.firstWhere((e) =>
e.exchangeProvider ==
widget.exchangeProvider);
int decimals;
try {
decimals = coinFromTickerCaseInsensitive(
receivingCurrency.ticker)
.decimals;
} catch (_) {
decimals = 8; // some reasonable alternative
}
Amount rate;
if (estimate.reversed) {
rate = (toAmount / estimate.estimatedAmount)
.toDecimal(scaleOnInfinitePrecision: 18)
.toAmount(fractionDigits: decimals);
} else {
rate = (estimate.estimatedAmount / fromAmount)
.toDecimal(scaleOnInfinitePrecision: 18)
.toAmount(fractionDigits: decimals);
}
return ConditionalParent(
condition: widget.exchange.name ==
TrocadorExchange.exchangeName,
builder: (child) {
return Row(
children: [
child,
TrocadorKYCInfoButton(
kycType: TrocadorKYCType.fromString(
estimate.kycRating!,
),
),
],
);
},
child: Text(
"1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed(
locale: ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
),
)} ${receivingCurrency.ticker.toUpperCase()}",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
);
} else if (snapshot.data?.exception
is PairUnavailableException) {
return Text(
"Unsupported pair",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else {
Logging.instance.log(
"$runtimeType failed to fetch rate for $_id}: ${snapshot.data}",
level: LogLevel.Warning,
);
return Text(
"Failed to fetch rate",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Loading",
"Loading.",
"Loading..",
"Loading...",
],
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
},
),
if (!(sendCurrency != null &&
receivingCurrency != null &&
toAmount != null &&
toAmount > Decimal.zero &&
fromAmount != null &&
fromAmount > Decimal.zero))
Text(
"n/a",
style: STextStyles.itemSubtitle12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
],
),
),
),
),
);
}
}

View file

@ -1,27 +1,13 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/exceptions/exchange/pair_unavailable_exception.dart';
import 'package:stackwallet/models/exchange/aggregate_currency.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_provider_option.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart';
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/animated_text.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/exchange/trocador/trocador_kyc_info_button.dart';
import 'package:stackwallet/widgets/exchange/trocador/trocador_rating_type_enum.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class ExchangeProviderOptions extends ConsumerStatefulWidget {
@ -63,11 +49,16 @@ class _ExchangeProviderOptionsState
@override
Widget build(BuildContext context) {
final sendCurrency = ref.watch(exchangeFormStateProvider).sendCurrency;
final receivingCurrency =
ref.watch(exchangeFormStateProvider).receiveCurrency;
final fromAmount = ref.watch(exchangeFormStateProvider).sendAmount;
final toAmount = ref.watch(exchangeFormStateProvider).receiveAmount;
final sendCurrency = ref.watch(
exchangeFormStateProvider.select(
(value) => value.sendCurrency,
),
);
final receivingCurrency = ref.watch(
exchangeFormStateProvider.select(
(value) => value.receiveCurrency,
),
);
final showChangeNow = exchangeSupported(
exchangeName: ChangeNowExchange.exchangeName,
@ -93,239 +84,12 @@ class _ExchangeProviderOptionsState
child: Column(
children: [
if (showChangeNow)
ConditionalParent(
condition: isDesktop,
builder: (child) => MouseRegion(
cursor: SystemMouseCursors.click,
child: child,
),
child: GestureDetector(
onTap: () {
if (ref.read(exchangeFormStateProvider).exchange.name !=
ChangeNowExchange.exchangeName) {
showLoading(
whileFuture:
ref.read(exchangeFormStateProvider).updateExchange(
exchange: ChangeNowExchange.instance,
shouldUpdateData: true,
shouldNotifyListeners: true,
),
context: context,
message: "Updating rates",
isDesktop: isDesktop,
);
}
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Padding(
padding:
EdgeInsets.only(top: isDesktop ? 20.0 : 15.0),
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: ChangeNowExchange.exchangeName,
groupValue: ref.watch(exchangeFormStateProvider
.select((value) => value.exchange.name)),
onChanged: (_) {
if (ref
.read(exchangeFormStateProvider)
.exchange
.name !=
ChangeNowExchange.exchangeName) {
ref
.read(exchangeFormStateProvider)
.updateExchange(
exchange: ChangeNowExchange.instance,
shouldUpdateData: true,
shouldNotifyListeners: true,
);
}
},
),
),
),
const SizedBox(
width: 14,
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: SizedBox(
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
child: SvgPicture.asset(
Assets.exchange.changeNow,
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
),
),
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ChangeNowExchange.exchangeName,
style:
STextStyles.titleBold12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark2,
),
),
if (sendCurrency != null &&
receivingCurrency != null &&
toAmount != null &&
toAmount > Decimal.zero &&
fromAmount != null &&
fromAmount > Decimal.zero)
FutureBuilder(
future:
ChangeNowExchange.instance.getEstimate(
sendCurrency.ticker,
receivingCurrency.ticker,
widget.reversed ? toAmount : fromAmount,
widget.fixedRate,
widget.reversed,
),
builder: (context,
AsyncSnapshot<ExchangeResponse<Estimate>>
snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
final estimate = snapshot.data?.value;
if (estimate != null) {
Coin coin;
try {
coin = coinFromTickerCaseInsensitive(
receivingCurrency.ticker);
} catch (_) {
coin = Coin.bitcoin;
}
Amount rate;
if (estimate.reversed) {
rate = (toAmount /
estimate.estimatedAmount)
.toDecimal(
scaleOnInfinitePrecision: 18)
.toAmount(
fractionDigits:
coin.decimals);
} else {
rate = (estimate.estimatedAmount /
fromAmount)
.toDecimal(
scaleOnInfinitePrecision: 18)
.toAmount(
fractionDigits:
coin.decimals);
}
return Text(
"1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed(
locale: ref.watch(
localeServiceChangeNotifierProvider
.select(
(value) => value.locale),
),
)} ${receivingCurrency.ticker.toUpperCase()}",
style: STextStyles.itemSubtitle12(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else if (snapshot.data?.exception
is PairUnavailableException) {
return Text(
"Unsupported pair",
style: STextStyles.itemSubtitle12(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else {
Logging.instance.log(
"$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}",
level: LogLevel.Warning,
);
return Text(
"Failed to fetch rate",
style: STextStyles.itemSubtitle12(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Loading",
"Loading.",
"Loading..",
"Loading...",
],
style:
STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
},
),
if (!(sendCurrency != null &&
receivingCurrency != null &&
toAmount != null &&
toAmount > Decimal.zero &&
fromAmount != null &&
fromAmount > Decimal.zero))
Text(
"n/a",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
],
),
),
),
),
ExchangeProviderOption(
exchange: ChangeNowExchange.instance,
exchangeProvider: ChangeNowExchange.exchangeName,
fixedRate: widget.fixedRate,
reversed: widget.reversed,
),
if (showChangeNow && showMajesticBank)
isDesktop
? Container(
@ -336,239 +100,12 @@ class _ExchangeProviderOptionsState
: const SizedBox(
height: 16,
),
if (showMajesticBank)
ConditionalParent(
condition: isDesktop,
builder: (child) => MouseRegion(
cursor: SystemMouseCursors.click,
child: child,
),
child: GestureDetector(
onTap: () {
if (ref.read(exchangeFormStateProvider).exchange.name !=
MajesticBankExchange.exchangeName) {
showLoading(
whileFuture:
ref.read(exchangeFormStateProvider).updateExchange(
exchange: MajesticBankExchange.instance,
shouldUpdateData: true,
shouldNotifyListeners: true,
),
context: context,
isDesktop: isDesktop,
message: "Updating rates",
);
}
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Padding(
padding:
EdgeInsets.only(top: isDesktop ? 20.0 : 15.0),
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: MajesticBankExchange.exchangeName,
groupValue: ref.watch(exchangeFormStateProvider
.select((value) => value.exchange.name)),
onChanged: (_) {
if (ref
.read(exchangeFormStateProvider)
.exchange
.name !=
MajesticBankExchange.exchangeName) {
ref
.read(exchangeFormStateProvider)
.updateExchange(
exchange: MajesticBankExchange.instance,
shouldUpdateData: true,
shouldNotifyListeners: true,
);
}
},
),
),
),
const SizedBox(
width: 14,
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: SizedBox(
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
child: SvgPicture.asset(
Assets.exchange.majesticBankBlue,
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
),
),
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
MajesticBankExchange.exchangeName,
style:
STextStyles.titleBold12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark2,
),
),
if (sendCurrency != null &&
receivingCurrency != null &&
toAmount != null &&
toAmount > Decimal.zero &&
fromAmount != null &&
fromAmount > Decimal.zero)
FutureBuilder(
future:
MajesticBankExchange.instance.getEstimate(
sendCurrency.ticker,
receivingCurrency.ticker,
widget.reversed ? toAmount : fromAmount,
widget.fixedRate,
widget.reversed,
),
builder: (context,
AsyncSnapshot<ExchangeResponse<Estimate>>
snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
final estimate = snapshot.data?.value;
if (estimate != null) {
Coin coin;
try {
coin = coinFromTickerCaseInsensitive(
receivingCurrency.ticker);
} catch (_) {
coin = Coin.bitcoin;
}
Amount rate;
if (estimate.reversed) {
rate = (toAmount /
estimate.estimatedAmount)
.toDecimal(
scaleOnInfinitePrecision: 18)
.toAmount(
fractionDigits: coin.decimals,
);
} else {
rate = (estimate.estimatedAmount /
fromAmount)
.toDecimal(
scaleOnInfinitePrecision: 18)
.toAmount(
fractionDigits: coin.decimals,
);
}
return Text(
"1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed(
locale: ref.watch(
localeServiceChangeNotifierProvider
.select(
(value) => value.locale),
),
)} ${receivingCurrency.ticker.toUpperCase()}",
style: STextStyles.itemSubtitle12(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else if (snapshot.data?.exception
is PairUnavailableException) {
return Text(
"Unsupported pair",
style: STextStyles.itemSubtitle12(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else {
Logging.instance.log(
"$runtimeType failed to fetch rate for Majestic Bank: ${snapshot.data}",
level: LogLevel.Warning,
);
return Text(
"Failed to fetch rate",
style: STextStyles.itemSubtitle12(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Loading",
"Loading.",
"Loading..",
"Loading...",
],
style:
STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
},
),
if (!(sendCurrency != null &&
receivingCurrency != null &&
toAmount != null &&
toAmount > Decimal.zero &&
fromAmount != null &&
fromAmount > Decimal.zero))
Text(
"n/a",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
],
),
),
),
),
ExchangeProviderOption(
exchange: MajesticBankExchange.instance,
exchangeProvider: MajesticBankExchange.exchangeName,
fixedRate: widget.fixedRate,
reversed: widget.reversed,
),
if ((showChangeNow || showMajesticBank) && showTrocador)
isDesktop
@ -580,448 +117,13 @@ class _ExchangeProviderOptionsState
: const SizedBox(
height: 16,
),
if (showTrocador)
ConditionalParent(
condition: isDesktop,
builder: (child) => MouseRegion(
cursor: SystemMouseCursors.click,
child: child,
),
child: GestureDetector(
onTap: () {
if (ref.read(exchangeFormStateProvider).exchange.name !=
TrocadorExchange.exchangeName) {
showLoading(
whileFuture:
ref.read(exchangeFormStateProvider).updateExchange(
exchange: TrocadorExchange.instance,
shouldUpdateData: true,
shouldNotifyListeners: true,
),
context: context,
isDesktop: isDesktop,
message: "Updating rates",
);
}
},
child: Container(
color: Colors.transparent,
child: Padding(
padding: isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 20,
height: 20,
child: Padding(
padding:
EdgeInsets.only(top: isDesktop ? 20.0 : 15.0),
child: Radio(
activeColor: Theme.of(context)
.extension<StackColors>()!
.radioButtonIconEnabled,
value: TrocadorExchange.exchangeName,
groupValue: ref.watch(exchangeFormStateProvider
.select((value) => value.exchange.name)),
onChanged: (_) {
if (ref
.read(exchangeFormStateProvider)
.exchange
.name !=
TrocadorExchange.exchangeName) {
ref
.read(exchangeFormStateProvider)
.updateExchange(
exchange: TrocadorExchange.instance,
shouldUpdateData: true,
shouldNotifyListeners: true,
);
}
},
),
),
),
const SizedBox(
width: 14,
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: SizedBox(
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
child: SvgPicture.asset(
Assets.exchange.trocador,
width: isDesktop ? 32 : 24,
height: isDesktop ? 32 : 24,
),
),
),
const SizedBox(
width: 10,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
TrocadorExchange.exchangeName,
style:
STextStyles.titleBold12(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark2,
),
),
if (sendCurrency != null &&
receivingCurrency != null &&
toAmount != null &&
toAmount > Decimal.zero &&
fromAmount != null &&
fromAmount > Decimal.zero)
FutureBuilder(
future: TrocadorExchange.instance.getEstimate(
sendCurrency.ticker,
receivingCurrency.ticker,
widget.reversed ? toAmount : fromAmount,
widget.fixedRate,
widget.reversed,
),
builder: (context,
AsyncSnapshot<ExchangeResponse<Estimate>>
snapshot) {
if (snapshot.connectionState ==
ConnectionState.done &&
snapshot.hasData) {
final estimate = snapshot.data?.value;
if (estimate != null) {
Coin coin;
try {
coin = coinFromTickerCaseInsensitive(
receivingCurrency.ticker);
} catch (_) {
coin = Coin.bitcoin;
}
Amount rate;
if (estimate.reversed) {
rate = (toAmount /
estimate.estimatedAmount)
.toDecimal(
scaleOnInfinitePrecision: 18)
.toAmount(
fractionDigits: coin.decimals,
);
} else {
rate = (estimate.estimatedAmount /
fromAmount)
.toDecimal(
scaleOnInfinitePrecision: 18)
.toAmount(
fractionDigits: coin.decimals,
);
}
return Text(
"1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed(
locale: ref.watch(
localeServiceChangeNotifierProvider
.select(
(value) => value.locale),
),
)} ${receivingCurrency.ticker.toUpperCase()}",
style: STextStyles.itemSubtitle12(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else if (snapshot.data?.exception
is PairUnavailableException) {
return Text(
"Unsupported pair",
style: STextStyles.itemSubtitle12(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
} else {
Logging.instance.log(
"$runtimeType failed to fetch rate for Trocador: ${snapshot.data}",
level: LogLevel.Warning,
);
return Text(
"Failed to fetch rate",
style: STextStyles.itemSubtitle12(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
} else {
return AnimatedText(
stringsToLoopThrough: const [
"Loading",
"Loading.",
"Loading..",
"Loading...",
],
style:
STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
);
}
},
),
if (!(sendCurrency != null &&
receivingCurrency != null &&
toAmount != null &&
toAmount > Decimal.zero &&
fromAmount != null &&
fromAmount > Decimal.zero))
Text(
"n/a",
style: STextStyles.itemSubtitle12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
],
),
),
const TrocadorKYCInfoButton(
kycType: TrocadorKYCType.a,
),
],
),
),
),
),
ExchangeProviderOption(
fixedRate: widget.fixedRate,
reversed: widget.reversed,
exchange: TrocadorExchange.instance,
exchangeProvider: 'LetsExchange',
),
// if (isDesktop)
// Container(
// height: 1,
// color: Theme.of(context).extension<StackColors>()!.background,
// ),
// if (!isDesktop)
// const SizedBox(
// height: 16,
// ),
// ConditionalParent(
// condition: isDesktop,
// builder: (child) => MouseRegion(
// cursor: SystemMouseCursors.click,
// child: child,
// ),
// child: GestureDetector(
// onTap: () {
// if (ref.read(currentExchangeNameStateProvider.state).state !=
// SimpleSwapExchange.exchangeName) {
// // ref.read(currentExchangeNameStateProvider.state).state =
// // SimpleSwapExchange.exchangeName;
// ref.read(exchangeFormStateProvider).exchange =
// Exchange.fromName(ref
// .read(currentExchangeNameStateProvider.state)
// .state);
// }
// },
// child: Container(
// color: Colors.transparent,
// child: Padding(
// padding: isDesktop
// ? const EdgeInsets.all(16)
// : const EdgeInsets.all(0),
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// SizedBox(
// width: 20,
// height: 20,
// child: Radio(
// activeColor: Theme.of(context)
// .extension<StackColors>()!
// .radioButtonIconEnabled,
// value: SimpleSwapExchange.exchangeName,
// groupValue: ref
// .watch(currentExchangeNameStateProvider.state)
// .state,
// onChanged: (value) {
// if (value is String) {
// ref
// .read(currentExchangeNameStateProvider.state)
// .state = value;
// ref.read(exchangeFormStateProvider).exchange =
// Exchange.fromName(ref
// .read(currentExchangeNameStateProvider
// .state)
// .state);
// }
// },
// ),
// ),
// const SizedBox(
// width: 14,
// ),
// // SvgPicture.asset(
// // Assets.exchange.simpleSwap,
// // width: isDesktop ? 32 : 24,
// // height: isDesktop ? 32 : 24,
// // ),
// // const SizedBox(
// // width: 10,
// // ),
// // Expanded(
// // child: Column(
// // mainAxisAlignment: MainAxisAlignment.start,
// // mainAxisSize: MainAxisSize.min,
// // crossAxisAlignment: CrossAxisAlignment.start,
// // children: [
// // Text(
// // SimpleSwapExchange.exchangeName,
// // style: STextStyles.titleBold12(context).copyWith(
// // color: Theme.of(context)
// // .extension<StackColors>()!
// // .textDark2,
// // ),
// // ),
// // if (from != null &&
// // to != null &&
// // toAmount != null &&
// // toAmount! > Decimal.zero &&
// // fromAmount != null &&
// // fromAmount! > Decimal.zero)
// // FutureBuilder(
// // future: SimpleSwapExchange().getEstimate(
// // from!,
// // to!,
// // // reversed ? toAmount! : fromAmount!,
// // fromAmount!,
// // fixedRate,
// // // reversed,
// // false,
// // ),
// // builder: (context,
// // AsyncSnapshot<ExchangeResponse<Estimate>>
// // snapshot) {
// // if (snapshot.connectionState ==
// // ConnectionState.done &&
// // snapshot.hasData) {
// // final estimate = snapshot.data?.value;
// // if (estimate != null) {
// // Decimal rate = (estimate.estimatedAmount /
// // fromAmount!)
// // .toDecimal(
// // scaleOnInfinitePrecision: 12);
// //
// // Coin coin;
// // try {
// // coin =
// // coinFromTickerCaseInsensitive(to!);
// // } catch (_) {
// // coin = Coin.bitcoin;
// // }
// // return Text(
// // "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed(
// // value: rate,
// // locale: ref.watch(
// // localeServiceChangeNotifierProvider
// // .select(
// // (value) => value.locale),
// // ),
// // decimalPlaces:
// // Constants.decimalPlacesForCoin(
// // coin),
// // )} ${to!.toUpperCase()}",
// // style:
// // STextStyles.itemSubtitle12(context)
// // .copyWith(
// // color: Theme.of(context)
// // .extension<StackColors>()!
// // .textSubtitle1,
// // ),
// // );
// // } else {
// // Logging.instance.log(
// // "$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}",
// // level: LogLevel.Warning,
// // );
// // return Text(
// // "Failed to fetch rate",
// // style:
// // STextStyles.itemSubtitle12(context)
// // .copyWith(
// // color: Theme.of(context)
// // .extension<StackColors>()!
// // .textSubtitle1,
// // ),
// // );
// // }
// // } else {
// // return AnimatedText(
// // stringsToLoopThrough: const [
// // "Loading",
// // "Loading.",
// // "Loading..",
// // "Loading...",
// // ],
// // style: STextStyles.itemSubtitle12(context)
// // .copyWith(
// // color: Theme.of(context)
// // .extension<StackColors>()!
// // .textSubtitle1,
// // ),
// // );
// // }
// // },
// // ),
// // // if (!(from != null &&
// // // to != null &&
// // // (reversed
// // // ? toAmount != null && toAmount! > Decimal.zero
// // // : fromAmount != null &&
// // // fromAmount! > Decimal.zero)))
// // if (!(from != null &&
// // to != null &&
// // toAmount != null &&
// // toAmount! > Decimal.zero &&
// // fromAmount != null &&
// // fromAmount! > Decimal.zero))
// // Text(
// // "n/a",
// // style: STextStyles.itemSubtitle12(context)
// // .copyWith(
// // color: Theme.of(context)
// // .extension<StackColors>()!
// // .textSubtitle1,
// // ),
// // ),
// // ],
// // ),
// // ),
// ],
// ),
// ),
// ),
// ),
// ),
],
),
);

View file

@ -483,6 +483,7 @@ class ChangeNowAPI {
reversed: false,
rateId: value.rateId,
warningMessage: value.warningMessage,
exchangeProvider: ChangeNowExchange.exchangeName,
),
);
} catch (_) {
@ -566,6 +567,7 @@ class ChangeNowAPI {
reversed: reversed,
rateId: value.rateId,
warningMessage: value.warningMessage,
exchangeProvider: ChangeNowExchange.exchangeName,
),
);
} catch (_) {

View file

@ -128,7 +128,7 @@ class ChangeNowExchange extends Exchange {
}
@override
Future<ExchangeResponse<Estimate>> getEstimate(
Future<ExchangeResponse<List<Estimate>>> getEstimates(
String from,
String to,
Decimal amount,
@ -151,7 +151,10 @@ class ChangeNowExchange extends Exchange {
fromAmount: amount,
);
}
return response;
return ExchangeResponse(
value: response.value == null ? null : [response.value!],
exception: response.exception,
);
}
@override

View file

@ -60,7 +60,7 @@ abstract class Exchange {
bool fixedRate,
);
Future<ExchangeResponse<Estimate>> getEstimate(
Future<ExchangeResponse<List<Estimate>>> getEstimates(
String from,
String to,
Decimal amount,

View file

@ -170,7 +170,7 @@ class MajesticBankExchange extends Exchange {
}
@override
Future<ExchangeResponse<Estimate>> getEstimate(
Future<ExchangeResponse<List<Estimate>>> getEstimates(
String from,
String to,
Decimal amount,
@ -192,8 +192,9 @@ class MajesticBankExchange extends Exchange {
estimatedAmount: reversed ? calc.fromAmount : calc.receiveAmount,
fixedRate: fixedRate,
reversed: reversed,
exchangeProvider: MajesticBankExchange.exchangeName,
);
return ExchangeResponse(value: estimate);
return ExchangeResponse(value: [estimate]);
}
@override

View file

@ -89,7 +89,7 @@ class SimpleSwapExchange extends Exchange {
}
@override
Future<ExchangeResponse<Estimate>> getEstimate(
Future<ExchangeResponse<List<Estimate>>> getEstimates(
String from,
String to,
Decimal amount,
@ -109,11 +109,14 @@ class SimpleSwapExchange extends Exchange {
}
return ExchangeResponse(
value: Estimate(
estimatedAmount: Decimal.parse(response.value!),
fixedRate: fixedRate,
reversed: reversed,
),
value: [
Estimate(
estimatedAmount: Decimal.parse(response.value!),
fixedRate: fixedRate,
reversed: reversed,
exchangeProvider: SimpleSwapExchange.exchangeName,
),
],
);
}

View file

@ -5,7 +5,8 @@ class TrocadorQuote {
final String kycRating;
final int insurance;
final bool fixed;
final Decimal amountTo;
final Decimal? amountTo;
final Decimal? amountFrom;
final Decimal waste;
TrocadorQuote({
@ -14,6 +15,7 @@ class TrocadorQuote {
required this.insurance,
required this.fixed,
required this.amountTo,
required this.amountFrom,
required this.waste,
});
@ -24,7 +26,8 @@ class TrocadorQuote {
insurance: map['insurance'] as int,
// wtf trocador?
fixed: map['fixed'] == "True",
amountTo: Decimal.parse(map['amount_to'].toString()),
amountTo: Decimal.tryParse(map['amount_to'].toString()),
amountFrom: Decimal.tryParse(map['amount_from'].toString()),
waste: Decimal.parse(map['waste'].toString()),
);
}
@ -37,6 +40,7 @@ class TrocadorQuote {
'insurance: $insurance, '
'fixed: $fixed, '
'amountTo: $amountTo, '
'amountFrom: $amountFrom, '
'waste: $waste '
')';
}

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:decimal/decimal.dart';
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
@ -8,6 +10,7 @@ import 'package:stackwallet/models/isar/exchange_cache/pair.dart';
import 'package:stackwallet/services/exchange/exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_coin.dart';
import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_quote.dart';
import 'package:stackwallet/services/exchange/trocador/trocador_api.dart';
import 'package:uuid/uuid.dart';
@ -177,7 +180,7 @@ class TrocadorExchange extends Exchange {
}
@override
Future<ExchangeResponse<Estimate>> getEstimate(
Future<ExchangeResponse<List<Estimate>>> getEstimates(
String from,
String to,
Decimal amount,
@ -208,15 +211,47 @@ class TrocadorExchange extends Exchange {
return ExchangeResponse(exception: response.exception);
}
final List<Estimate> estimates = [];
final List<TrocadorQuote> cOrLowerQuotes = [];
for (final quote in response.value!.quotes) {
if (quote.fixed == isPayment) {
final rating = quote.kycRating.toLowerCase();
if (rating == "a" || rating == "b") {
estimates.add(
Estimate(
estimatedAmount: isPayment ? quote.amountFrom! : quote.amountTo!,
fixedRate: quote.fixed,
reversed: isPayment,
exchangeProvider: quote.provider,
rateId: response.value!.tradeId,
kycRating: quote.kycRating,
),
);
} else {
cOrLowerQuotes.add(quote);
}
}
}
cOrLowerQuotes.sort((a, b) => b.waste.compareTo(a.waste));
for (int i = 0; i < max(3, cOrLowerQuotes.length); i++) {
final quote = cOrLowerQuotes[i];
estimates.add(
Estimate(
estimatedAmount: isPayment ? quote.amountFrom! : quote.amountTo!,
fixedRate: quote.fixed,
reversed: isPayment,
exchangeProvider: quote.provider,
rateId: response.value!.tradeId,
kycRating: quote.kycRating,
),
);
}
return ExchangeResponse(
value: Estimate(
estimatedAmount:
isPayment ? response.value!.amountFrom : response.value!.amountTo,
fixedRate: isPayment,
reversed: isPayment,
exchangeProvider: response.value!.provider,
rateId: response.value!.tradeId,
),
value: estimates,
);
}

View file

@ -1,5 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart';
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart';
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/theme/color_theme.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
@ -64,6 +68,22 @@ class _EXCHANGE {
String get majesticBankBlue => "${_path}mb_blue.svg";
String get majesticBankGreen => "${_path}mb_green.svg";
String get trocador => "${_path}trocador.svg";
String getIconFor({required String exchangeName}) {
switch (exchangeName) {
case SimpleSwapExchange.exchangeName:
return simpleSwap;
case ChangeNowExchange.exchangeName:
return changeNow;
case MajesticBankExchange.exchangeName:
return majesticBankBlue;
case TrocadorExchange.exchangeName:
return trocador;
default:
throw ArgumentError("Invalid exchange name passed to "
"Assets.exchange.getIconFor()");
}
}
}
class _BUY {

View file

@ -3,4 +3,13 @@ enum TrocadorKYCType {
b,
c,
d;
static TrocadorKYCType fromString(String type) {
for (final result in values) {
if (result.name == type.toLowerCase()) {
return result;
}
}
throw ArgumentError("Invalid trocador kyc type: $type");
}
}