diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4c5ecc7f2..a7fa2608d 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -185,6 +185,8 @@ abstract class ElectrumWalletBase final hasMultiDestination = outputs.length > 1; var allInputsAmount = 0; + final String opReturnMemo = '=:ETH.ETH:0x2cd098e5662a01947d61ded62cc74782f51ac6f4'; + if (unspentCoins.isEmpty) { await updateUnspent(); } @@ -328,6 +330,8 @@ abstract class ElectrumWalletBase txb.addOutput(changeAddress, changeValue); } + txb.addOutputData(opReturnMemo); + for (var i = 0; i < inputs.length; i++) { final input = inputs[i]; final keyPair = generateKeyPair( diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index abfac3a6b..6d141cb54 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -22,8 +22,10 @@ class ExchangeProviderDescription extends EnumerableItem 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: 7, image: 'assets/images/exolix.png'); - static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); + static const all = ExchangeProviderDescription(title: 'All trades', raw: 8, image: ''); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -42,6 +44,8 @@ class ExchangeProviderDescription extends EnumerableItem with Serializable< case 6: return exolix; case 7: + return thorChain; + case 8: return all; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart new file mode 100644 index 000000000..f72b3dbca --- /dev/null +++ b/lib/exchange/provider/thorchain_exchange.provider.dart @@ -0,0 +1,160 @@ +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:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:http/http.dart' as http; + +class ThorChainExchangeProvider extends ExchangeProvider { + ThorChainExchangeProvider({required SettingsStore settingsStore}) + : _settingsStore = settingsStore, + super(pairList: supportedPairs(_notSupported)); + + static final List _notSupported = [ + ...(CryptoCurrency.all + .where((element) => ![CryptoCurrency.btc, CryptoCurrency.eth].contains(element)) + .toList()) + ]; + + static const _baseURL = 'https://thornode.ninerealms.com'; + static const _quotePath = '/thorchain/quote/swap'; + + final SettingsStore _settingsStore; + + @override + String get title => 'ThorChain'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.thorChain; + + @override + Future checkIsAvailable() async => true; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final params = { + 'from_asset': _normalizeCurrency(from), + 'to_asset': _normalizeCurrency(to), + 'amount': '100000000', + }; + + final url = Uri.parse('$_baseURL$_quotePath${Uri(queryParameters: params)}'); + final response = await http.get(url); + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseJSON = json.decode(response.body) as Map; + final minAmountIn = responseJSON['recommended_min_amount_in'] as String?; + final formattedMinAmountIn = minAmountIn != null ? double.parse(minAmountIn) / 100000000 : 0.0; + + return Limits(min: formattedMinAmountIn); + } + + @override + Future createTrade({required TradeRequest request, required bool isFixedRateMode}) async { + double amountBTC = double.parse(request.fromAmount); + int amountSatoshi = (amountBTC * 100000000).toInt(); + String formattedAmount = amountSatoshi.toString(); + + final params = { + 'from_asset': _normalizeCurrency(request.fromCurrency), + 'to_asset': _normalizeCurrency(request.toCurrency), + 'amount': formattedAmount, + 'destination': request.toAddress, + }; + final url = Uri.parse('$_baseURL$_quotePath${Uri(queryParameters: params)}'); + final response = await http.get(url); + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseJSON = json.decode(response.body) as Map; + final inputAddress = responseJSON['inbound_address'] as String?; + + return Trade( + id: 'id', + from: request.fromCurrency, + to: request.toCurrency, + provider: description, + inputAddress: inputAddress, + refundAddress: 'refundAddress', + extraId: 'extraId', + createdAt: DateTime.now(), + amount: request.fromAmount, + state: TradeState.created, + payoutAddress: request.toAddress); + } + + @override + Future 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': (amount * 100000000).toInt().toString(), + }; + + final url = Uri.parse('$_baseURL$_quotePath${Uri(queryParameters: params)}'); + final response = await http.get(url); + + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + + final responseJSON = json.decode(response.body) as Map; + print(responseJSON.toString()); + + final expectedAmountOutString = responseJSON['expected_amount_out'] as String? ?? '0'; + final expectedAmountOut = double.parse(expectedAmountOutString); + + double formattedAmountOut = expectedAmountOut / 1e9; + + return formattedAmountOut; + } catch (e) { + print(e.toString()); + return 0.0; + } + } + + @override + Future findTradeById({required String id}) { + throw UnimplementedError('findTradeById'); + } + + String _normalizeCurrency(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.btc: + return 'BTC.BTC'; + case CryptoCurrency.eth: + return 'ETH.ETH'; + default: + return currency.title.toLowerCase(); + } + } +} diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 7930c9c82..729f20ead 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -208,6 +208,7 @@ class ExchangeTradeState extends State { bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24), bottomSection: Observer(builder: (_) { final trade = widget.exchangeTradeViewModel.trade; + print('trade.amount: ${trade.amount}'); final sendingState = widget.exchangeTradeViewModel.sendViewModel.state; diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 93877a525..091bfda1e 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -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(settingsStore: sendViewModel.balanceViewModel.settingsStore); + break; } _updateItems(); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index afe617803..22ffe947c 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -18,6 +18,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'; @@ -147,6 +148,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with SimpleSwapExchangeProvider(), TrocadorExchangeProvider(useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), + ThorChainExchangeProvider(settingsStore: _settingsStore), if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), ]; diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 45502fd74..1ed88b275 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -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(settingsStore: settingsStore); + break; } _updateItems();