CW-961-Integrate-xoswap (#2060)

* integrate xoswap

* fix network id

* fix calculating amount with rate twice

* minor: move xoswap a bit up [skip ci]

* minor fix [skip ci]

* add tracking url

* improve fetching exchange rate

* Update trade_filter_store.dart

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii
2025-03-14 03:02:39 +02:00
committed by GitHub
co-authored by OmarHatem
parent 4f35cc9b0f
commit 1c29be7993
10 changed files with 373 additions and 3 deletions
@@ -34,6 +34,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
ExchangeProviderDescription(title: 'StealthEx', raw: 11, image: 'assets/images/stealthex.png');
static const chainflip =
ExchangeProviderDescription(title: 'Chainflip', raw: 12, image: 'assets/images/chainflip.png');
static const xoSwap =
ExchangeProviderDescription(title: 'XOSwap', raw: 13, image: 'assets/images/xoswap.svg');
static ExchangeProviderDescription deserialize({required int raw}) {
switch (raw) {
@@ -63,6 +65,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
return stealthEx;
case 12:
return chainflip;
case 13:
return xoSwap;
default:
throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
}
@@ -0,0 +1,309 @@
import 'dart:convert';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:http/http.dart' as http;
class XOSwapExchangeProvider extends ExchangeProvider {
XOSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported));
static const List<CryptoCurrency> _notSupported = [];
static const _apiAuthority = 'exchange.exodus.io';
static const _apiPath = '/v3';
static const _pairsPath = '/pairs';
static const _ratePath = '/rates';
static const _orders = '/orders';
static const _assets = '/assets';
static const _headers = {'Content-Type': 'application/json', 'App-Name': 'cake-labs'};
final _networks = <String, String>{
'POL': 'matic',
'ETH': 'ethereum',
'BTC': 'bitcoin',
'BSC': 'bsc',
'SOL': 'solana',
'TRX': 'tronmainnet',
'ZEC': 'zcash',
'ADA': 'cardano',
'DOGE': 'dogecoin',
'XMR': 'monero',
'BCH': 'bcash',
'BSV': 'bitcoinsv',
'XRP': 'ripple',
'LTC': 'litecoin',
'EOS': 'eosio',
'XLM': 'stellar',
};
@override
String get title => 'XOSwap';
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
bool get supportsFixedRate => true;
@override
ExchangeProviderDescription get description => ExchangeProviderDescription.xoSwap;
@override
Future<bool> checkIsAvailable() async => true;
Future<String?> _getAssets(CryptoCurrency currency) async {
if (currency.tag == null) return currency.title;
try {
final normalizedNetwork = _networks[currency.tag];
if (normalizedNetwork == null) return null;
final uri = Uri.https(_apiAuthority, _apiPath + _assets,
{'networks': normalizedNetwork, 'query': currency.title});
final response = await http.get(uri, headers: _headers);
if (response.statusCode != 200) {
throw Exception('Failed to fetch assets for ${currency.title} on ${currency.tag}');
}
final assets = json.decode(response.body) as List<dynamic>;
final asset = assets.firstWhere(
(asset) {
final assetSymbol = (asset['symbol'] as String).toUpperCase();
return assetSymbol == currency.title.toUpperCase();
},
orElse: () => null,
);
return asset != null ? asset['id'] as String : null;
} catch (e) {
printV(e.toString());
return null;
}
}
Future<List<dynamic>> getRatesForPair({
required CryptoCurrency from,
required CryptoCurrency to,
}) async {
try {
final curFrom = await _getAssets(from);
final curTo = await _getAssets(to);
if (curFrom == null || curTo == null) return [];
final pairId = curFrom + '_' + curTo;
final uri = Uri.https(_apiAuthority, '$_apiPath$_pairsPath/$pairId$_ratePath');
final response = await http.get(uri, headers: _headers);
if (response.statusCode != 200) return [];
return json.decode(response.body) as List<dynamic>;
} catch (e) {
printV(e.toString());
return [];
}
}
Future<Limits> fetchLimits({
required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode,
}) async {
final rates = await getRatesForPair(from: from, to: to);
if (rates.isEmpty) return Limits(min: 0, max: 0);
double minLimit = double.infinity;
double maxLimit = 0;
for (var rate in rates) {
final double currentMin = double.parse(rate['min']['value'].toString());
final double currentMax = double.parse(rate['max']['value'].toString());
if (currentMin < minLimit) minLimit = currentMin;
if (currentMax > maxLimit) maxLimit = currentMax;
}
return Limits(min: minLimit, max: maxLimit);
}
Future<double> fetchRate({
required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount,
}) async {
try {
final rates = await getRatesForPair(from: from, to: to);
if (rates.isEmpty) return 0;
if (!isFixedRateMode) {
double bestOutput = 0.0;
for (var rate in rates) {
final double minVal = double.parse(rate['min']['value'].toString());
final double maxVal = double.parse(rate['max']['value'].toString());
if (amount >= minVal && amount <= maxVal) {
final double rateMultiplier = double.parse(rate['amount']['value'].toString());
final double minerFee = double.parse(rate['minerFee']['value'].toString());
final double outputAmount = (amount * rateMultiplier) - minerFee;
if (outputAmount > bestOutput) {
bestOutput = outputAmount;
}
}
}
return bestOutput > 0 ? (bestOutput / amount) : 0;
} else {
double bestInput = double.infinity;
for (var rate in rates) {
final double rateMultiplier = double.parse(rate['amount']['value'].toString());
final double minerFee = double.parse(rate['minerFee']['value'].toString());
final double minVal = double.parse(rate['min']['value'].toString());
final double maxVal = double.parse(rate['max']['value'].toString());
final double requiredSend = (amount + minerFee) / rateMultiplier;
if (requiredSend >= minVal && requiredSend <= maxVal) {
if (requiredSend < bestInput) {
bestInput = requiredSend;
}
}
}
return bestInput < double.infinity ? amount / bestInput : 0;
}
} catch (e) {
printV(e.toString());
return 0;
}
}
@override
Future<Trade> createTrade({
required TradeRequest request,
required bool isFixedRateMode,
required bool isSendAll,
}) async {
try {
final uri = Uri.https(_apiAuthority, '$_apiPath$_orders');
final payload = {
'fromAmount': request.fromAmount,
'fromAddress': request.refundAddress,
'toAmount': request.toAmount,
'toAddress': request.toAddress,
'pairId': '${request.fromCurrency.title}_${request.toCurrency.title}',
};
final response = await http.post(uri, headers: _headers, body: json.encode(payload));
if (response.statusCode != 201) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final error = responseJSON['error'] ?? 'Unknown error';
final message = responseJSON['message'] ?? '';
throw Exception('$error\n$message');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final amount = responseJSON['amount'] as Map<String, dynamic>;
final toAmount = responseJSON['toAmount'] as Map<String, dynamic>;
final orderId = responseJSON['id'] as String;
final from = request.fromCurrency;
final to = request.toCurrency;
final payoutAddress = responseJSON['toAddress'] as String;
final depositAddress = responseJSON['payInAddress'] as String;
final refundAddress = responseJSON['fromAddress'] as String;
final depositAmount = _toDouble(amount['value']);
final receiveAmount = toAmount['value'] as String;
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['createdAt'] as String;
final extraId = responseJSON['payInAddressTag'] as String?;
final createdAt = DateTime.parse(createdAtString).toLocal();
return Trade(
id: orderId,
from: from,
to: to,
provider: description,
inputAddress: depositAddress,
refundAddress: refundAddress,
state: TradeState.deserialize(raw: status),
createdAt: createdAt,
amount: depositAmount.toString(),
receiveAmount: receiveAmount.toString(),
payoutAddress: payoutAddress,
extraId: extraId,
);
} catch (e) {
printV(e.toString());
throw TradeNotCreatedException(description);
}
}
@override
Future<Trade> findTradeById({required String id}) async {
try {
final uri = Uri.https(_apiAuthority, '$_apiPath$_orders/$id');
final response = await http.get(uri, headers: _headers);
if (response.statusCode != 200) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
if (responseJSON.containsKey('code') && responseJSON['code'] == 'NOT_FOUND') {
throw Exception('Trade not found');
}
final error = responseJSON['error'] ?? 'Unknown error';
final message = responseJSON['message'] ?? responseJSON['details'] ?? '';
throw Exception('$error\n$message');
}
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final pairId = responseJSON['pairId'] as String;
final pairParts = pairId.split('_');
final CryptoCurrency fromCurrency =
CryptoCurrency.fromString(pairParts.isNotEmpty ? pairParts[0] : "");
final CryptoCurrency toCurrency =
CryptoCurrency.fromString(pairParts.length > 1 ? pairParts[1] : "");
final amount = responseJSON['amount'] as Map<String, dynamic>;
final toAmount = responseJSON['toAmount'] as Map<String, dynamic>;
final orderId = responseJSON['id'] as String;
final depositAmount = amount['value'] as String;
final receiveAmount = toAmount['value'] as String;
final depositAddress = responseJSON['payInAddress'] as String;
final payoutAddress = responseJSON['toAddress'] as String;
final refundAddress = responseJSON['fromAddress'] as String;
final status = responseJSON['status'] as String;
final createdAtString = responseJSON['createdAt'] as String;
final createdAt = DateTime.parse(createdAtString).toLocal();
final extraId = responseJSON['payInAddressTag'] as String?;
return Trade(
id: orderId,
from: fromCurrency,
to: toCurrency,
provider: description,
inputAddress: depositAddress,
refundAddress: refundAddress,
state: TradeState.deserialize(raw: status),
createdAt: createdAt,
amount: depositAmount,
receiveAmount: receiveAmount,
payoutAddress: payoutAddress,
extraId: extraId,
);
} catch (e) {
printV(e.toString());
throw TradeNotCreatedException(description);
}
}
double _toDouble(dynamic value) {
if (value is int) {
return value.toDouble();
} else if (value is double) {
return value;
} else {
return 0.0;
}
}
}
+1
View File
@@ -100,6 +100,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
case 'waiting':
return waiting;
case 'processing':
case 'inProgress':
return processing;
case 'waitingPayment':
return waitingPayment;
@@ -21,6 +21,7 @@ class SyncIndicatorIcon extends StatelessWidget {
static const String fetching = 'fetching';
static const String finished = 'finished';
static const String success = 'success';
static const String complete = 'complete';
@override
Widget build(BuildContext context) {
@@ -47,6 +48,7 @@ class SyncIndicatorIcon extends StatelessWidget {
break;
case finished:
case success:
case complete:
indicatorColor = PaletteDark.brightGreen;
break;
default:
+15 -3
View File
@@ -20,6 +20,7 @@ abstract class TradeFilterStoreBase with Store {
displayThorChain = true,
displayLetsExchange = true,
displayStealthEx = true,
displayXOSwap = true,
displaySwapTrade = true;
@observable
@@ -45,7 +46,7 @@ abstract class TradeFilterStoreBase with Store {
@observable
bool displayChainflip;
@observable
bool displayThorChain;
@@ -55,6 +56,9 @@ abstract class TradeFilterStoreBase with Store {
@observable
bool displayStealthEx;
@observable
bool displayXOSwap;
@observable
bool displaySwapTrade;
@@ -64,11 +68,12 @@ abstract class TradeFilterStoreBase with Store {
displaySideShift &&
displaySimpleSwap &&
displayTrocador &&
displayExolix &&
displayExolix &&
displayChainflip &&
displayThorChain &&
displayLetsExchange &&
displayStealthEx &&
displayXOSwap &&
displaySwapTrade;
@action
@@ -107,8 +112,12 @@ abstract class TradeFilterStoreBase with Store {
case ExchangeProviderDescription.stealthEx:
displayStealthEx = !displayStealthEx;
break;
case ExchangeProviderDescription.swapTrade:
case ExchangeProviderDescription.xoSwap:
displayXOSwap = !displayXOSwap;
break;
case ExchangeProviderDescription.swapTrade:
displaySwapTrade = !displaySwapTrade;
break;
case ExchangeProviderDescription.all:
if (displayAllTrades) {
displayChangeNow = false;
@@ -122,6 +131,7 @@ abstract class TradeFilterStoreBase with Store {
displayThorChain = false;
displayLetsExchange = false;
displayStealthEx = false;
displayXOSwap = false;
displaySwapTrade = false;
} else {
displayChangeNow = true;
@@ -135,6 +145,7 @@ abstract class TradeFilterStoreBase with Store {
displayThorChain = true;
displayLetsExchange = true;
displayStealthEx = true;
displayXOSwap = true;
displaySwapTrade = true;
}
break;
@@ -168,6 +179,7 @@ abstract class TradeFilterStoreBase with Store {
(displayLetsExchange &&
item.trade.provider == ExchangeProviderDescription.letsExchange) ||
(displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx) ||
(displayXOSwap && item.trade.provider == ExchangeProviderDescription.xoSwap) ||
(displaySwapTrade && item.trade.provider == ExchangeProviderDescription.swapTrade))
.toList()
: _trades;
@@ -152,6 +152,11 @@ abstract class DashboardViewModelBase with Store {
caption: ExchangeProviderDescription.stealthEx.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)),
FilterItem(
value: () => tradeFilterStore.displayXOSwap,
caption: ExchangeProviderDescription.xoSwap.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.xoSwap)),
FilterItem(
value: () => tradeFilterStore.displaySwapTrade,
caption: ExchangeProviderDescription.swapTrade.title,
@@ -11,6 +11,7 @@ import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'
import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart';
@@ -64,6 +65,8 @@ abstract class ExchangeTradeViewModelBase with Store {
case ExchangeProviderDescription.chainflip:
_provider = ChainflipExchangeProvider(tradesStore: trades);
break;
case ExchangeProviderDescription.xoSwap:
_provider = XOSwapExchangeProvider();
}
_updateItems();
@@ -7,6 +7,7 @@ import 'package:cake_wallet/core/create_trade_result.dart';
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_priority.dart';
@@ -187,6 +188,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
SwapTradeExchangeProvider(),
LetsExchangeExchangeProvider(),
StealthExExchangeProvider(),
XOSwapExchangeProvider(),
TrocadorExchangeProvider(
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
];
@@ -12,6 +12,7 @@ import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'
import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart';
@@ -72,6 +73,9 @@ abstract class TradeDetailsViewModelBase with Store {
case ExchangeProviderDescription.chainflip:
_provider = ChainflipExchangeProvider(tradesStore: trades);
break;
case ExchangeProviderDescription.xoSwap:
_provider = XOSwapExchangeProvider();
break;
}
_updateItems();
@@ -104,6 +108,8 @@ abstract class TradeDetailsViewModelBase with Store {
return 'https://stealthex.io/exchange/?id=${trade.id}';
case ExchangeProviderDescription.chainflip:
return 'https://scan.chainflip.io/channels/${trade.id}';
case ExchangeProviderDescription.xoSwap:
return 'https://orders.xoswap.com/${trade.id}';
}
return null;
}