Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CW-961-Integrate-xoswap #2060

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions assets/images/xoswap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions lib/exchange/exchange_provider_description.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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');
}
Expand Down
263 changes: 263 additions & 0 deletions lib/exchange/provider/xoswap_exchange_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
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 _headers = {'Content-Type': 'application/json', 'App-Name': 'cake-labs'};

@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<List<dynamic>> getRatesForPair({
required CryptoCurrency from,
required CryptoCurrency to,
}) async {
try {
final curFrom = '${from.title}${_networkId(from)}';
final curTo = '${to.title}${_networkId(to)}';
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 = amount['value'] as double;
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);
}
}

String _networkId(CryptoCurrency currency) {
if (currency.tag == 'POL') {
if (currency.title.toUpperCase() == 'USDT') return 'matic86E249C1';

if (currency.title.toUpperCase() == 'USDC') return 'matic0A883D9B';

return '';
}

if (currency.tag == 'ETH') return '';

return currency.tag ?? '';
}
}
1 change: 1 addition & 0 deletions lib/exchange/trade_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 16 additions & 5 deletions lib/store/dashboard/trade_filter_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ abstract class TradeFilterStoreBase with Store {
displayChainflip = true,
displayThorChain = true,
displayLetsExchange = true,
displayStealthEx = true;
displayStealthEx = true,
displayXOSwap = true;

@observable
bool displayXMRTO;
Expand All @@ -44,7 +45,7 @@ abstract class TradeFilterStoreBase with Store {

@observable
bool displayChainflip;

@observable
bool displayThorChain;

Expand All @@ -54,17 +55,21 @@ abstract class TradeFilterStoreBase with Store {
@observable
bool displayStealthEx;

@observable
bool displayXOSwap;

@computed
bool get displayAllTrades =>
displayChangeNow &&
displaySideShift &&
displaySimpleSwap &&
displayTrocador &&
displayExolix &&
displayExolix &&
displayChainflip &&
displayThorChain &&
displayLetsExchange &&
displayStealthEx;
displayStealthEx &&
displayXOSwap;

@action
void toggleDisplayExchange(ExchangeProviderDescription provider) {
Expand Down Expand Up @@ -102,6 +107,9 @@ abstract class TradeFilterStoreBase with Store {
case ExchangeProviderDescription.stealthEx:
displayStealthEx = !displayStealthEx;
break;
case ExchangeProviderDescription.xoSwap:
displayXOSwap = !displayXOSwap;
break;
case ExchangeProviderDescription.all:
if (displayAllTrades) {
displayChangeNow = false;
Expand All @@ -115,6 +123,7 @@ abstract class TradeFilterStoreBase with Store {
displayThorChain = false;
displayLetsExchange = false;
displayStealthEx = false;
displayXOSwap = false;
} else {
displayChangeNow = true;
displaySideShift = true;
Expand All @@ -127,6 +136,7 @@ abstract class TradeFilterStoreBase with Store {
displayThorChain = true;
displayLetsExchange = true;
displayStealthEx = true;
displayXOSwap = true;
}
break;
}
Expand Down Expand Up @@ -158,7 +168,8 @@ abstract class TradeFilterStoreBase with Store {
item.trade.provider == ExchangeProviderDescription.thorChain) ||
(displayLetsExchange &&
item.trade.provider == ExchangeProviderDescription.letsExchange) ||
(displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx))
(displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx) ||
(displayXOSwap && item.trade.provider == ExchangeProviderDescription.xoSwap))
.toList()
: _trades;
}
Expand Down
Loading
Loading