Cw 537 integrate thor chain swaps (#1280)

* thorChain btc to eth swap

* eth to btc swap

* update the UI

* update localization

* Update thorchain_exchange.provider.dart

* minor fixes

* minor fix

* fix min amount bug

* revert amount_converter changes

* fetching thorChain traid info

* resolve evm related merge conflicts

* minor fix

* Fix eth transaction hash for Thorchain Integration

* add new status endpoint and refund address for eth

* Adjust affiliate fee

* Fix conflicts with main

* review comments + transaction filter item

* taproot addresses check

* added 10 outputs check

* Update thorchain_exchange.provider.dart

* minor fixes

* update thorchain title

* fix fetching rate for thorchain

* Revert "fix fetching rate for thorchain"

This reverts commit 3aa1386ecfbca14271bf01a73b424de19c4fd484.

* fix thorchain exchange rate

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii
2024-03-28 14:41:11 +02:00
committed by GitHub
co-authored by OmarHatem
parent b9e803f3bd
commit cdf081edfd
55 changed files with 534 additions and 102 deletions
+2 -1
View File
@@ -85,7 +85,8 @@ class CWBitcoin extends Bitcoin {
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: feeRate);
+2 -1
View File
@@ -76,7 +76,8 @@ class CWEthereum extends Ethereum {
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as EVMChainTransactionPriority,
currency: currency,
@@ -22,6 +22,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png');
static const exolix =
ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png');
static const thorChain =
ExchangeProviderDescription(title: 'ThorChain' , raw: 8, image: 'assets/images/thorchain.png');
static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: '');
@@ -41,6 +43,8 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable<
return trocador;
case 6:
return exolix;
case 8:
return thorChain;
case 7:
return all;
default:
@@ -0,0 +1,248 @@
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_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:hive/hive.dart';
import 'package:http/http.dart' as http;
class ThorChainExchangeProvider extends ExchangeProvider {
ThorChainExchangeProvider({required this.tradesStore})
: super(pairList: supportedPairs(_notSupported));
static final List<CryptoCurrency> _notSupported = [
...(CryptoCurrency.all
.where((element) => ![
CryptoCurrency.btc,
CryptoCurrency.eth,
CryptoCurrency.ltc,
CryptoCurrency.bch,
CryptoCurrency.aave,
CryptoCurrency.dai,
CryptoCurrency.gusd,
CryptoCurrency.usdc,
CryptoCurrency.usdterc20,
CryptoCurrency.wbtc,
].contains(element))
.toList())
];
static final isRefundAddressSupported = [CryptoCurrency.eth];
static const _baseURL = 'thornode.ninerealms.com';
static const _quotePath = '/thorchain/quote/swap';
static const _txInfoPath = '/thorchain/tx/status/';
static const _affiliateName = 'cakewallet';
static const _affiliateBps = '175';
final Box<Trade> tradesStore;
@override
String get title => 'THORChain';
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
bool get supportsFixedRate => false;
@override
ExchangeProviderDescription get description => ExchangeProviderDescription.thorChain;
@override
Future<bool> checkIsAvailable() async => true;
@override
Future<double> fetchRate(
{required CryptoCurrency from,
required CryptoCurrency to,
required double amount,
required bool isFixedRateMode,
required bool isReceiveAmount}) async {
try {
if (amount == 0) return 0.0;
final params = {
'from_asset': _normalizeCurrency(from),
'to_asset': _normalizeCurrency(to),
'amount': _doubleToThorChainString(amount),
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps
};
final responseJSON = await _getSwapQuote(params);
final expectedAmountOut = responseJSON['expected_amount_out'] as String? ?? '0.0';
return _thorChainAmountToDouble(expectedAmountOut) / amount;
} catch (e) {
print(e.toString());
return 0.0;
}
}
@override
Future<Limits> fetchLimits(
{required CryptoCurrency from,
required CryptoCurrency to,
required bool isFixedRateMode}) async {
final params = {
'from_asset': _normalizeCurrency(from),
'to_asset': _normalizeCurrency(to),
'amount': _doubleToThorChainString(1),
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps
};
final responseJSON = await _getSwapQuote(params);
final minAmountIn = responseJSON['recommended_min_amount_in'] as String? ?? '0.0';
return Limits(min: _thorChainAmountToDouble(minAmountIn));
}
@override
Future<Trade> createTrade({required TradeRequest request, required bool isFixedRateMode}) async {
String formattedToAddress = request.toAddress.startsWith('bitcoincash:')
? request.toAddress.replaceFirst('bitcoincash:', '')
: request.toAddress;
final formattedFromAmount = double.parse(request.fromAmount);
final params = {
'from_asset': _normalizeCurrency(request.fromCurrency),
'to_asset': _normalizeCurrency(request.toCurrency),
'amount': _doubleToThorChainString(formattedFromAmount),
'destination': formattedToAddress,
'affiliate': _affiliateName,
'affiliate_bps': _affiliateBps,
'refund_address':
isRefundAddressSupported.contains(request.fromCurrency) ? request.refundAddress : '',
};
final responseJSON = await _getSwapQuote(params);
final inputAddress = responseJSON['inbound_address'] as String?;
final memo = responseJSON['memo'] as String?;
return Trade(
id: '',
from: request.fromCurrency,
to: request.toCurrency,
provider: description,
inputAddress: inputAddress,
createdAt: DateTime.now(),
amount: request.fromAmount,
state: TradeState.notFound,
payoutAddress: request.toAddress,
memo: memo);
}
@override
Future<Trade> findTradeById({required String id}) async {
if (id.isEmpty) throw Exception('Trade id is empty');
final formattedId = id.startsWith('0x') ? id.substring(2) : id;
final uri = Uri.https(_baseURL, '$_txInfoPath$formattedId');
final response = await http.get(uri);
if (response.statusCode == 404) {
throw Exception('Trade not found for id: $formattedId');
} else if (response.statusCode != 200) {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
final responseJSON = json.decode(response.body);
final Map<String, dynamic> stagesJson = responseJSON['stages'] as Map<String, dynamic>;
final inboundObservedStarted = stagesJson['inbound_observed']?['started'] as bool? ?? true;
if (!inboundObservedStarted) {
throw Exception('Trade has not started for id: $formattedId');
}
final currentState = _updateStateBasedOnStages(stagesJson) ?? TradeState.notFound;
final tx = responseJSON['tx'];
final String fromAddress = tx['from_address'] as String? ?? '';
final String toAddress = tx['to_address'] as String? ?? '';
final List<dynamic> coins = tx['coins'] as List<dynamic>;
final String? memo = tx['memo'] as String?;
final parts = memo?.split(':') ?? [];
final String toChain = parts.length > 1 ? parts[1].split('.')[0] : '';
final String toAsset = parts.length > 1 && parts[1].split('.').length > 1 ? parts[1].split('.')[1].split('-')[0] : '';
final formattedToChain = CryptoCurrency.fromString(toChain);
final toAssetWithChain = CryptoCurrency.fromString(toAsset, walletCurrency:formattedToChain);
final plannedOutTxs = responseJSON['planned_out_txs'] as List<dynamic>?;
final isRefund = plannedOutTxs?.any((tx) => tx['refund'] == true) ?? false;
return Trade(
id: id,
from: CryptoCurrency.fromString(tx['chain'] as String? ?? ''),
to: toAssetWithChain,
provider: description,
inputAddress: fromAddress,
payoutAddress: toAddress,
amount: coins.first['amount'] as String? ?? '0.0',
state: currentState,
memo: memo,
isRefund: isRefund,
);
}
Future<Map<String, dynamic>> _getSwapQuote(Map<String, String> params) async {
Uri uri = Uri.https(_baseURL, _quotePath, params);
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Unexpected HTTP status: ${response.statusCode}');
}
if (response.body.contains('error')) {
throw Exception('Unexpected response: ${response.body}');
}
return json.decode(response.body) as Map<String, dynamic>;
}
String _normalizeCurrency(CryptoCurrency currency) {
final networkTitle = currency.tag == 'ETH' ? 'ETH' : currency.title;
return '$networkTitle.${currency.title}';
}
String _doubleToThorChainString(double amount) => (amount * 1e8).toInt().toString();
double _thorChainAmountToDouble(String amount) => double.parse(amount) / 1e8;
TradeState? _updateStateBasedOnStages(Map<String, dynamic> stages) {
TradeState? currentState;
if (stages['inbound_observed']['completed'] as bool? ?? false) {
currentState = TradeState.confirmation;
}
if (stages['inbound_confirmation_counted']['completed'] as bool? ?? false) {
currentState = TradeState.confirmed;
}
if (stages['inbound_finalised']['completed'] as bool? ?? false) {
currentState = TradeState.processing;
}
if (stages['swap_finalised']['completed'] as bool? ?? false) {
currentState = TradeState.traded;
}
if (stages['outbound_signed']['completed'] as bool? ?? false) {
currentState = TradeState.success;
}
return currentState;
}
}
+21 -3
View File
@@ -27,7 +27,10 @@ class Trade extends HiveObject {
this.password,
this.providerId,
this.providerName,
this.fromWalletAddress
this.fromWalletAddress,
this.memo,
this.txId,
this.isRefund,
}) {
if (provider != null) providerRaw = provider.raw;
@@ -105,6 +108,15 @@ class Trade extends HiveObject {
@HiveField(17)
String? fromWalletAddress;
@HiveField(18)
String? memo;
@HiveField(19)
String? txId;
@HiveField(20)
bool? isRefund;
static Trade fromMap(Map<String, Object?> map) {
return Trade(
id: map['id'] as String,
@@ -115,7 +127,10 @@ class Trade extends HiveObject {
map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null,
amount: map['amount'] as String,
walletId: map['wallet_id'] as String,
fromWalletAddress: map['from_wallet_address'] as String?
fromWalletAddress: map['from_wallet_address'] as String?,
memo: map['memo'] as String?,
txId: map['tx_id'] as String?,
isRefund: map['isRefund'] as bool?
);
}
@@ -128,7 +143,10 @@ class Trade extends HiveObject {
'date': createdAt != null ? createdAt!.millisecondsSinceEpoch : null,
'amount': amount,
'wallet_id': walletId,
'from_wallet_address': fromWalletAddress
'from_wallet_address': fromWalletAddress,
'memo': memo,
'tx_id': txId,
'isRefund': isRefund
};
}
+3
View File
@@ -41,6 +41,8 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
static const success = TradeState(raw: 'success', title: 'Success');
static TradeState deserialize({required String raw}) {
switch (raw) {
case 'NOT_FOUND':
return notFound;
case 'pending':
return pending;
case 'confirming':
@@ -98,6 +100,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> {
case 'sending':
return sending;
case 'success':
case 'done':
return success;
default:
throw Exception('Unexpected token: $raw in TradeState deserialize');
@@ -9,7 +9,7 @@ class FilterTile extends StatelessWidget {
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0),
padding: EdgeInsets.symmetric(vertical: 6.0, horizontal: 24.0),
child: child,
);
}
@@ -20,6 +20,7 @@ class SyncIndicatorIcon extends StatelessWidget {
static const String created = 'created';
static const String fetching = 'fetching';
static const String finished = 'finished';
static const String success = 'success';
@override
Widget build(BuildContext context) {
@@ -45,6 +46,7 @@ class SyncIndicatorIcon extends StatelessWidget {
indicatorColor = Colors.red;
break;
case finished:
case success:
indicatorColor = PaletteDark.brightGreen;
break;
default:
@@ -34,7 +34,9 @@ class TradeRow extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getPoweredImage(provider)!,
ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset(provider.image, width: 36, height: 36)),
SizedBox(width: 12),
Expanded(
child: Column(
@@ -69,38 +71,4 @@ class TradeRow extends StatelessWidget {
),
));
}
Widget? _getPoweredImage(ExchangeProviderDescription provider) {
Widget? image;
switch (provider) {
case ExchangeProviderDescription.xmrto:
image = Image.asset('assets/images/xmrto.png', height: 36, width: 36);
break;
case ExchangeProviderDescription.changeNow:
image = Image.asset('assets/images/changenow.png', height: 36, width: 36);
break;
case ExchangeProviderDescription.morphToken:
image = Image.asset('assets/images/morph.png', height: 36, width: 36);
break;
case ExchangeProviderDescription.sideShift:
image = Image.asset('assets/images/sideshift.png', width: 36, height: 36);
break;
case ExchangeProviderDescription.simpleSwap:
image = Image.asset('assets/images/simpleSwap.png', width: 36, height: 36);
break;
case ExchangeProviderDescription.trocador:
image = ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset('assets/images/trocador.png', width: 36, height: 36));
break;
case ExchangeProviderDescription.exolix:
image = Image.asset('assets/images/exolix.png', width: 36, height: 36);
break;
default:
image = null;
}
return image;
}
}
+13 -2
View File
@@ -1,3 +1,5 @@
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/core/auth_service.dart';
@@ -60,7 +62,7 @@ class ExchangePage extends BasePage {
final _receiveAmountFocus = FocusNode();
final _receiveAddressFocus = FocusNode();
final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500));
final _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
Debounce _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
var _isReactionsSet = false;
final arrowBottomPurple = Image.asset(
@@ -431,7 +433,9 @@ class ExchangePage extends BasePage {
}
if (state is TradeIsCreatedSuccessfully) {
exchangeViewModel.reset();
Navigator.of(context).pushNamed(Routes.exchangeConfirm);
(exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain)
? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade)
: Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm);
}
});
@@ -470,6 +474,13 @@ class ExchangePage extends BasePage {
if (depositAmountController.text != exchangeViewModel.depositAmount &&
depositAmountController.text != S.of(context).all) {
exchangeViewModel.isSendAllEnabled = false;
final isThorChain = exchangeViewModel.selectedProviders
.any((provider) => provider is ThorChainExchangeProvider);
_depositAmountDebounce = isThorChain
? Debounce(Duration(milliseconds: 1000))
: Debounce(Duration(milliseconds: 500));
_depositAmountDebounce.run(() {
exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
exchangeViewModel.isReceiveAmountEntered = false;
+26 -13
View File
@@ -3,18 +3,20 @@ import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:mobx/mobx.dart';
part'trade_filter_store.g.dart';
part 'trade_filter_store.g.dart';
class TradeFilterStore = TradeFilterStoreBase with _$TradeFilterStore;
abstract class TradeFilterStoreBase with Store {
TradeFilterStoreBase() : displayXMRTO = true,
TradeFilterStoreBase()
: displayXMRTO = true,
displayChangeNow = true,
displaySideShift = true,
displayMorphToken = true,
displaySimpleSwap = true,
displayTrocador = true,
displayExolix = true;
displayExolix = true,
displayThorChain = true;
@observable
bool displayXMRTO;
@@ -37,8 +39,17 @@ abstract class TradeFilterStoreBase with Store {
@observable
bool displayExolix;
@observable
bool displayThorChain;
@computed
bool get displayAllTrades => displayChangeNow && displaySideShift && displaySimpleSwap && displayTrocador && displayExolix;
bool get displayAllTrades =>
displayChangeNow &&
displaySideShift &&
displaySimpleSwap &&
displayTrocador &&
displayExolix &&
displayThorChain;
@action
void toggleDisplayExchange(ExchangeProviderDescription provider) {
@@ -64,6 +75,9 @@ abstract class TradeFilterStoreBase with Store {
case ExchangeProviderDescription.exolix:
displayExolix = !displayExolix;
break;
case ExchangeProviderDescription.thorChain:
displayThorChain = !displayThorChain;
break;
case ExchangeProviderDescription.all:
if (displayAllTrades) {
displayChangeNow = false;
@@ -73,6 +87,7 @@ abstract class TradeFilterStoreBase with Store {
displaySimpleSwap = false;
displayTrocador = false;
displayExolix = false;
displayThorChain = false;
} else {
displayChangeNow = true;
displaySideShift = true;
@@ -81,6 +96,7 @@ abstract class TradeFilterStoreBase with Store {
displaySimpleSwap = true;
displayTrocador = true;
displayExolix = true;
displayThorChain = true;
}
break;
}
@@ -96,16 +112,13 @@ 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))
(displayExolix && item.trade.provider == ExchangeProviderDescription.exolix) ||
(displayThorChain && item.trade.provider == ExchangeProviderDescription.thorChain))
.toList()
: _trades;
}
@@ -71,7 +71,7 @@ abstract class AnonpayDetailsViewModelBase with Store {
]);
items.add(TrackTradeListItem(
title: 'Track',
title: S.current.track,
value: invoiceDetail.clearnetStatusUrl,
onTap: () => launchUrlString(invoiceDetail.clearnetStatusUrl)));
}
@@ -120,6 +120,11 @@ abstract class DashboardViewModelBase with Store {
caption: ExchangeProviderDescription.exolix.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)),
FilterItem(
value: () => tradeFilterStore.displayThorChain,
caption: ExchangeProviderDescription.thorChain.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.thorChain)),
]
},
subname = '',
@@ -6,6 +6,7 @@ import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exolix_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/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -47,6 +48,9 @@ abstract class ExchangeTradeViewModelBase with Store {
case ExchangeProviderDescription.exolix:
_provider = ExolixExchangeProvider();
break;
case ExchangeProviderDescription.thorChain:
_provider = ThorChainExchangeProvider(tradesStore: trades);
break;
}
_updateItems();
@@ -100,8 +104,13 @@ abstract class ExchangeTradeViewModelBase with Store {
final output = sendViewModel.outputs.first;
output.address = trade.inputAddress ?? '';
output.setCryptoAmount(trade.amount);
if (_provider is ThorChainExchangeProvider) output.memo = trade.memo;
sendViewModel.selectedCryptoCurrency = trade.from;
await sendViewModel.createTransaction();
final pendingTransaction = await sendViewModel.createTransaction(provider: _provider);
if (_provider is ThorChainExchangeProvider) {
trade.id = pendingTransaction?.id ?? '';
trades.add(trade);
}
}
@action
@@ -127,6 +136,8 @@ abstract class ExchangeTradeViewModelBase with Store {
tradesStore.trade!.from.tag != null ? '${tradesStore.trade!.from.tag}' + ' ' : '';
final tagTo = tradesStore.trade!.to.tag != null ? '${tradesStore.trade!.to.tag}' + ' ' : '';
items.clear();
if(trade.provider != ExchangeProviderDescription.thorChain)
items.add(ExchangeTradeItem(
title: "${trade.provider.title} ${S.current.id}", data: '${trade.id}', isCopied: true));
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
@@ -9,6 +10,7 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/exchange_trade_state.dart';
import 'package:cake_wallet/exchange/limits.dart';
@@ -18,6 +20,7 @@ import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exolix_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/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
@@ -96,7 +99,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
/// if the provider is not in the user settings (user's first time or newly added provider)
/// then use its default value decided by us
selectedProviders = ObservableList.of(providersForCurrentPair()
selectedProviders = ObservableList.of(providerList
.where((element) => exchangeProvidersSelection[element.title] == null
? element.isEnabled
: (exchangeProvidersSelection[element.title] as bool))
@@ -148,6 +151,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
SimpleSwapExchangeProvider(),
TrocadorExchangeProvider(
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
ThorChainExchangeProvider(tradesStore: trades),
if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
];
@@ -496,8 +500,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
await provider.createTrade(request: request, isFixedRateMode: isFixedRateMode);
trade.walletId = wallet.id;
trade.fromWalletAddress = wallet.walletAddresses.address;
if (!isCanCreateTrade(trade)) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.thorchain_taproot_address_not_supported);
return;
}
tradesStore.setTrade(trade);
await trades.add(trade);
if (trade.provider != ExchangeProviderDescription.thorChain) await trades.add(trade);
tradeState = TradeIsCreatedSuccessfully(trade: trade);
/// return after the first successful trade
@@ -749,4 +761,17 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
int get depositMaxDigits => depositCurrency.decimals;
int get receiveMaxDigits => receiveCurrency.decimals;
bool isCanCreateTrade(Trade trade) {
if (trade.provider == ExchangeProviderDescription.thorChain) {
final payoutAddress = trade.payoutAddress ?? '';
final fromWalletAddress = trade.fromWalletAddress ?? '';
final tapRootPattern = RegExp(P2trAddress.regex.pattern);
if (tapRootPattern.hasMatch(payoutAddress) || tapRootPattern.hasMatch(fromWalletAddress)) {
return false;
}
}
return true;
}
}
+1 -1
View File
@@ -99,7 +99,7 @@ abstract class OrderDetailsViewModelBase with Store {
final buildURL = trackUrl + '${order.transferId}';
items.add(
TrackTradeListItem(
title: 'Track',
title: S.current.track,
value: buildURL,
onTap: () {
try {
+3
View File
@@ -66,6 +66,8 @@ abstract class OutputBase with Store {
@observable
String extractedAddress;
String? memo;
@computed
bool get isParsedAddress =>
parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty;
@@ -175,6 +177,7 @@ abstract class OutputBase with Store {
fiatAmount = '';
address = '';
note = '';
memo = null;
resetParsedAddress();
}
+9 -1
View File
@@ -2,6 +2,8 @@ import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/contact_record.dart';
@@ -296,14 +298,20 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
@action
Future<void> createTransaction() async {
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider}) async {
try {
state = IsExecutingState();
pendingTransaction = await wallet.createTransaction(_credentials());
if (provider is ThorChainExchangeProvider) {
final outputCount = pendingTransaction?.outputCount ?? 0;
if (outputCount > 10) throw Exception("ThorChain does not support more than 10 outputs");
}
state = ExecutedSuccessfullyState();
return pendingTransaction;
} catch (e) {
print('Failed with ${e.toString()}');
state = FailureState(e.toString());
return null;
}
}
+30 -28
View File
@@ -6,6 +6,7 @@ import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/exolix_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/thorchain_exchange.provider.dart';
import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -52,6 +53,9 @@ abstract class TradeDetailsViewModelBase with Store {
case ExchangeProviderDescription.exolix:
_provider = ExolixExchangeProvider();
break;
case ExchangeProviderDescription.thorChain:
_provider = ThorChainExchangeProvider(tradesStore: trades);
break;
}
_updateItems();
@@ -62,6 +66,24 @@ abstract class TradeDetailsViewModelBase with Store {
}
}
static String? getTrackUrl(ExchangeProviderDescription provider, Trade trade) {
switch (provider) {
case ExchangeProviderDescription.changeNow:
return 'https://changenow.io/exchange/txs/${trade.id}';
case ExchangeProviderDescription.sideShift:
return 'https://sideshift.ai/orders/${trade.id}';
case ExchangeProviderDescription.simpleSwap:
return 'https://simpleswap.io/exchange?id=${trade.id}';
case ExchangeProviderDescription.trocador:
return 'https://trocador.app/en/checkout/${trade.id}';
case ExchangeProviderDescription.exolix:
return 'https://exolix.com/transaction/${trade.id}';
case ExchangeProviderDescription.thorChain:
return 'https://track.ninerealms.com/${trade.id}';
}
return null;
}
final Box<Trade> trades;
@observable
@@ -125,46 +147,26 @@ abstract class TradeDetailsViewModelBase with Store {
items.add(StandartListItem(
title: S.current.trade_details_provider, value: trade.provider.toString()));
if (trade.provider == ExchangeProviderDescription.changeNow) {
final buildURL = 'https://changenow.io/exchange/txs/${trade.id.toString()}';
final trackUrl = TradeDetailsViewModelBase.getTrackUrl(trade.provider, trade);
if (trackUrl != null) {
items.add(TrackTradeListItem(
title: 'Track',
value: buildURL,
onTap: () {
_launchUrl(buildURL);
}));
title: S.current.track, value: trackUrl, onTap: () => _launchUrl(trackUrl)));
}
if (trade.provider == ExchangeProviderDescription.sideShift) {
final buildURL = 'https://sideshift.ai/orders/${trade.id.toString()}';
items.add(
TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => _launchUrl(buildURL)));
}
if (trade.provider == ExchangeProviderDescription.simpleSwap) {
final buildURL = 'https://simpleswap.io/exchange?id=${trade.id.toString()}';
items.add(
TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => _launchUrl(buildURL)));
if (trade.isRefund == true) {
items.add(StandartListItem(
title: 'Refund', value: trade.refundAddress ?? ''));
}
if (trade.provider == ExchangeProviderDescription.trocador) {
final buildURL = 'https://trocador.app/en/checkout/${trade.id.toString()}';
items.add(
TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => _launchUrl(buildURL)));
items.add(StandartListItem(
title: '${trade.providerName} ${S.current.id.toUpperCase()}',
value: trade.providerId ?? ''));
if (trade.password != null && trade.password!.isNotEmpty)
if (trade.password != null && trade.password!.isNotEmpty) {
items.add(StandartListItem(
title: '${trade.providerName} ${S.current.password}', value: trade.password ?? ''));
}
if (trade.provider == ExchangeProviderDescription.exolix) {
final buildURL = 'https://exolix.com/transaction/${trade.id.toString()}';
items.add(
TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => _launchUrl(buildURL)));
}
}
}