CW-961-Integrate-xoswap ()

* 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
parent 4f35cc9b0f
commit 1c29be7993
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 373 additions and 3 deletions

26
assets/images/xoswap.svg Normal file
View file

@ -0,0 +1,26 @@
<svg width="41" height="41" viewBox="0 0 41 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="20.5" cy="20.5" r="20" fill="url(#paint0_linear_46_2768)"/>
<path opacity="0.3" d="M35.4175 29.1152L37.8232 30.5049C34.3648 36.4818 27.902 40.5028 20.5 40.5028C13.0987 40.5028 6.63636 36.4825 3.17773 30.5065L5.58379 29.1175C8.5621 34.2633 14.1268 37.7251 20.5 37.7251C26.8741 37.7251 32.4395 34.2623 35.4175 29.1152Z" fill="url(#paint1_linear_46_2768)"/>
<path opacity="0.5" d="M20.5 0.5C31.5452 0.5 40.4995 9.45431 40.4995 20.5C40.4995 24.1435 39.5252 27.5594 37.823 30.5015L35.4172 29.1119C36.8829 26.5785 37.7217 23.6372 37.7217 20.5C37.7217 10.9886 30.0113 3.27808 20.5 3.27778V0.5Z" fill="url(#paint2_linear_46_2768)"/>
<path opacity="0.15" d="M20.4995 0.5V3.27722L20.2152 3.28009C10.835 3.43217 3.27778 11.0836 3.27778 20.5C3.27778 23.6377 4.11686 26.5794 5.58288 29.113L3.17714 30.5026C1.47452 27.5603 0.5 24.144 0.5 20.5C0.5 9.4545 9.45402 0.5003 20.4995 0.5Z" fill="url(#paint3_linear_46_2768)"/>
<path d="M28.3176 15.9228C27.5969 15.5799 26.8383 15.4041 26.0416 15.401C25.1349 15.397 24.7278 15.3978 23.8717 15.4026C23.7244 15.4033 23.5233 15.4928 23.4243 15.6013C21.9498 17.2192 20.4792 18.8411 19.007 20.4621C18.931 20.5461 18.8542 20.63 18.7718 20.7203C18.7219 20.7749 18.7219 20.8597 18.7718 20.9143C19.1099 21.2833 19.4378 21.6421 19.7656 22C20.9987 23.3463 22.2309 24.6925 23.4679 26.0356C23.5415 26.1156 23.6912 26.1814 23.7997 26.1821C24.6874 26.1885 25.0715 26.1909 25.9838 26.1853C28.6747 26.1687 30.8755 24.295 31.3253 21.6413C31.7197 19.3162 30.4724 16.9468 28.3168 15.9228H28.3176ZM25.6963 23.9862C23.9311 23.9862 22.5009 22.5552 22.5009 20.7908C22.5009 19.0264 23.9319 17.5954 25.6963 17.5954C27.4607 17.5954 28.8917 19.0264 28.8917 20.7908C28.8917 22.5552 27.4607 23.9862 25.6963 23.9862Z" fill="white"/>
<path d="M17.7312 20.8805C17.6987 20.8449 17.6987 20.791 17.7312 20.7554C19.281 19.048 20.8236 17.3486 22.3837 15.6301C22.4605 15.5454 22.4019 15.4084 22.2871 15.4068C22.0685 15.4036 20.2162 15.3989 19.1067 15.4052C19.0331 15.4052 18.9404 15.4527 18.8897 15.5081C17.9323 16.5527 16.9796 17.6012 16.0262 18.6489C16.008 18.6695 15.989 18.6893 15.962 18.717C15.9193 18.7613 15.844 18.7542 15.8147 18.7004C15.8036 18.6806 15.7918 18.6624 15.7775 18.6465C14.8367 17.6059 13.8935 16.5701 12.9551 15.5295C12.8728 15.4385 12.792 15.3965 12.6661 15.3965C11.5883 15.402 10.5097 15.3997 9.43108 15.3997H9.4295C9.34872 15.3997 9.30675 15.4955 9.36139 15.5557C9.7328 15.9643 10.0812 16.3492 10.4305 16.7332C11.6318 18.0557 12.8316 19.379 14.0353 20.6984C14.1177 20.7887 14.1216 20.8433 14.0353 20.9367C12.5172 22.5871 11.0022 24.2414 9.48652 25.8949C9.44138 25.944 9.39703 25.9939 9.34793 26.0486C9.30358 26.0985 9.33843 26.1784 9.40574 26.1784C9.41287 26.1784 9.41999 26.1784 9.42712 26.1784C10.5311 26.1784 11.6176 26.1832 12.7215 26.1895C12.8458 26.1895 12.9187 26.1389 12.9947 26.0549C13.9355 25.0167 14.8787 23.9801 15.8211 22.9427C15.8448 22.9165 15.8607 22.8833 15.8892 22.8389C16.1299 23.1018 16.3485 23.3386 16.5647 23.5762C17.3289 24.4148 18.0915 25.2543 18.8589 26.0898C18.9016 26.1365 18.9769 26.1808 19.037 26.1816C20.1869 26.1872 21.2117 26.1856 22.3615 26.1848C22.3726 26.1848 22.4257 26.1848 22.4827 26.1832C22.5286 26.1824 22.5516 26.1278 22.5207 26.0945C20.9218 24.3499 19.3237 22.6164 17.7328 20.8797L17.7312 20.8805Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_46_2768" x1="40.5" y1="40.5" x2="0.5" y2="0.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#420BE6"/>
<stop offset="1" stop-color="#A16BB3"/>
</linearGradient>
<linearGradient id="paint1_linear_46_2768" x1="36.4999" y1="30.0201" x2="4.63786" y2="30.296" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_46_2768" x1="20.6374" y1="1.18966" x2="37.2481" y2="29.0165" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_46_2768" x1="4.08621" y1="30.569" x2="20.2018" y2="1.65204" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

(image error) Size: 4.2 KiB

View file

@ -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');
}

View file

@ -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;
}
}
}

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;

View file

@ -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:

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;

View file

@ -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,

View file

@ -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();

View file

@ -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),
];

View file

@ -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;
}