diff --git a/assets/images/xoswap.svg b/assets/images/xoswap.svg
new file mode 100644
index 000000000..ef83c58b1
--- /dev/null
+++ b/assets/images/xoswap.svg
@@ -0,0 +1,26 @@
+
diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart
index 8d1c75211..b3761ab73 100644
--- a/lib/exchange/exchange_provider_description.dart
+++ b/lib/exchange/exchange_provider_description.dart
@@ -34,6 +34,8 @@ class ExchangeProviderDescription extends EnumerableItem 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 with Serializable<
return stealthEx;
case 12:
return chainflip;
+ case 13:
+ return xoSwap;
default:
throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize');
}
diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart
new file mode 100644
index 000000000..0b3826aa9
--- /dev/null
+++ b/lib/exchange/provider/xoswap_exchange_provider.dart
@@ -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 _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 = {
+ '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 checkIsAvailable() async => true;
+
+ Future _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;
+
+ 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> 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;
+ } catch (e) {
+ printV(e.toString());
+ return [];
+ }
+ }
+
+ Future 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 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 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;
+ final error = responseJSON['error'] ?? 'Unknown error';
+ final message = responseJSON['message'] ?? '';
+ throw Exception('$error\n$message');
+ }
+ final responseJSON = json.decode(response.body) as Map;
+
+ final amount = responseJSON['amount'] as Map;
+ final toAmount = responseJSON['toAmount'] as Map;
+ 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 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;
+ 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;
+
+ 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;
+ final toAmount = responseJSON['toAmount'] as Map;
+ 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;
+ }
+ }
+}
diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart
index 7bce8c7e7..f63b41e5d 100644
--- a/lib/exchange/trade_state.dart
+++ b/lib/exchange/trade_state.dart
@@ -100,6 +100,7 @@ class TradeState extends EnumerableItem with Serializable {
case 'waiting':
return waiting;
case 'processing':
+ case 'inProgress':
return processing;
case 'waitingPayment':
return waitingPayment;
diff --git a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart
index aca3231ec..c1202d3ff 100644
--- a/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart
+++ b/lib/src/screens/dashboard/widgets/sync_indicator_icon.dart
@@ -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:
diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart
index 6a98329d6..e3212fd11 100644
--- a/lib/store/dashboard/trade_filter_store.dart
+++ b/lib/store/dashboard/trade_filter_store.dart
@@ -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;
diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart
index 575193027..ee9c57b65 100644
--- a/lib/view_model/dashboard/dashboard_view_model.dart
+++ b/lib/view_model/dashboard/dashboard_view_model.dart
@@ -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,
diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart
index fd8c29f1f..d488447e2 100644
--- a/lib/view_model/exchange/exchange_trade_view_model.dart
+++ b/lib/view_model/exchange/exchange_trade_view_model.dart
@@ -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();
diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart
index 3a0ca3f1f..d3871ffbd 100644
--- a/lib/view_model/exchange/exchange_view_model.dart
+++ b/lib/view_model/exchange/exchange_view_model.dart
@@ -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),
];
diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart
index 6b509dc22..a9a77fb54 100644
--- a/lib/view_model/trade_details_view_model.dart
+++ b/lib/view_model/trade_details_view_model.dart
@@ -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;
}