initial Trocador integration

This commit is contained in:
julian 2023-04-28 14:31:26 -06:00
parent d3b3092281
commit 05c4d01ee4
5 changed files with 631 additions and 1 deletions

View file

@ -5,6 +5,7 @@ import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange.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/enums/exchange_rate_type_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
@ -346,6 +347,16 @@ class ExchangeFormState extends ChangeNotifier {
_exchange = ChangeNowExchange.instance;
}
break;
case TrocadorExchange.exchangeName:
if (!_exchangeSupported(
exchangeName: exchange.name,
sendCurrency: sendCurrency,
receiveCurrency: receiveCurrency,
exchangeRateType: exchangeRateType,
)) {
_exchange = ChangeNowExchange.instance;
}
break;
}
await _updateRanges(shouldNotifyListeners: false);

View file

@ -9,6 +9,7 @@ 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';
@ -76,6 +77,11 @@ class _ExchangeProviderOptionsState
sendCurrency: sendCurrency,
receiveCurrency: receivingCurrency,
);
final showTrocador = exchangeSupported(
exchangeName: TrocadorExchange.exchangeName,
sendCurrency: sendCurrency,
receiveCurrency: receivingCurrency,
);
return RoundedWhiteContainer(
padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12),
@ -505,7 +511,250 @@ class _ExchangeProviderOptionsState
);
} else {
Logging.instance.log(
"$runtimeType failed to fetch rate for ChangeNOW: ${snapshot.data}",
"$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,
),
),
],
),
),
],
),
),
),
),
),
if ((showChangeNow || showMajesticBank) && showTrocador)
isDesktop
? Container(
height: 1,
color:
Theme.of(context).extension<StackColors>()!.background,
)
: 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.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(
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(

View file

@ -20,6 +20,7 @@ import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dar
import 'package:stackwallet/services/exchange/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/amount/amount.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
@ -1205,6 +1206,13 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
url =
"https://majesticbank.sc/track?trx=${trade.tradeId}";
break;
default:
if (trade.exchangeName
.startsWith(TrocadorExchange.exchangeName)) {
url =
"https://trocador.app/en/checkout${trade.tradeId}";
}
}
return ConditionalParent(
condition: isDesktop,

View file

@ -8,6 +8,7 @@ import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dar
import 'package:stackwallet/services/exchange/exchange_response.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';
abstract class Exchange {
static Exchange get defaultExchange => ChangeNowExchange.instance;
@ -20,6 +21,8 @@ abstract class Exchange {
return SimpleSwapExchange.instance;
case MajesticBankExchange.exchangeName:
return MajesticBankExchange.instance;
case TrocadorExchange.exchangeName:
return TrocadorExchange.instance;
default:
throw ArgumentError("Unknown exchange name");
}

View file

@ -0,0 +1,359 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/models/exchange/response_objects/range.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/isar/exchange_cache/currency.dart';
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/trocador_api.dart';
import 'package:uuid/uuid.dart';
class TrocadorExchange extends Exchange {
TrocadorExchange._();
static TrocadorExchange? _instance;
static TrocadorExchange get instance => _instance ??= TrocadorExchange._();
static const exchangeName = "Trocador";
static const onlySupportedNetwork = "Mainnet";
@override
Future<ExchangeResponse<Trade>> createTrade({
required String from,
required String to,
required bool fixedRate,
required Decimal amount,
required String addressTo,
String? extraId,
required String addressRefund,
required String refundExtraId,
Estimate? estimate,
required bool reversed,
}) async {
final response = reversed
? await TrocadorAPI.createNewPaymentRateTrade(
isOnion: false,
rateId: estimate?.rateId,
fromTicker: from.toLowerCase(),
fromNetwork: onlySupportedNetwork,
toTicker: to.toLowerCase(),
toNetwork: onlySupportedNetwork,
toAmount: amount.toString(),
receivingAddress: addressTo,
receivingMemo: null,
refundAddress: addressRefund,
refundMemo: null,
exchangeProvider: estimate!.exchangeProvider!,
isFixedRate: fixedRate,
)
: await TrocadorAPI.createNewStandardRateTrade(
isOnion: false,
rateId: estimate?.rateId,
fromTicker: from.toLowerCase(),
fromNetwork: onlySupportedNetwork,
toTicker: to.toLowerCase(),
toNetwork: onlySupportedNetwork,
fromAmount: amount.toString(),
receivingAddress: addressTo,
receivingMemo: null,
refundAddress: addressRefund,
refundMemo: null,
exchangeProvider: estimate!.exchangeProvider!,
isFixedRate: fixedRate,
);
if (response.value == null) {
return ExchangeResponse(exception: response.exception);
}
final trade = response.value!;
return ExchangeResponse(
value: Trade(
uuid: const Uuid().v1(),
tradeId: trade.tradeId,
rateType: fixedRate ? "fixed" : "floating",
direction: reversed ? "reversed" : "direct",
timestamp: trade.date,
updatedAt: trade.date,
payInCurrency: trade.coinFrom,
payInAmount: trade.amountFrom.toString(),
payInAddress: trade.addressProvider,
payInNetwork: trade.networkFrom,
payInExtraId: trade.addressProviderMemo,
payInTxid: "",
payOutCurrency: trade.coinTo,
payOutAmount: trade.amountTo.toString(),
payOutAddress: trade.addressUser,
payOutNetwork: trade.networkTo,
payOutExtraId: trade.addressUserMemo,
payOutTxid: "",
refundAddress: trade.refundAddress,
refundExtraId: trade.refundAddressMemo,
status: trade.status,
exchangeName: "$exchangeName (${trade.provider})",
),
);
}
List<TrocadorCoin>? _cachedCurrencies;
@override
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(
bool fixedRate) async {
_cachedCurrencies ??= (await TrocadorAPI.getCoins(isOnion: false)).value;
_cachedCurrencies?.removeWhere((e) => e.network != onlySupportedNetwork);
final value = _cachedCurrencies
?.map(
(e) => Currency(
exchangeName: exchangeName,
ticker: e.ticker,
name: e.name,
network: e.network,
image: e.image,
isFiat: false,
rateType: SupportedRateType.both,
isStackCoin: Currency.checkIsStackCoin(e.ticker),
tokenContract: null,
isAvailable: true,
),
)
.toList();
if (value == null) {
return ExchangeResponse(
exception: ExchangeException(
"Failed to fetch trocador coins",
ExchangeExceptionType.generic,
),
);
} else {
return ExchangeResponse(value: value);
}
}
@override
Future<ExchangeResponse<List<Pair>>> getAllPairs(bool fixedRate) async {
final response = await getAllCurrencies(fixedRate);
if (response.value == null) {
return ExchangeResponse(exception: response.exception);
}
final List<Pair> pairs = [];
for (int i = 0; i < response.value!.length; i++) {
final a = response.value![i];
for (int j = i + 1; j < response.value!.length; j++) {
final b = response.value![j];
pairs.add(
Pair(
exchangeName: exchangeName,
from: a.ticker,
to: b.ticker,
rateType: SupportedRateType.both,
),
);
pairs.add(
Pair(
exchangeName: exchangeName,
to: a.ticker,
from: b.ticker,
rateType: SupportedRateType.both,
),
);
}
}
return ExchangeResponse(value: pairs);
}
@override
Future<ExchangeResponse<Estimate>> getEstimate(
String from,
String to,
Decimal amount,
bool fixedRate,
bool reversed,
) async {
final isPayment = reversed || fixedRate;
final response = isPayment
? await TrocadorAPI.getNewPaymentRate(
isOnion: false,
fromTicker: from,
fromNetwork: onlySupportedNetwork,
toTicker: to,
toNetwork: onlySupportedNetwork,
toAmount: amount.toString(),
)
: await TrocadorAPI.getNewStandardRate(
isOnion: false,
fromTicker: from,
fromNetwork: onlySupportedNetwork,
toTicker: to,
toNetwork: onlySupportedNetwork,
fromAmount: amount.toString(),
);
if (response.value == null) {
return ExchangeResponse(exception: response.exception);
}
return ExchangeResponse(
value: Estimate(
estimatedAmount:
isPayment ? response.value!.amountFrom : response.value!.amountTo,
fixedRate: isPayment,
reversed: isPayment,
exchangeProvider: response.value!.provider,
rateId: response.value!.tradeId,
),
);
}
@override
Future<ExchangeResponse<List<Currency>>> getPairedCurrencies(
String forCurrency, bool fixedRate) async {
// TODO: implement getPairedCurrencies
throw UnimplementedError();
}
@override
Future<ExchangeResponse<List<Pair>>> getPairsFor(
String currency,
bool fixedRate,
) async {
final response = await getAllPairs(fixedRate);
if (response.value == null) {
return ExchangeResponse(exception: response.exception);
}
final pairs = response.value!.where(
(e) =>
e.from.toUpperCase() == currency.toUpperCase() ||
e.to.toUpperCase() == currency.toUpperCase(),
);
return ExchangeResponse(value: pairs.toList());
}
@override
Future<ExchangeResponse<Range>> getRange(
String from,
String to,
bool fixedRate,
) async {
if (_cachedCurrencies == null) {
await getAllCurrencies(fixedRate);
}
if (_cachedCurrencies == null) {
return ExchangeResponse(
exception: ExchangeException(
"Failed to updated trocador cached coins to get min/max range",
ExchangeExceptionType.generic,
),
);
}
final fromCoin = _cachedCurrencies!
.firstWhere((e) => e.ticker.toLowerCase() == from.toLowerCase());
return ExchangeResponse(
value: Range(
max: fromCoin.maximum,
min: fromCoin.minimum,
),
);
}
@override
Future<ExchangeResponse<Trade>> getTrade(String tradeId) async {
// TODO: implement getTrade
throw UnimplementedError();
}
@override
Future<ExchangeResponse<List<Trade>>> getTrades() async {
// TODO: implement getTrades
throw UnimplementedError();
}
@override
String get name => exchangeName;
@override
Future<ExchangeResponse<Trade>> updateTrade(Trade trade) async {
final response = await TrocadorAPI.getTrade(
isOnion: false,
tradeId: trade.tradeId,
);
if (response.value != null) {
final updated = response.value!;
final updatedTrade = Trade(
uuid: trade.uuid,
tradeId: updated.tradeId,
rateType: trade.rateType,
direction: trade.direction,
timestamp: trade.timestamp,
updatedAt: DateTime.now(),
payInCurrency: updated.coinFrom,
payInAmount: updated.amountFrom.toString(),
payInAddress: updated.addressProvider,
payInNetwork: trade.payInNetwork,
payInExtraId: trade.payInExtraId,
payInTxid: trade.payInTxid,
payOutCurrency: updated.coinTo,
payOutAmount: updated.amountTo.toString(),
payOutAddress: updated.addressUser,
payOutNetwork: trade.payOutNetwork,
payOutExtraId: trade.payOutExtraId,
payOutTxid: trade.payOutTxid,
refundAddress: trade.refundAddress,
refundExtraId: trade.refundExtraId,
status: updated.status,
exchangeName: "$exchangeName (${updated.provider})",
);
return ExchangeResponse(value: updatedTrade);
} else {
if (response.exception?.type == ExchangeExceptionType.orderNotFound) {
final updatedTrade = Trade(
uuid: trade.uuid,
tradeId: trade.tradeId,
rateType: trade.rateType,
direction: trade.direction,
timestamp: trade.timestamp,
updatedAt: DateTime.now(),
payInCurrency: trade.payInCurrency,
payInAmount: trade.payInAmount,
payInAddress: trade.payInAddress,
payInNetwork: trade.payInNetwork,
payInExtraId: trade.payInExtraId,
payInTxid: trade.payInTxid,
payOutCurrency: trade.payOutCurrency,
payOutAmount: trade.payOutAmount,
payOutAddress: trade.payOutAddress,
payOutNetwork: trade.payOutNetwork,
payOutExtraId: trade.payOutExtraId,
payOutTxid: trade.payOutTxid,
refundAddress: trade.refundAddress,
refundExtraId: trade.refundExtraId,
status: "Unknown",
exchangeName: trade.exchangeName,
);
return ExchangeResponse(value: updatedTrade);
}
return ExchangeResponse(exception: response.exception);
}
}
}