mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-24 06:48:38 +02:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user