From 94216e698775acf51e60ebcd712327432c4571ea Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Fri, 2 Jun 2023 04:02:43 +0300 Subject: [PATCH] Add send ERC-20 tokens initial flow --- cw_core/lib/crypto_currency.dart | 3 +- cw_ethereum/lib/ethereum_client.dart | 54 +- .../lib/ethereum_transaction_credentials.dart | 9 +- cw_ethereum/lib/ethereum_wallet.dart | 26 +- lib/core/address_validator.dart | 2 + lib/ethereum/cw_ethereum.dart | 26 +- lib/reactions/fiat_rate_update.dart | 12 + .../exchange/widgets/exchange_card.dart | 21 +- lib/src/screens/send/widgets/send_card.dart | 745 +++++++++--------- lib/view_model/send/send_view_model.dart | 22 +- tool/configure.dart | 16 +- 11 files changed, 515 insertions(+), 421 deletions(-) diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 6d4d86a57..698631af4 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -67,6 +67,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.uni, CryptoCurrency.stx, CryptoCurrency.btcln, + CryptoCurrency.shib, ]; static const havenCurrencies = [ @@ -152,7 +153,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const uni = CryptoCurrency(title: 'UNI', tag: 'ETH', fullName: 'Uniswap', raw: 60, name: 'uni', iconPath: 'assets/images/uni_icon.png'); static const stx = CryptoCurrency(title: 'STX', fullName: 'Stacks', raw: 61, name: 'stx', iconPath: 'assets/images/stx_icon.png'); static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/btc.png'); - static const shib = CryptoCurrency(title: 'SHIB', tag: 'ETH', fullName: 'SHIBA INU', raw: 63, name: 'shib', iconPath: 'assets/images/shib.png'); // TODO: add image + static const shib = CryptoCurrency(title: 'SHIB', tag: 'ETH', fullName: 'SHIBA INU', raw: 63, name: 'shib', iconPath: 'assets/images/shib_icon.png'); static final Map _rawCurrencyMap = diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart index ea2b312e1..378d2d13b 100644 --- a/cw_ethereum/lib/ethereum_client.dart +++ b/cw_ethereum/lib/ethereum_client.dart @@ -14,6 +14,8 @@ class EthereumClient { CryptoCurrency.shib: "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", }; + Map get erc20Currencies => _erc20Currencies; + Web3Client? _client; bool connect(Node node) { @@ -47,13 +49,14 @@ class EthereumClient { return result.map((e) => e.toInt()).toList(); } - Future signTransaction( - EthPrivateKey privateKey, - String toAddress, - String amount, - int gas, - EthereumTransactionPriority priority, - ) async { + Future signTransaction({ + required EthPrivateKey privateKey, + required String toAddress, + required String amount, + required int gas, + required EthereumTransactionPriority priority, + required CryptoCurrency currency, + }) async { final estimatedGas = await _client!.estimateGas( maxPriorityFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip), maxFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, 100), @@ -64,13 +67,36 @@ class EthereumClient { final price = await _client!.getGasPrice(); - final transaction = Transaction( - from: privateKey.address, - to: EthereumAddress.fromHex(toAddress), - maxGas: gas, - gasPrice: price, - value: EtherAmount.inWei(BigInt.parse(amount)), - ); + final Transaction transaction; + + if (erc20Currencies.containsKey(currency)) { + final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json"); + final contractAbi = ContractAbi.fromJson(abi, "ERC20"); + + final contract = DeployedContract( + contractAbi, + EthereumAddress.fromHex(_erc20Currencies[currency]!), + ); + + final transferFunction = contract.function('transfer'); + transaction = Transaction.callContract( + contract: contract, + function: transferFunction, + parameters: [EthereumAddress.fromHex(toAddress), BigInt.parse(amount)], + from: privateKey.address, + maxGas: gas, + gasPrice: price, + value: EtherAmount.inWei(BigInt.parse(amount)), + ); + } else { + transaction = Transaction( + from: privateKey.address, + to: EthereumAddress.fromHex(toAddress), + maxGas: gas, + gasPrice: price, + value: EtherAmount.inWei(BigInt.parse(amount)), + ); + } final signedTransaction = await _client!.signTransaction(privateKey, transaction); diff --git a/cw_ethereum/lib/ethereum_transaction_credentials.dart b/cw_ethereum/lib/ethereum_transaction_credentials.dart index 0a38e8e79..b015b7141 100644 --- a/cw_ethereum/lib/ethereum_transaction_credentials.dart +++ b/cw_ethereum/lib/ethereum_transaction_credentials.dart @@ -1,10 +1,17 @@ +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/output_info.dart'; import 'package:cw_ethereum/ethereum_transaction_priority.dart'; class EthereumTransactionCredentials { - EthereumTransactionCredentials(this.outputs, {required this.priority, this.feeRate}); + EthereumTransactionCredentials( + this.outputs, { + required this.priority, + required this.currency, + this.feeRate, + }); final List outputs; final EthereumTransactionPriority? priority; final int? feeRate; + final CryptoCurrency currency; } diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index f9831909a..1a4b57e63 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -123,7 +123,7 @@ abstract class EthereumWalletBase final _credentials = credentials as EthereumTransactionCredentials; final outputs = _credentials.outputs; final hasMultiDestination = outputs.length > 1; - final balance = await _client.getBalance(_privateKey.address); + final _erc20Balance = balance[_credentials.currency]!; int totalAmount = 0; if (hasMultiDestination) { @@ -133,27 +133,28 @@ abstract class EthereumWalletBase totalAmount = outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); - if (balance.getInWei < EtherAmount.inWei(totalAmount as BigInt).getInWei) { + if (_erc20Balance.balance < EtherAmount.inWei(totalAmount as BigInt).getInWei) { throw EthereumTransactionCreationException(); } } else { final output = outputs.first; - final int allAmount = balance.getInWei.toInt() - feeRate(_credentials.priority!); + final int allAmount = _erc20Balance.balance.toInt() - feeRate(_credentials.priority!); totalAmount = output.sendAll ? allAmount : output.formattedCryptoAmount ?? 0; if ((output.sendAll && - balance.getInWei < EtherAmount.inWei(totalAmount as BigInt).getInWei) || - (!output.sendAll && balance.getInWei.toInt() <= 0)) { + _erc20Balance.balance < EtherAmount.inWei(totalAmount as BigInt).getInWei) || + (!output.sendAll && _erc20Balance.balance.toInt() <= 0)) { throw EthereumTransactionCreationException(); } } final pendingEthereumTransaction = await _client.signTransaction( - _privateKey, - _credentials.outputs.first.address, - totalAmount.toString(), - _priorityFees[_credentials.priority!.raw], - _credentials.priority!, + privateKey: _privateKey, + toAddress: _credentials.outputs.first.address, + amount: totalAmount.toString(), + gas: _priorityFees[_credentials.priority!.raw], + priority: _credentials.priority!, + currency: _credentials.currency, ); return pendingEthereumTransaction; @@ -233,8 +234,7 @@ abstract class EthereumWalletBase final jsonSource = await read(path: path, password: password); final data = json.decode(jsonSource) as Map; final mnemonic = data['mnemonic'] as String; - final balance = - ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero); + final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero); return EthereumWallet( walletInfo: walletInfo, @@ -268,4 +268,6 @@ abstract class EthereumWalletBase } Future? updateBalance() => null; + + List get erc20Currencies => _client.erc20Currencies.keys.toList(); } diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 064efa11b..6ddb2b139 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -36,6 +36,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.oxt: case CryptoCurrency.paxg: case CryptoCurrency.uni: + case CryptoCurrency.shib: return '0x[0-9a-zA-Z]'; case CryptoCurrency.xrp: return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; @@ -118,6 +119,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.eos: return [42]; case CryptoCurrency.eth: + case CryptoCurrency.shib: return [42]; case CryptoCurrency.ltc: return [34, 43, 63]; diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 6aa5c614d..bd253dbfb 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -41,8 +41,12 @@ class CWEthereum extends Ethereum { return ethereumWallet.feeRate(priority); } - Object createEthereumTransactionCredentials(List outputs, - {required TransactionPriority priority, int? feeRate}) => + Object createEthereumTransactionCredentials( + List outputs, { + required TransactionPriority priority, + required CryptoCurrency currency, + int? feeRate, + }) => EthereumTransactionCredentials( outputs .map((out) => OutputInfo( @@ -56,17 +60,29 @@ class CWEthereum extends Ethereum { formattedCryptoAmount: out.formattedCryptoAmount)) .toList(), priority: priority as EthereumTransactionPriority, + currency: currency, feeRate: feeRate, ); - Object createEthereumTransactionCredentialsRaw(List outputs, - {TransactionPriority? priority, required int feeRate}) => + Object createEthereumTransactionCredentialsRaw( + List outputs, { + TransactionPriority? priority, + required CryptoCurrency currency, + required int feeRate, + }) => EthereumTransactionCredentials( outputs, - priority: priority as EthereumTransactionPriority, + priority: priority as EthereumTransactionPriority?, + currency: currency, feeRate: feeRate, ); @override int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount); + + @override + List getERC20Currencies(Object wallet) { + final ethereumWallet = wallet as EthereumWallet; + return ethereumWallet.erc20Currencies; + } } diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index f9ddbef52..d4f143eb5 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -30,6 +31,17 @@ Future startFiatRateUpdate( fiat: settingsStore.fiatCurrency, torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); } + + if (appStore.wallet!.type == WalletType.ethereum) { + final currencies = ethereum!.getERC20Currencies(appStore.wallet!); + + for (final currency in currencies) { + fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice( + crypto: currency, + fiat: settingsStore.fiatCurrency, + torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); + } + } } catch (e) { print(e); } diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index f77e885c2..ce8928cc6 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -511,17 +511,16 @@ class ExchangeCardState extends State { void _presentPicker(BuildContext context) { showPopUp( - builder: (_) => CurrencyPicker( - selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), - items: widget.currencies, - hintText: S.of(context).search_currency, - isMoneroWallet: _isMoneroWallet, - isConvertFrom: widget.hasRefundAddress, - onItemSelected: (Currency item) => - widget.onCurrencySelected != null - ? widget.onCurrencySelected(item as CryptoCurrency) - : null), - context: context); + context: context, + builder: (_) => CurrencyPicker( + selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), + items: widget.currencies, + hintText: S.of(context).search_currency, + isMoneroWallet: _isMoneroWallet, + isConvertFrom: widget.hasRefundAddress, + onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency), + ), + ); } void _showAmountPopup(BuildContext context, PaymentRequest paymentRequest) { diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 96045b1bd..69fd3a3e6 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -1,7 +1,10 @@ import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; @@ -32,18 +35,14 @@ class SendCard extends StatefulWidget { @override SendCardState createState() => SendCardState( - output: output, - sendViewModel: sendViewModel, - initialPaymentRequest: initialPaymentRequest, - ); + output: output, + sendViewModel: sendViewModel, + initialPaymentRequest: initialPaymentRequest, + ); } -class SendCardState extends State - with AutomaticKeepAliveClientMixin { - SendCardState({ - required this.output, - required this.sendViewModel, - this.initialPaymentRequest}) +class SendCardState extends State with AutomaticKeepAliveClientMixin { + SendCardState({required this.output, required this.sendViewModel, this.initialPaymentRequest}) : addressController = TextEditingController(), cryptoAmountController = TextEditingController(), fiatAmountController = TextEditingController(), @@ -100,40 +99,39 @@ class SendCardState extends State return Stack( children: [ KeyboardActions( - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context) - .accentTextTheme! - .bodyLarge! - .backgroundColor!, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: cryptoAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ), - KeyboardActionsItem( - focusNode: fiatAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ) - ]), - child: Container( - height: 0, - color: Colors.transparent, - )), + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: cryptoAmountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ), + KeyboardActionsItem( + focusNode: fiatAmountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ) + ], + ), + child: Container( + height: 0, + color: Colors.transparent, + ), + ), Container( - decoration: ResponsiveLayoutUtil.instance.isMobile(context) ? BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24)), - gradient: LinearGradient(colors: [ - Theme.of(context).primaryTextTheme!.titleMedium!.color!, - Theme.of(context) - .primaryTextTheme! - .titleMedium! - .decorationColor!, - ], begin: Alignment.topLeft, end: Alignment.bottomRight), - ) : null, + decoration: ResponsiveLayoutUtil.instance.isMobile(context) + ? BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + gradient: LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.titleMedium!.color!, + Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!, + ], begin: Alignment.topLeft, end: Alignment.bottomRight), + ) + : null, child: Padding( padding: EdgeInsets.fromLTRB( 24, @@ -142,7 +140,8 @@ class SendCardState extends State ResponsiveLayoutUtil.instance.isMobile(context) ? 32 : 0, ), child: SingleChildScrollView( - child: Observer(builder: (_) => Column( + child: Observer( + builder: (_) => Column( mainAxisSize: MainAxisSize.min, children: [ Observer(builder: (_) { @@ -164,24 +163,15 @@ class SendCardState extends State AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook ], - buttonColor: Theme.of(context) - .primaryTextTheme! - .headlineMedium! - .color!, - borderColor: Theme.of(context) - .primaryTextTheme! - .headlineSmall! - .color!, + buttonColor: Theme.of(context).primaryTextTheme.headlineMedium!.color!, + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), + fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), hintStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Theme.of(context) - .primaryTextTheme! - .headlineSmall! + .primaryTextTheme.headlineSmall! .decorationColor!), onPushPasteButton: (context) async { output.resetParsedAddress(); @@ -195,181 +185,209 @@ class SendCardState extends State selectedCurrency: sendViewModel.currency, ); }), - if (output.isParsedAddress) Padding( - padding: const EdgeInsets.only(top: 20), - child: BaseTextFormField( - controller: extractedAddressController, - readOnly: true, - borderColor: Theme.of(context) - .primaryTextTheme! - .headlineSmall! - .color!, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - validator: sendViewModel.addressValidator - ) - ), + if (output.isParsedAddress) + Padding( + padding: const EdgeInsets.only(top: 20), + child: BaseTextFormField( + controller: extractedAddressController, + readOnly: true, + borderColor: + Theme.of(context).primaryTextTheme.headlineSmall!.color!, + textStyle: TextStyle( + fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), + validator: sendViewModel.addressValidator)), Observer( - builder: (_) => Padding( - padding: const EdgeInsets.only(top: 20), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( - children: [ - Text( - sendViewModel.selectedCryptoCurrency.title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), - sendViewModel.selectedCryptoCurrency.tag != null ? Padding( - padding: const EdgeInsets.fromLTRB(3.0,0,3.0,0), - child: Container( - height: 32, - decoration: BoxDecoration( - color: Theme.of(context) - .primaryTextTheme! - .headlineMedium! - .color!, - borderRadius: - BorderRadius.all(Radius.circular(6))), - child: Center( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( sendViewModel.selectedCryptoCurrency.tag!, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .primaryTextTheme! - .headlineMedium! - .decorationColor!)), + builder: (_) => Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + sendViewModel.hasMultipleTokens + ? Container( + padding: EdgeInsets.only(right: 8), + height: 32, + child: InkWell( + onTap: () => _presentPicker(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.only(right: 5), + child: Image.asset( + 'assets/images/arrow_bottom_purple_icon.png', + color: Colors.white, + height: 8, + ), + ), + Text( + sendViewModel.selectedCryptoCurrency.title, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.white), + ), + ], + ), ), - ), - ), - ) : Container(), - Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text(':', + ) + : Text( + sendViewModel.selectedCryptoCurrency.title, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16, - color: Colors.white)), - ), - ], - ), - ), - Expanded( - child: Stack( - children: [ - BaseTextFormField( - focusNode: cryptoAmountFocus, - controller: cryptoAmountController, - keyboardType: - TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) - ], - suffixIcon: SizedBox( - width: prefixIconWidth, - ), - hintText: '0.0000', - borderColor: Colors.transparent, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context) - .primaryTextTheme! - .headlineSmall! - .decorationColor!, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: output.sendAll - ? sendViewModel.allAmountValidator - : sendViewModel - .amountValidator), - if (!sendViewModel.isBatchSending) Positioned( - top: 2, - right: 0, + ), + sendViewModel.selectedCryptoCurrency.tag != null + ? Padding( + padding: const EdgeInsets.fromLTRB(3.0, 0, 3.0, 0), child: Container( - width: prefixIconWidth, - height: prefixIconHeight, - child: InkWell( - onTap: () async => - output.setSendAll(), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .primaryTextTheme! - .headlineMedium! - .color!, - borderRadius: - BorderRadius.all( - Radius.circular(6))), - child: Center( - child: Text( - S.of(context).all, - textAlign: - TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.bold, - color: - Theme.of(context) - .primaryTextTheme! - .headlineMedium! - .decorationColor!))), - ))))]), - ), - ], - ) - )), - Divider(height: 1,color: Theme.of(context) - .primaryTextTheme! - .headlineSmall! - .decorationColor!), - Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(top: 10), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - S.of(context).available_balance + + height: 32, + decoration: BoxDecoration( + color: Theme.of(context) + .primaryTextTheme.headlineMedium! + .color!, + borderRadius: BorderRadius.all( + Radius.circular(6), + )), + child: Center( + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + sendViewModel.selectedCryptoCurrency.tag!, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme.headlineMedium! + .decorationColor!), + ), + ), + ), + ), + ) + : Container(), + Padding( + padding: const EdgeInsets.only(right: 10.0), + child: Text( ':', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .primaryTextTheme! - .headlineSmall! - .decorationColor!), - )), - Text( - sendViewModel.balance, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.white), + ), + ), + ], + ), + ), + Expanded( + child: Stack( + children: [ + BaseTextFormField( + focusNode: cryptoAmountFocus, + controller: cryptoAmountController, + keyboardType: TextInputType.numberWithOptions( + signed: false, decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) + ], + suffixIcon: SizedBox( + width: prefixIconWidth, + ), + hintText: '0.0000', + borderColor: Colors.transparent, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme.headlineSmall! + .decorationColor!, + fontWeight: FontWeight.w500, + fontSize: 14), + validator: output.sendAll + ? sendViewModel.allAmountValidator + : sendViewModel.amountValidator, + ), + if (!sendViewModel.isBatchSending) + Positioned( + top: 2, + right: 0, + child: Container( + width: prefixIconWidth, + height: prefixIconHeight, + child: InkWell( + onTap: () async => output.setSendAll(), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .primaryTextTheme.headlineMedium! + .color!, + borderRadius: BorderRadius.all( + Radius.circular(6), + ), + ), + child: Center( + child: Text( + S.of(context).all, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme.headlineMedium! + .decorationColor!, + ), + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], + )), + ), + Divider( + height: 1, + color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!), + Observer( + builder: (_) => Padding( + padding: EdgeInsets.only(top: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + S.of(context).available_balance + ':', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Theme.of(context) - .primaryTextTheme! - .headlineSmall! + .primaryTextTheme.headlineSmall! .decorationColor!), - ) - ], - ), - )), + ), + ), + Text( + sendViewModel.balance, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme.headlineSmall! + .decorationColor!), + ) + ], + ), + ), + ), if (!sendViewModel.isFiatDisabled) Padding( padding: const EdgeInsets.only(top: 20), @@ -377,171 +395,154 @@ class SendCardState extends State focusNode: fiatAmountFocus, controller: fiatAmountController, keyboardType: - TextInputType.numberWithOptions( - signed: false, decimal: true), + TextInputType.numberWithOptions(signed: false, decimal: true), inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) + FilteringTextInputFormatter.deny( + RegExp('[\\-|\\ ]'), + ) ], prefixIcon: Padding( padding: EdgeInsets.only(top: 9), - child: - Text(sendViewModel.fiat.title + ':', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), + child: Text( + sendViewModel.fiat.title + ':', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), ), hintText: '0.00', - borderColor: Theme.of(context) - .primaryTextTheme! - .headlineSmall! - .color!, + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), + fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), placeholderTextStyle: TextStyle( color: Theme.of(context) - .primaryTextTheme!.headlineSmall!.decorationColor!, + .primaryTextTheme.headlineSmall! + .decorationColor!, fontWeight: FontWeight.w500, fontSize: 14), - )), + ), + ), Padding( padding: EdgeInsets.only(top: 20), child: BaseTextFormField( controller: noteController, keyboardType: TextInputType.multiline, maxLines: null, - borderColor: Theme.of(context) - .primaryTextTheme! - .headlineSmall! - .color!, + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), + fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), hintText: S.of(context).note_optional, placeholderTextStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Theme.of(context) - .primaryTextTheme! - .headlineSmall! + .primaryTextTheme.headlineSmall! .decorationColor!), ), ), Observer( - builder: (_) => GestureDetector( - onTap: () => - _setTransactionPriority(context), - child: Container( - padding: EdgeInsets.only(top: 24), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S - .of(context) - .send_estimated_fee, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w500, - //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, - color: Colors.white)), - Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - output - .estimatedFee - .toString() + - ' ' + - sendViewModel - .selectedCryptoCurrency.toString(), - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w600, - //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, - color: - Colors.white)), - Padding( - padding: - EdgeInsets.only(top: 5), - child: sendViewModel.isFiatDisabled - ? const SizedBox(height: 14) - : Text(output - .estimatedFeeFiatAmount - + ' ' + - sendViewModel - .fiat.title, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w600, - color: Theme - .of(context) - .primaryTextTheme! - .headlineSmall! - .decorationColor!)) + builder: (_) => GestureDetector( + onTap: () => _setTransactionPriority(context), + child: Container( + padding: EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).send_estimated_fee, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, + color: Colors.white), + ), + Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + output.estimatedFee.toString() + + ' ' + + sendViewModel.selectedCryptoCurrency.toString(), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, + color: Colors.white, ), - ], - ), - Padding( - padding: EdgeInsets.only( - top: 2, - left: 5), - child: Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white, ), - ) - ], - ), - ) + Padding( + padding: EdgeInsets.only(top: 5), + child: sendViewModel.isFiatDisabled + ? const SizedBox(height: 14) + : Text( + output.estimatedFeeFiatAmount + + ' ' + + sendViewModel.fiat.title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme.headlineSmall! + .decorationColor!, + ), + ), + ), + ], + ), + Padding( + padding: EdgeInsets.only(top: 2, left: 5), + child: Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ), + ) + ], + ), + ) + ], + ), + ), + ), + ), + if (sendViewModel.isElectrumWallet) + Padding( + padding: EdgeInsets.only(top: 6), + child: GestureDetector( + onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList), + child: Container( + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).coin_control, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white), + ), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ), ], ), ), - )), - if (sendViewModel.isElectrumWallet) Padding( - padding: EdgeInsets.only(top: 6), - child: GestureDetector( - onTap: () => Navigator.of(context) - .pushNamed(Routes.unspentCoinsList), - child: Container( - color: Colors.transparent, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).coin_control, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.white)), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white, - ) - ], - ) - ) - ) - ) + ), + ), ], - )) + ), + ), ), ), ) @@ -550,10 +551,10 @@ class SendCardState extends State } void _setEffects(BuildContext context) { - if (_effectsInstalled) { + if (_effectsInstalled) { return; } - + if (output.address.isNotEmpty) { addressController.text = output.address; } @@ -586,7 +587,7 @@ class SendCardState extends State }); noteController.addListener(() { - final note = noteController.text ?? ''; + final note = noteController.text; if (note != output.note) { output.note = note; @@ -662,18 +663,32 @@ class SendCardState extends State final selectedItem = items.indexOf(sendViewModel.transactionPriority); await showPopUp( - builder: (_) => Picker( - items: items, - displayItem: sendViewModel.displayFeeRate, - selectedAtIndex: selectedItem, - title: S.of(context).please_select, - mainAxisAlignment: MainAxisAlignment.center, - onItemSelected: (TransactionPriority priority) => - sendViewModel.setTransactionPriority(priority), - ), - context: context); + context: context, + builder: (_) => Picker( + items: items, + displayItem: sendViewModel.displayFeeRate, + selectedAtIndex: selectedItem, + title: S.of(context).please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (TransactionPriority priority) => + sendViewModel.setTransactionPriority(priority), + ), + ); + } + + void _presentPicker(BuildContext context) { + showPopUp( + context: context, + builder: (_) => CurrencyPicker( + selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), + items: sendViewModel.currencies, + hintText: S.of(context).search_currency, + onItemSelected: (Currency cur) => + sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency), + ), + ); } @override bool get wantKeepAlive => true; -} \ No newline at end of file +} diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 50309c5fb..894f01e15 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/entities/balance_display_mode.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'; @@ -43,6 +42,7 @@ abstract class SendViewModelBase with Store { : state = InitialExecutionState(), currencies = _wallet.balance.keys.toList(), selectedCryptoCurrency = _wallet.currency, + hasMultipleTokens = _wallet.type == WalletType.ethereum, outputs = ObservableList(), fiatFromSettings = _settingsStore.fiatCurrency { final priority = _settingsStore.priority[_wallet.type]; @@ -51,7 +51,7 @@ abstract class SendViewModelBase with Store { if (!priorityForWalletType(_wallet.type).contains(priority)) { _settingsStore.priority[_wallet.type] = priorities.first; } - + outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); } @@ -127,14 +127,14 @@ abstract class SendViewModelBase with Store { CryptoCurrency get currency => _wallet.currency; - Validator get amountValidator => + Validator get amountValidator => AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type)); - Validator get allAmountValidator => AllAmountValidator(); + Validator get allAmountValidator => AllAmountValidator(); - Validator get addressValidator => AddressValidator(type: selectedCryptoCurrency); + Validator get addressValidator => AddressValidator(type: selectedCryptoCurrency); - Validator get textValidator => TextValidator(); + Validator get textValidator => TextValidator(); final FiatCurrency fiatFromSettings; @@ -142,7 +142,7 @@ abstract class SendViewModelBase with Store { PendingTransaction? pendingTransaction; @computed - String get balance => balanceViewModel.availableBalance; + String get balance => _wallet.balance[selectedCryptoCurrency]!.formattedAvailableBalance; @computed bool get isFiatDisabled => balanceViewModel.isFiatDisabled; @@ -196,6 +196,7 @@ abstract class SendViewModelBase with Store { final BalanceViewModel balanceViewModel; final FiatConversionStore _fiatConversationStore; final Box transactionDescriptionBox; + final bool hasMultipleTokens; @action Future createTransaction() async { @@ -285,7 +286,7 @@ abstract class SendViewModelBase with Store { if (priority == null) { throw Exception('Priority is null for wallet type: ${_wallet.type}'); } - + return haven!.createHavenTransactionCreationCredentials( outputs: outputs, priority: priority, assetType: selectedCryptoCurrency.title); case WalletType.ethereum: @@ -295,7 +296,8 @@ abstract class SendViewModelBase with Store { throw Exception('Priority is null for wallet type: ${_wallet.type}'); } - return ethereum!.createEthereumTransactionCredentials(outputs, priority: priority); + return ethereum!.createEthereumTransactionCredentials( + outputs, priority: priority, currency: selectedCryptoCurrency); default: throw Exception('Unexpected wallet type: ${_wallet.type}'); } @@ -313,7 +315,7 @@ abstract class SendViewModelBase with Store { return priority.toString(); } - bool _isEqualCurrency(String currency) => + bool _isEqualCurrency(String currency) => currency.toLowerCase() == _wallet.currency.title.toLowerCase(); @action diff --git a/tool/configure.dart b/tool/configure.dart index 4a57d9159..44c379c8e 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -508,10 +508,22 @@ abstract class Ethereum { TransactionPriority deserializeEthereumTransactionPriority(int raw); int getEstimatedFee(Object wallet, TransactionPriority priority); - Object createEthereumTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}); - Object createEthereumTransactionCredentialsRaw(List outputs, {TransactionPriority? priority, required int feeRate}); + Object createEthereumTransactionCredentials( + List outputs, { + required TransactionPriority priority, + required CryptoCurrency currency, + int? feeRate, + }); + + Object createEthereumTransactionCredentialsRaw( + List outputs, { + TransactionPriority? priority, + required CryptoCurrency currency, + required int feeRate, + }); int formatterEthereumParseAmount(String amount); + List getERC20Currencies(Object wallet); } """;