diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index a7fa2608d..634935669 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -185,7 +185,8 @@ abstract class ElectrumWalletBase final hasMultiDestination = outputs.length > 1; var allInputsAmount = 0; - final String opReturnMemo = '=:ETH.ETH:0x2cd098e5662a01947d61ded62cc74782f51ac6f4'; + final String? opReturnMemo = outputs.first.memo; + if (unspentCoins.isEmpty) { await updateUnspent(); @@ -330,7 +331,7 @@ abstract class ElectrumWalletBase txb.addOutput(changeAddress, changeValue); } - txb.addOutputData(opReturnMemo); + if (opReturnMemo != null) txb.addOutputData(opReturnMemo); for (var i = 0; i < inputs.length; i++) { final input = inputs[i]; diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index c23220423..7ce21b82f 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:bitbox/bitbox.dart' as bitbox; +import 'package:bitbox/src/utils/opcodes.dart' as bitboxOPCodes; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -110,6 +111,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { var allInputsAmount = 0; + final String? opReturnMemo = outputs.first.memo; + if (unspentCoins.isEmpty) await updateUnspent(); for (final utx in unspentCoins) { @@ -252,6 +255,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { txb.addOutput(changeAddress, changeValue); } + if (opReturnMemo != null) txb.addOutput(createOpReturnScript(opReturnMemo), 0); + for (var i = 0; i < inputs.length; i++) { final input = inputs[i]; final keyPair = generateKeyPair( @@ -260,7 +265,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { txb.sign(i, keyPair, input.value); } - // Build the transaction final tx = txb.build(); return PendingBitcoinCashTransaction(tx, type, @@ -326,4 +330,11 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { final HD = index == null ? hd : hd.derive(index); return base64Encode(HD.signMessage(message)); } + + Uint8List createOpReturnScript(String data) { + List script = []; + script.add(bitboxOPCodes.Opcodes.OP_RETURN); + script.addAll(utf8.encode(data)); + return Uint8List.fromList(script); + } } diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart index 6fd43dd82..9725bb9bb 100644 --- a/cw_core/lib/amount_converter.dart +++ b/cw_core/lib/amount_converter.dart @@ -81,6 +81,7 @@ class AmountConverter { return _moneroAmountToString(amount); case CryptoCurrency.btc: case CryptoCurrency.bch: + case CryptoCurrency.ltc: return _bitcoinAmountToString(amount); case CryptoCurrency.xhv: case CryptoCurrency.xag: @@ -102,27 +103,44 @@ class AmountConverter { } } + static int amountToSmallestUnit( + {required CryptoCurrency cryptoCurrency, required double amount}) { + switch (cryptoCurrency) { + case CryptoCurrency.btc: + return (amount * _bitcoinAmountDivider).toInt(); + case CryptoCurrency.eth: + return (amount * _ethereumAmountDivider).toInt(); + case CryptoCurrency.bch: + return (amount * _bitcoinCashAmountDivider).toInt(); + case CryptoCurrency.ltc: + return (amount * _litecoinAmountDivider).toInt(); + case CryptoCurrency.dash: + return (amount * _dashAmountDivider).toInt(); + case CryptoCurrency.xmr: + return (amount * _moneroAmountDivider).toInt(); + default: + return 0; + } + } + static double cryptoAmountToDouble({required num amount, required num divider}) => amount / divider; - static String _moneroAmountToString(int amount) => _moneroAmountFormat.format( - cryptoAmountToDouble(amount: amount, divider: _moneroAmountDivider)); + static String _moneroAmountToString(int amount) => _moneroAmountFormat + .format(cryptoAmountToDouble(amount: amount, divider: _moneroAmountDivider)); static double _moneroAmountToDouble(int amount) => cryptoAmountToDouble(amount: amount, divider: _moneroAmountDivider); - static int _moneroParseAmount(String amount) => - _moneroAmountFormat.parse(amount).toInt(); + static int _moneroParseAmount(String amount) => _moneroAmountFormat.parse(amount).toInt(); - static String _bitcoinAmountToString(int amount) => - _bitcoinAmountFormat.format( - cryptoAmountToDouble(amount: amount, divider: _bitcoinAmountDivider)); + static String _bitcoinAmountToString(int amount) => _bitcoinAmountFormat + .format(cryptoAmountToDouble(amount: amount, divider: _bitcoinAmountDivider)); static double _bitcoinAmountToDouble(int amount) => cryptoAmountToDouble(amount: amount, divider: _bitcoinAmountDivider); - static int _doubleToBitcoinAmount(double amount) => - (amount * _bitcoinAmountDivider).toInt(); + static int _doubleToBitcoinAmount(double amount) => (amount * _bitcoinAmountDivider).toInt(); static double _bitcoinCashAmountToDouble(int amount) => cryptoAmountToDouble(amount: amount, divider: _bitcoinCashAmountDivider); diff --git a/cw_core/lib/output_info.dart b/cw_core/lib/output_info.dart index e2b1201a8..9e3ac4ffc 100644 --- a/cw_core/lib/output_info.dart +++ b/cw_core/lib/output_info.dart @@ -7,7 +7,8 @@ class OutputInfo { this.formattedCryptoAmount, this.fiatAmount, this.note, - this.extractedAddress,}); + this.extractedAddress, + this.memo}); final String? fiatAmount; final String? cryptoAmount; @@ -17,4 +18,5 @@ class OutputInfo { final bool sendAll; final bool isParsedAddress; final int? formattedCryptoAmount; + final String? memo; } \ No newline at end of file diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart index fccbf778d..6c3739c11 100644 --- a/cw_ethereum/lib/ethereum_client.dart +++ b/cw_ethereum/lib/ethereum_client.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:cw_core/crypto_currency.dart'; +import 'package:hex/hex.dart' as hex; import 'package:cw_ethereum/erc20_balance.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_ethereum/ethereum_transaction_model.dart'; @@ -73,6 +74,7 @@ class EthereumClient { required CryptoCurrency currency, required int exponent, String? contractAddress, + String? data, }) async { assert(currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly || @@ -88,6 +90,7 @@ class EthereumClient { to: EthereumAddress.fromHex(toAddress), maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), amount: _isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), + data: data != null ? hexToBytes(data) : null, ); final signedTransaction = @@ -123,6 +126,13 @@ class EthereumClient { ); } + Uint8List hexToBytes(String hexString) { + if (hexString.startsWith('0x')) { + hexString = hexString.substring(2); + } + return Uint8List.fromList(hex.HEX.decode(hexString)); + } + int get chainId => 1; Transaction createTransaction({ @@ -130,12 +140,14 @@ class EthereumClient { required EthereumAddress to, required EtherAmount amount, EtherAmount? maxPriorityFeePerGas, + Uint8List? data, }) { return Transaction( from: from, to: to, maxPriorityFeePerGas: maxPriorityFeePerGas, value: amount, + data: data, ); } diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index cd4bd84cc..40175279c 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -197,6 +197,8 @@ abstract class EthereumWalletBase final outputs = _credentials.outputs; final hasMultiDestination = outputs.length > 1; + final String? opReturnMemo = outputs.first.memo; + final CryptoCurrency transactionCurrency = balance.keys.firstWhere((element) => element.title == _credentials.currency.title); @@ -240,6 +242,11 @@ abstract class EthereumWalletBase } } + String? hexOpReturnMemo; + if (opReturnMemo != null) { + hexOpReturnMemo = '0x' + opReturnMemo.codeUnits.map((char) => char.toRadixString(16).padLeft(2, '0')).join(); + } + final pendingEthereumTransaction = await _client.signTransaction( privateKey: _ethPrivateKey, toAddress: _credentials.outputs.first.isParsedAddress @@ -252,6 +259,7 @@ abstract class EthereumWalletBase exponent: exponent, contractAddress: transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null, + data: hexOpReturnMemo, ); return pendingEthereumTransaction; diff --git a/cw_polygon/lib/polygon_client.dart b/cw_polygon/lib/polygon_client.dart index 876f4c60d..896602f27 100644 --- a/cw_polygon/lib/polygon_client.dart +++ b/cw_polygon/lib/polygon_client.dart @@ -13,6 +13,8 @@ class PolygonClient extends EthereumClient { required EthereumAddress to, required EtherAmount amount, EtherAmount? maxPriorityFeePerGas, + Uint8List? data, + }) { return Transaction( from: from, diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index dd713fd15..aa89b2747 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -79,7 +79,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); diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index abafc2f26..860958d71 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -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 EthereumTransactionPriority, currency: currency, diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart index f72b3dbca..ea8abc5d9 100644 --- a/lib/exchange/provider/thorchain_exchange.provider.dart +++ b/lib/exchange/provider/thorchain_exchange.provider.dart @@ -7,25 +7,28 @@ 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/amount_converter.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)); + ThorChainExchangeProvider() : super(pairList: supportedPairs(_notSupported)); static final List _notSupported = [ ...(CryptoCurrency.all - .where((element) => ![CryptoCurrency.btc, CryptoCurrency.eth].contains(element)) + .where((element) => ![ + CryptoCurrency.btc, + CryptoCurrency.eth, + CryptoCurrency.ltc, + CryptoCurrency.bch + ].contains(element)) .toList()) ]; static const _baseURL = 'https://thornode.ninerealms.com'; static const _quotePath = '/thorchain/quote/swap'; - - final SettingsStore _settingsStore; + static const _affiliateName = 'cakewallet'; + static const _affiliateBps = '0'; @override String get title => 'ThorChain'; @@ -45,6 +48,21 @@ class ThorChainExchangeProvider extends ExchangeProvider { @override Future checkIsAvailable() async => true; + Future> _getSwapQuote(Map params) async { + final uri = Uri.parse('$_baseURL$_quotePath${Uri(queryParameters: 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; + } + @override Future fetchLimits( {required CryptoCurrency from, @@ -53,18 +71,14 @@ class ThorChainExchangeProvider extends ExchangeProvider { final params = { 'from_asset': _normalizeCurrency(from), 'to_asset': _normalizeCurrency(to), - 'amount': '100000000', + 'amount': AmountConverter.amountToSmallestUnit(cryptoCurrency: from, amount: 1).toString(), + 'affiliate': _affiliateName, + 'affiliate_bps': _affiliateBps, }; - 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 responseJSON = await _getSwapQuote(params); final minAmountIn = responseJSON['recommended_min_amount_in'] as String?; - final formattedMinAmountIn = minAmountIn != null ? double.parse(minAmountIn) / 100000000 : 0.0; + final formattedMinAmountIn = minAmountIn != null ? int.parse(minAmountIn) / 1e8 : 0.0; return Limits(min: formattedMinAmountIn); } @@ -72,23 +86,23 @@ class ThorChainExchangeProvider extends ExchangeProvider { @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(); + String formattedAmount = AmountConverter.amountToSmallestUnit( + cryptoCurrency: request.fromCurrency, amount: amountBTC) + .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); + 'affiliate': _affiliateName, + 'affiliate_bps': _affiliateBps}; - if (response.statusCode != 200) - throw Exception('Unexpected http status: ${response.statusCode}'); + final responseJSON = await _getSwapQuote(params); - final responseJSON = json.decode(response.body) as Map; + print('createTrade _ responseJSON________: $responseJSON'); final inputAddress = responseJSON['inbound_address'] as String?; + final memo = responseJSON['memo'] as String?; return Trade( id: 'id', @@ -101,7 +115,8 @@ class ThorChainExchangeProvider extends ExchangeProvider { createdAt: DateTime.now(), amount: request.fromAmount, state: TradeState.created, - payoutAddress: request.toAddress); + payoutAddress: request.toAddress, + memo: memo); } @override @@ -117,23 +132,22 @@ class ThorChainExchangeProvider extends ExchangeProvider { final params = { 'from_asset': _normalizeCurrency(from), 'to_asset': _normalizeCurrency(to), - 'amount': (amount * 100000000).toInt().toString(), + 'amount': + AmountConverter.amountToSmallestUnit(cryptoCurrency: from, amount: amount).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 responseJSON = await _getSwapQuote(params); + print(responseJSON); final expectedAmountOutString = responseJSON['expected_amount_out'] as String? ?? '0'; final expectedAmountOut = double.parse(expectedAmountOutString); + double formattedAmountOut = 0.0; - double formattedAmountOut = expectedAmountOut / 1e9; + if (to == CryptoCurrency.eth) { + formattedAmountOut = expectedAmountOut / 1e9; + } else { + formattedAmountOut = AmountConverter.amountIntToDouble(to, expectedAmountOut.toInt()); + } return formattedAmountOut; } catch (e) { @@ -153,6 +167,10 @@ class ThorChainExchangeProvider extends ExchangeProvider { return 'BTC.BTC'; case CryptoCurrency.eth: return 'ETH.ETH'; + case CryptoCurrency.ltc: + return 'LTC.LTC'; + case CryptoCurrency.bch: + return 'BCH.BCH'; default: return currency.title.toLowerCase(); } diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index 4eb48c248..4e4420629 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -27,7 +27,8 @@ class Trade extends HiveObject { this.password, this.providerId, this.providerName, - this.fromWalletAddress + this.fromWalletAddress, + this.memo, }) { if (provider != null) providerRaw = provider.raw; @@ -105,6 +106,9 @@ class Trade extends HiveObject { @HiveField(17) String? fromWalletAddress; + @HiveField(18) + String? memo; + static Trade fromMap(Map map) { return Trade( id: map['id'] as String, @@ -115,7 +119,8 @@ 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? ); } @@ -128,7 +133,8 @@ 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 }; } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 091bfda1e..b85776f71 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -49,7 +49,7 @@ abstract class ExchangeTradeViewModelBase with Store { _provider = ExolixExchangeProvider(); break; case ExchangeProviderDescription.thorChain: - _provider = ThorChainExchangeProvider(settingsStore: sendViewModel.balanceViewModel.settingsStore); + _provider = ThorChainExchangeProvider(); break; } @@ -104,6 +104,7 @@ 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(); } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 22ffe947c..f96bd39f0 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -148,7 +148,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with SimpleSwapExchangeProvider(), TrocadorExchangeProvider(useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), - ThorChainExchangeProvider(settingsStore: _settingsStore), + ThorChainExchangeProvider(), if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), ]; diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 73fb535f2..12c5d1f87 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -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; @@ -176,6 +178,7 @@ abstract class OutputBase with Store { fiatAmount = ''; address = ''; note = ''; + memo = null; resetParsedAddress(); } diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 1ed88b275..f5aa2122a 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -54,7 +54,7 @@ abstract class TradeDetailsViewModelBase with Store { _provider = ExolixExchangeProvider(); break; case ExchangeProviderDescription.thorChain: - _provider = ThorChainExchangeProvider(settingsStore: settingsStore); + _provider = ThorChainExchangeProvider(); break; }