From f279a222df152c2e03c66fe910a36e6f49360ff5 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 6 Sep 2024 16:03:18 +0300 Subject: [PATCH] Cw 682 integrate stealth ex exchange provider (#1575) * add stealthEx provider * minor fix * Update pr_test_build.yml * Update dashboard_view_model.dart * update api key * add api key * add secret to linux [skip ci] * fix network param issue * additional fee percent [skip ci] * fix for poly network * add StealthEx tracking link. * minor fix * update name [skip ci] --------- Co-authored-by: OmarHatem --- .github/workflows/pr_test_build_android.yml | 2 + .github/workflows/pr_test_build_linux.yml | 2 + assets/images/stealthex.png | Bin 0 -> 7602 bytes .../exchange_provider_description.dart | 4 + .../stealth_ex_exchange_provider.dart | 299 ++++++++++++++++++ lib/exchange/trade_state.dart | 2 +- lib/store/dashboard/trade_filter_store.dart | 30 +- .../dashboard/dashboard_view_model.dart | 13 +- .../exchange/exchange_trade_view_model.dart | 3 + .../exchange/exchange_view_model.dart | 20 +- lib/view_model/trade_details_view_model.dart | 6 + tool/utils/secret_key.dart | 2 + 12 files changed, 362 insertions(+), 21 deletions(-) create mode 100644 assets/images/stealthex.png create mode 100644 lib/exchange/provider/stealth_ex_exchange_provider.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index b7b2aaa71..b60ae1f7e 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -168,6 +168,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart + echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart - name: Rename app run: | diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 7935dd177..e25bea2ec 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -154,6 +154,8 @@ jobs: echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const stealthExBearerToken = '${{ secrets.STEALTH_EX_BEARER_TOKEN }}';" >> lib/.secrets.g.dart + echo "const stealthExAdditionalFeePercent = '${{ secrets.STEALTH_EX_ADDITIONAL_FEE_PERCENT }}';" >> lib/.secrets.g.dart - name: Rename app run: | diff --git a/assets/images/stealthex.png b/assets/images/stealthex.png new file mode 100644 index 0000000000000000000000000000000000000000..311d47b74b1755f4490cae29c77387cdd5808e3d GIT binary patch literal 7602 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 UyXE=t6D#mu<>&FoO2IGx1G&lE8vp with Serializable< ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png'); static const quantex = ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png'); + static const stealthEx = + ExchangeProviderDescription(title: 'StealthEx', raw: 10, image: 'assets/images/stealthex.png'); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -50,6 +52,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< return thorChain; case 9: return quantex; + case 10: + return stealthEx; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); } diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart new file mode 100644 index 000000000..3e05091e6 --- /dev/null +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -0,0 +1,299 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.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:http/http.dart' as http; + +class StealthExExchangeProvider extends ExchangeProvider { + StealthExExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + + static const List _notSupported = []; + + static final apiKey = secrets.stealthExBearerToken; + static final _additionalFeePercent = double.tryParse(secrets.stealthExAdditionalFeePercent); + static const _baseUrl = 'https://api.stealthex.io'; + static const _rangePath = '/v4/rates/range'; + static const _amountPath = '/v4/rates/estimated-amount'; + static const _exchangesPath = '/v4/exchanges'; + + @override + String get title => 'StealthEX'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.stealthEx; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final curFrom = isFixedRateMode ? to : from; + final curTo = isFixedRateMode ? from : to; + + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + final body = { + 'route': { + 'from': {'symbol': _getName(curFrom), 'network': _getNetwork(curFrom)}, + 'to': {'symbol': _getName(curTo), 'network': _getNetwork(curTo)} + }, + 'estimation': isFixedRateMode ? 'reversed' : 'direct', + 'rate': isFixedRateMode ? 'fixed' : 'floating', + 'additional_fee_percent': _additionalFeePercent, + }; + + try { + final response = await http.post(Uri.parse(_baseUrl + _rangePath), + headers: headers, body: json.encode(body)); + if (response.statusCode != 200) { + throw Exception('StealthEx fetch limits failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final min = responseJSON['min_amount'] as double?; + final max = responseJSON['max_amount'] as double?; + return Limits(min: min, max: max); + } catch (e) { + log(e.toString()); + throw Exception('StealthEx failed to fetch limits'); + } + } + + @override + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { + final response = await getEstimatedExchangeAmount( + from: from, to: to, amount: amount, isFixedRateMode: isFixedRateMode); + final estimatedAmount = response['estimated_amount'] as double? ?? 0.0; + return estimatedAmount > 0.0 + ? isFixedRateMode + ? amount / estimatedAmount + : estimatedAmount / amount + : 0.0; + } + + @override + Future createTrade( + {required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll}) async { + String? rateId; + String? validUntil; + + try { + if (isFixedRateMode) { + final response = await getEstimatedExchangeAmount( + from: request.fromCurrency, + to: request.toCurrency, + amount: double.parse(request.toAmount), + isFixedRateMode: isFixedRateMode); + rateId = response['rate_id'] as String?; + validUntil = response['valid_until'] as String?; + if (rateId == null) throw TradeNotCreatedException(description); + } + + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + final body = { + 'route': { + 'from': { + 'symbol': _getName(request.fromCurrency), + 'network': _getNetwork(request.fromCurrency) + }, + 'to': {'symbol': _getName(request.toCurrency), 'network': _getNetwork(request.toCurrency)} + }, + 'estimation': isFixedRateMode ? 'reversed' : 'direct', + 'rate': isFixedRateMode ? 'fixed' : 'floating', + if (isFixedRateMode) 'rate_id': rateId, + 'amount': + isFixedRateMode ? double.parse(request.toAmount) : double.parse(request.fromAmount), + 'address': request.toAddress, + 'refund_address': request.refundAddress, + 'additional_fee_percent': _additionalFeePercent, + }; + + final response = await http.post(Uri.parse(_baseUrl + _exchangesPath), + headers: headers, body: json.encode(body)); + + if (response.statusCode != 201) { + throw Exception('StealthEx create trade failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final deposit = responseJSON['deposit'] as Map; + final withdrawal = responseJSON['withdrawal'] as Map; + + final id = responseJSON['id'] as String; + final from = deposit['symbol'] as String; + final to = withdrawal['symbol'] as String; + final payoutAddress = withdrawal['address'] as String; + final depositAddress = deposit['address'] as String; + final refundAddress = responseJSON['refund_address'] as String; + final depositAmount = toDouble(deposit['amount']); + final receiveAmount = toDouble(withdrawal['amount']); + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['created_at'] as String; + + final createdAt = DateTime.parse(createdAtString); + final expiredAt = validUntil != null + ? DateTime.parse(validUntil) + : DateTime.now().add(Duration(minutes: 5)); + + + CryptoCurrency fromCurrency; + if (request.fromCurrency.tag != null && request.fromCurrency.title.toLowerCase() == from) { + fromCurrency = request.fromCurrency; + } else { + fromCurrency = CryptoCurrency.fromString(from); + } + + CryptoCurrency toCurrency; + if (request.toCurrency.tag != null && request.toCurrency.title.toLowerCase() == to) { + toCurrency = request.toCurrency; + } else { + toCurrency = CryptoCurrency.fromString(to); + } + + return Trade( + id: id, + from: fromCurrency, + to: toCurrency, + provider: description, + inputAddress: depositAddress, + payoutAddress: payoutAddress, + refundAddress: refundAddress, + amount: depositAmount.toString(), + receiveAmount: receiveAmount.toString(), + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + expiredAt: expiredAt, + ); + } catch (e) { + log(e.toString()); + throw TradeNotCreatedException(description); + } + } + + @override + Future findTradeById({required String id}) async { + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + + final uri = Uri.parse('$_baseUrl$_exchangesPath/$id'); + final response = await http.get(uri, headers: headers); + + if (response.statusCode != 200) { + throw Exception('StealthEx fetch trade failed: ${response.body}'); + } + final responseJSON = json.decode(response.body) as Map; + final deposit = responseJSON['deposit'] as Map; + final withdrawal = responseJSON['withdrawal'] as Map; + + final respId = responseJSON['id'] as String; + final from = deposit['symbol'] as String; + final to = withdrawal['symbol'] as String; + final payoutAddress = withdrawal['address'] as String; + final depositAddress = deposit['address'] as String; + final refundAddress = responseJSON['refund_address'] as String; + final depositAmount = toDouble(deposit['amount']); + final receiveAmount = toDouble(withdrawal['amount']); + final status = responseJSON['status'] as String; + final createdAtString = responseJSON['created_at'] as String; + final createdAt = DateTime.parse(createdAtString); + + return Trade( + id: respId, + from: CryptoCurrency.fromString(from), + to: CryptoCurrency.fromString(to), + provider: description, + inputAddress: depositAddress, + payoutAddress: payoutAddress, + refundAddress: refundAddress, + amount: depositAmount.toString(), + receiveAmount: receiveAmount.toString(), + state: TradeState.deserialize(raw: status), + createdAt: createdAt, + isRefund: status == 'refunded', + ); + } + + Future> getEstimatedExchangeAmount( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode}) async { + final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'}; + + final body = { + 'route': { + 'from': {'symbol': _getName(from), 'network': _getNetwork(from)}, + 'to': {'symbol': _getName(to), 'network': _getNetwork(to)} + }, + 'estimation': isFixedRateMode ? 'reversed' : 'direct', + 'rate': isFixedRateMode ? 'fixed' : 'floating', + 'amount': amount, + 'additional_fee_percent': _additionalFeePercent, + }; + + try { + final response = await http.post(Uri.parse(_baseUrl + _amountPath), + headers: headers, body: json.encode(body)); + if (response.statusCode != 200) return {}; + final responseJSON = json.decode(response.body) as Map; + final rate = responseJSON['rate'] as Map?; + return { + 'estimated_amount': responseJSON['estimated_amount'] as double?, + if (rate != null) 'valid_until': rate['valid_until'] as String?, + if (rate != null) 'rate_id': rate['id'] as String? + }; + } catch (e) { + log(e.toString()); + return {}; + } + } + + double toDouble(dynamic value) { + if (value is int) { + return value.toDouble(); + } else if (value is double) { + return value; + } else { + return 0.0; + } + } + + String _getName(CryptoCurrency currency) { + if (currency == CryptoCurrency.usdcEPoly) return 'usdce'; + return currency.title.toLowerCase(); + } + + String _getNetwork(CryptoCurrency currency) { + if (currency.tag == null) return 'mainnet'; + + if (currency == CryptoCurrency.maticpoly) return 'mainnet'; + + if (currency.tag == 'POLY') return 'matic'; + + return currency.tag!.toLowerCase(); + } +} diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index 0a196835e..e94906763 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -40,7 +40,6 @@ class TradeState extends EnumerableItem with Serializable { static const exchanging = TradeState(raw: 'exchanging', title: 'Exchanging'); static const sending = TradeState(raw: 'sending', title: 'Sending'); static const success = TradeState(raw: 'success', title: 'Success'); - static TradeState deserialize({required String raw}) { switch (raw) { @@ -119,6 +118,7 @@ class TradeState extends EnumerableItem with Serializable { case 'refunded': return refunded; case 'confirmation': + case 'verifying': return confirmation; case 'confirmed': return confirmed; diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index c05839578..4ceebedfd 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -16,7 +16,8 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap = true, displayTrocador = true, displayExolix = true, - displayThorChain = true; + displayThorChain = true, + displayStealthEx = true; @observable bool displayXMRTO; @@ -42,6 +43,9 @@ abstract class TradeFilterStoreBase with Store { @observable bool displayThorChain; + @observable + bool displayStealthEx; + @computed bool get displayAllTrades => displayChangeNow && @@ -49,7 +53,8 @@ abstract class TradeFilterStoreBase with Store { displaySimpleSwap && displayTrocador && displayExolix && - displayThorChain; + displayThorChain && + displayStealthEx; @action void toggleDisplayExchange(ExchangeProviderDescription provider) { @@ -78,6 +83,9 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.thorChain: displayThorChain = !displayThorChain; break; + case ExchangeProviderDescription.stealthEx: + displayStealthEx = !displayStealthEx; + break; case ExchangeProviderDescription.all: if (displayAllTrades) { displayChangeNow = false; @@ -88,6 +96,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = false; displayExolix = false; displayThorChain = false; + displayStealthEx = false; } else { displayChangeNow = true; displaySideShift = true; @@ -97,6 +106,7 @@ abstract class TradeFilterStoreBase with Store { displayTrocador = true; displayExolix = true; displayThorChain = true; + displayStealthEx = true; } break; } @@ -112,13 +122,19 @@ abstract class TradeFilterStoreBase with Store { ? _trades .where((item) => (displayXMRTO && item.trade.provider == ExchangeProviderDescription.xmrto) || - (displaySideShift && item.trade.provider == ExchangeProviderDescription.sideShift) || - (displayChangeNow && item.trade.provider == ExchangeProviderDescription.changeNow) || - (displayMorphToken && item.trade.provider == ExchangeProviderDescription.morphToken) || - (displaySimpleSwap && item.trade.provider == ExchangeProviderDescription.simpleSwap) || + (displaySideShift && + item.trade.provider == ExchangeProviderDescription.sideShift) || + (displayChangeNow && + item.trade.provider == ExchangeProviderDescription.changeNow) || + (displayMorphToken && + item.trade.provider == ExchangeProviderDescription.morphToken) || + (displaySimpleSwap && + item.trade.provider == ExchangeProviderDescription.simpleSwap) || (displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador) || (displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) || - (displayThorChain && item.trade.provider == ExchangeProviderDescription.thorChain)) + (displayThorChain && + item.trade.provider == ExchangeProviderDescription.thorChain) || + (displayStealthEx && item.trade.provider == ExchangeProviderDescription.stealthEx)) .toList() : _trades; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index d58d7535c..dc96c4461 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,12 +1,14 @@ import 'dart:convert'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/provider_types.dart'; -import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/service_status.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -45,11 +47,9 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/util/utils.dart'; import 'package:flutter/services.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:http/http.dart' as http; +import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; part 'dashboard_view_model.g.dart'; @@ -129,6 +129,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.thorChain.title, onChanged: () => tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)), + FilterItem( + value: () => tradeFilterStore.displayStealthEx, + caption: ExchangeProviderDescription.stealthEx.title, + onChanged: () => + tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.stealthEx)), ] }, subname = '', diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 5d99ff8a5..4cb7e4cad 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; 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/trade.dart'; @@ -52,6 +53,8 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.quantex: _provider = QuantexExchangeProvider(); break; + case ExchangeProviderDescription.stealthEx: + _provider = StealthExExchangeProvider(); case ExchangeProviderDescription.thorChain: _provider = ThorChainExchangeProvider(tradesStore: trades); break; diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 2bbe9954e..bd9474e39 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/core/create_trade_result.dart'; +import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -160,15 +161,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with final SharedPreferences sharedPreferences; List get _allProviders => [ - ChangeNowExchangeProvider(settingsStore: _settingsStore), - SideShiftExchangeProvider(), - SimpleSwapExchangeProvider(), - ThorChainExchangeProvider(tradesStore: trades), - if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), - QuantexExchangeProvider(), - TrocadorExchangeProvider( - useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), - ]; + ChangeNowExchangeProvider(settingsStore: _settingsStore), + SideShiftExchangeProvider(), + SimpleSwapExchangeProvider(), + ThorChainExchangeProvider(tradesStore: trades), + if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), + QuantexExchangeProvider(), + StealthExExchangeProvider(), + TrocadorExchangeProvider( + useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), + ]; @observable ExchangeProvider? provider; diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index eed1b6c75..e71d97ae0 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; 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/trade.dart'; @@ -60,6 +61,9 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.quantex: _provider = QuantexExchangeProvider(); break; + case ExchangeProviderDescription.stealthEx: + _provider = StealthExExchangeProvider(); + break; } _updateItems(); @@ -86,6 +90,8 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://track.ninerealms.com/${trade.id}'; case ExchangeProviderDescription.quantex: return 'https://myquantex.com/send/${trade.id}'; + case ExchangeProviderDescription.stealthEx: + return 'https://stealthex.io/exchange/?id=${trade.id}'; } return null; } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index a1d93fcf9..0741ab4e7 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -43,6 +43,8 @@ class SecretKey { SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''), SecretKey('authorization', () => ''), + SecretKey('stealthExBearerToken', () => ''), + SecretKey('stealthExAdditionalFeePercent', () => ''), ]; static final evmChainsSecrets = [