Add send ERC-20 tokens initial flow

This commit is contained in:
OmarHatem 2023-06-02 04:02:43 +03:00
parent f08ab62359
commit 94216e6987
11 changed files with 515 additions and 421 deletions

View file

@ -67,6 +67,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.uni, CryptoCurrency.uni,
CryptoCurrency.stx, CryptoCurrency.stx,
CryptoCurrency.btcln, CryptoCurrency.btcln,
CryptoCurrency.shib,
]; ];
static const havenCurrencies = [ static const havenCurrencies = [
@ -152,7 +153,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
static const uni = CryptoCurrency(title: 'UNI', tag: 'ETH', fullName: 'Uniswap', raw: 60, name: 'uni', iconPath: 'assets/images/uni_icon.png'); 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 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 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<int, CryptoCurrency> _rawCurrencyMap = static final Map<int, CryptoCurrency> _rawCurrencyMap =

View file

@ -14,6 +14,8 @@ class EthereumClient {
CryptoCurrency.shib: "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE", CryptoCurrency.shib: "0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE",
}; };
Map<CryptoCurrency, String> get erc20Currencies => _erc20Currencies;
Web3Client? _client; Web3Client? _client;
bool connect(Node node) { bool connect(Node node) {
@ -47,13 +49,14 @@ class EthereumClient {
return result.map((e) => e.toInt()).toList(); return result.map((e) => e.toInt()).toList();
} }
Future<PendingEthereumTransaction> signTransaction( Future<PendingEthereumTransaction> signTransaction({
EthPrivateKey privateKey, required EthPrivateKey privateKey,
String toAddress, required String toAddress,
String amount, required String amount,
int gas, required int gas,
EthereumTransactionPriority priority, required EthereumTransactionPriority priority,
) async { required CryptoCurrency currency,
}) async {
final estimatedGas = await _client!.estimateGas( final estimatedGas = await _client!.estimateGas(
maxPriorityFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip), maxPriorityFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip),
maxFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, 100), maxFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, 100),
@ -64,13 +67,36 @@ class EthereumClient {
final price = await _client!.getGasPrice(); final price = await _client!.getGasPrice();
final transaction = Transaction( 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, from: privateKey.address,
to: EthereumAddress.fromHex(toAddress), to: EthereumAddress.fromHex(toAddress),
maxGas: gas, maxGas: gas,
gasPrice: price, gasPrice: price,
value: EtherAmount.inWei(BigInt.parse(amount)), value: EtherAmount.inWei(BigInt.parse(amount)),
); );
}
final signedTransaction = await _client!.signTransaction(privateKey, transaction); final signedTransaction = await _client!.signTransaction(privateKey, transaction);

View file

@ -1,10 +1,17 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/output_info.dart'; import 'package:cw_core/output_info.dart';
import 'package:cw_ethereum/ethereum_transaction_priority.dart'; import 'package:cw_ethereum/ethereum_transaction_priority.dart';
class EthereumTransactionCredentials { class EthereumTransactionCredentials {
EthereumTransactionCredentials(this.outputs, {required this.priority, this.feeRate}); EthereumTransactionCredentials(
this.outputs, {
required this.priority,
required this.currency,
this.feeRate,
});
final List<OutputInfo> outputs; final List<OutputInfo> outputs;
final EthereumTransactionPriority? priority; final EthereumTransactionPriority? priority;
final int? feeRate; final int? feeRate;
final CryptoCurrency currency;
} }

View file

@ -123,7 +123,7 @@ abstract class EthereumWalletBase
final _credentials = credentials as EthereumTransactionCredentials; final _credentials = credentials as EthereumTransactionCredentials;
final outputs = _credentials.outputs; final outputs = _credentials.outputs;
final hasMultiDestination = outputs.length > 1; final hasMultiDestination = outputs.length > 1;
final balance = await _client.getBalance(_privateKey.address); final _erc20Balance = balance[_credentials.currency]!;
int totalAmount = 0; int totalAmount = 0;
if (hasMultiDestination) { if (hasMultiDestination) {
@ -133,27 +133,28 @@ abstract class EthereumWalletBase
totalAmount = outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); 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(); throw EthereumTransactionCreationException();
} }
} else { } else {
final output = outputs.first; 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; totalAmount = output.sendAll ? allAmount : output.formattedCryptoAmount ?? 0;
if ((output.sendAll && if ((output.sendAll &&
balance.getInWei < EtherAmount.inWei(totalAmount as BigInt).getInWei) || _erc20Balance.balance < EtherAmount.inWei(totalAmount as BigInt).getInWei) ||
(!output.sendAll && balance.getInWei.toInt() <= 0)) { (!output.sendAll && _erc20Balance.balance.toInt() <= 0)) {
throw EthereumTransactionCreationException(); throw EthereumTransactionCreationException();
} }
} }
final pendingEthereumTransaction = await _client.signTransaction( final pendingEthereumTransaction = await _client.signTransaction(
_privateKey, privateKey: _privateKey,
_credentials.outputs.first.address, toAddress: _credentials.outputs.first.address,
totalAmount.toString(), amount: totalAmount.toString(),
_priorityFees[_credentials.priority!.raw], gas: _priorityFees[_credentials.priority!.raw],
_credentials.priority!, priority: _credentials.priority!,
currency: _credentials.currency,
); );
return pendingEthereumTransaction; return pendingEthereumTransaction;
@ -233,8 +234,7 @@ abstract class EthereumWalletBase
final jsonSource = await read(path: path, password: password); final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map; final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String; final mnemonic = data['mnemonic'] as String;
final balance = final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero);
ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero);
return EthereumWallet( return EthereumWallet(
walletInfo: walletInfo, walletInfo: walletInfo,
@ -268,4 +268,6 @@ abstract class EthereumWalletBase
} }
Future<void>? updateBalance() => null; Future<void>? updateBalance() => null;
List<CryptoCurrency> get erc20Currencies => _client.erc20Currencies.keys.toList();
} }

View file

@ -36,6 +36,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.oxt: case CryptoCurrency.oxt:
case CryptoCurrency.paxg: case CryptoCurrency.paxg:
case CryptoCurrency.uni: case CryptoCurrency.uni:
case CryptoCurrency.shib:
return '0x[0-9a-zA-Z]'; return '0x[0-9a-zA-Z]';
case CryptoCurrency.xrp: case CryptoCurrency.xrp:
return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$';
@ -118,6 +119,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.eos: case CryptoCurrency.eos:
return [42]; return [42];
case CryptoCurrency.eth: case CryptoCurrency.eth:
case CryptoCurrency.shib:
return [42]; return [42];
case CryptoCurrency.ltc: case CryptoCurrency.ltc:
return [34, 43, 63]; return [34, 43, 63];

View file

@ -41,8 +41,12 @@ class CWEthereum extends Ethereum {
return ethereumWallet.feeRate(priority); return ethereumWallet.feeRate(priority);
} }
Object createEthereumTransactionCredentials(List<Output> outputs, Object createEthereumTransactionCredentials(
{required TransactionPriority priority, int? feeRate}) => List<Output> outputs, {
required TransactionPriority priority,
required CryptoCurrency currency,
int? feeRate,
}) =>
EthereumTransactionCredentials( EthereumTransactionCredentials(
outputs outputs
.map((out) => OutputInfo( .map((out) => OutputInfo(
@ -56,17 +60,29 @@ class CWEthereum extends Ethereum {
formattedCryptoAmount: out.formattedCryptoAmount)) formattedCryptoAmount: out.formattedCryptoAmount))
.toList(), .toList(),
priority: priority as EthereumTransactionPriority, priority: priority as EthereumTransactionPriority,
currency: currency,
feeRate: feeRate, feeRate: feeRate,
); );
Object createEthereumTransactionCredentialsRaw(List<OutputInfo> outputs, Object createEthereumTransactionCredentialsRaw(
{TransactionPriority? priority, required int feeRate}) => List<OutputInfo> outputs, {
TransactionPriority? priority,
required CryptoCurrency currency,
required int feeRate,
}) =>
EthereumTransactionCredentials( EthereumTransactionCredentials(
outputs, outputs,
priority: priority as EthereumTransactionPriority, priority: priority as EthereumTransactionPriority?,
currency: currency,
feeRate: feeRate, feeRate: feeRate,
); );
@override @override
int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount); int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount);
@override
List<CryptoCurrency> getERC20Currencies(Object wallet) {
final ethereumWallet = wallet as EthereumWallet;
return ethereumWallet.erc20Currencies;
}
} }

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/update_haven_rate.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/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
@ -30,6 +31,17 @@ Future<void> startFiatRateUpdate(
fiat: settingsStore.fiatCurrency, fiat: settingsStore.fiatCurrency,
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); 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) { } catch (e) {
print(e); print(e);
} }

View file

@ -511,17 +511,16 @@ class ExchangeCardState extends State<ExchangeCard> {
void _presentPicker(BuildContext context) { void _presentPicker(BuildContext context) {
showPopUp<void>( showPopUp<void>(
context: context,
builder: (_) => CurrencyPicker( builder: (_) => CurrencyPicker(
selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), selectedAtIndex: widget.currencies.indexOf(_selectedCurrency),
items: widget.currencies, items: widget.currencies,
hintText: S.of(context).search_currency, hintText: S.of(context).search_currency,
isMoneroWallet: _isMoneroWallet, isMoneroWallet: _isMoneroWallet,
isConvertFrom: widget.hasRefundAddress, isConvertFrom: widget.hasRefundAddress,
onItemSelected: (Currency item) => onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency),
widget.onCurrencySelected != null ),
? widget.onCurrencySelected(item as CryptoCurrency) );
: null),
context: context);
} }
void _showAmountPopup(BuildContext context, PaymentRequest paymentRequest) { void _showAmountPopup(BuildContext context, PaymentRequest paymentRequest) {

View file

@ -1,7 +1,10 @@
import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; 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/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.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:cw_core/transaction_priority.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
@ -38,12 +41,8 @@ class SendCard extends StatefulWidget {
); );
} }
class SendCardState extends State<SendCard> class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<SendCard> {
with AutomaticKeepAliveClientMixin<SendCard> { SendCardState({required this.output, required this.sendViewModel, this.initialPaymentRequest})
SendCardState({
required this.output,
required this.sendViewModel,
this.initialPaymentRequest})
: addressController = TextEditingController(), : addressController = TextEditingController(),
cryptoAmountController = TextEditingController(), cryptoAmountController = TextEditingController(),
fiatAmountController = TextEditingController(), fiatAmountController = TextEditingController(),
@ -102,10 +101,7 @@ class SendCardState extends State<SendCard>
KeyboardActions( KeyboardActions(
config: KeyboardActionsConfig( config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS, keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context) keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!,
.accentTextTheme!
.bodyLarge!
.backgroundColor!,
nextFocus: false, nextFocus: false,
actions: [ actions: [
KeyboardActionsItem( KeyboardActionsItem(
@ -116,24 +112,26 @@ class SendCardState extends State<SendCard>
focusNode: fiatAmountFocus, focusNode: fiatAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()], toolbarButtons: [(_) => KeyboardDoneButton()],
) )
]), ],
),
child: Container( child: Container(
height: 0, height: 0,
color: Colors.transparent, color: Colors.transparent,
)), ),
),
Container( Container(
decoration: ResponsiveLayoutUtil.instance.isMobile(context) ? BoxDecoration( decoration: ResponsiveLayoutUtil.instance.isMobile(context)
? BoxDecoration(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24)), bottomRight: Radius.circular(24),
),
gradient: LinearGradient(colors: [ gradient: LinearGradient(colors: [
Theme.of(context).primaryTextTheme!.titleMedium!.color!, Theme.of(context).primaryTextTheme.titleMedium!.color!,
Theme.of(context) Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!,
.primaryTextTheme!
.titleMedium!
.decorationColor!,
], begin: Alignment.topLeft, end: Alignment.bottomRight), ], begin: Alignment.topLeft, end: Alignment.bottomRight),
) : null, )
: null,
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
24, 24,
@ -142,7 +140,8 @@ class SendCardState extends State<SendCard>
ResponsiveLayoutUtil.instance.isMobile(context) ? 32 : 0, ResponsiveLayoutUtil.instance.isMobile(context) ? 32 : 0,
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Observer(builder: (_) => Column( child: Observer(
builder: (_) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Observer(builder: (_) { Observer(builder: (_) {
@ -164,24 +163,15 @@ class SendCardState extends State<SendCard>
AddressTextFieldOption.qrCode, AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook AddressTextFieldOption.addressBook
], ],
buttonColor: Theme.of(context) buttonColor: Theme.of(context).primaryTextTheme.headlineMedium!.color!,
.primaryTextTheme! borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
.headlineMedium!
.color!,
borderColor: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.color!,
textStyle: TextStyle( textStyle: TextStyle(
fontSize: 14, fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
fontWeight: FontWeight.w500,
color: Colors.white),
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme! .primaryTextTheme.headlineSmall!
.headlineSmall!
.decorationColor!), .decorationColor!),
onPushPasteButton: (context) async { onPushPasteButton: (context) async {
output.resetParsedAddress(); output.resetParsedAddress();
@ -195,22 +185,17 @@ class SendCardState extends State<SendCard>
selectedCurrency: sendViewModel.currency, selectedCurrency: sendViewModel.currency,
); );
}), }),
if (output.isParsedAddress) Padding( if (output.isParsedAddress)
Padding(
padding: const EdgeInsets.only(top: 20), padding: const EdgeInsets.only(top: 20),
child: BaseTextFormField( child: BaseTextFormField(
controller: extractedAddressController, controller: extractedAddressController,
readOnly: true, readOnly: true,
borderColor: Theme.of(context) borderColor:
.primaryTextTheme! Theme.of(context).primaryTextTheme.headlineSmall!.color!,
.headlineSmall!
.color!,
textStyle: TextStyle( textStyle: TextStyle(
fontSize: 14, fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
fontWeight: FontWeight.w500, validator: sendViewModel.addressValidator)),
color: Colors.white),
validator: sendViewModel.addressValidator
)
),
Observer( Observer(
builder: (_) => Padding( builder: (_) => Padding(
padding: const EdgeInsets.only(top: 20), padding: const EdgeInsets.only(top: 20),
@ -220,46 +205,80 @@ class SendCardState extends State<SendCard>
padding: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.only(bottom: 8.0),
child: Row( child: Row(
children: [ 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: <Widget>[
Padding(
padding: EdgeInsets.only(right: 5),
child: Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
color: Colors.white,
height: 8,
),
),
Text( Text(
sendViewModel.selectedCryptoCurrency.title, sendViewModel.selectedCryptoCurrency.title,
style: TextStyle( style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.white, fontSize: 16,
)), color: Colors.white),
sendViewModel.selectedCryptoCurrency.tag != null ? Padding( ),
padding: const EdgeInsets.fromLTRB(3.0,0,3.0,0), ],
),
),
)
: Text(
sendViewModel.selectedCryptoCurrency.title,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white),
),
sendViewModel.selectedCryptoCurrency.tag != null
? Padding(
padding: const EdgeInsets.fromLTRB(3.0, 0, 3.0, 0),
child: Container( child: Container(
height: 32, height: 32,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme! .primaryTextTheme.headlineMedium!
.headlineMedium!
.color!, .color!,
borderRadius: borderRadius: BorderRadius.all(
BorderRadius.all(Radius.circular(6))), Radius.circular(6),
)),
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(6.0), padding: const EdgeInsets.all(6.0),
child: Text( sendViewModel.selectedCryptoCurrency.tag!, child: Text(
sendViewModel.selectedCryptoCurrency.tag!,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme! .primaryTextTheme.headlineMedium!
.headlineMedium! .decorationColor!),
.decorationColor!)),
), ),
), ),
), ),
) : Container(), ),
)
: Container(),
Padding( Padding(
padding: const EdgeInsets.only(right: 10.0), padding: const EdgeInsets.only(right: 10.0),
child: Text(':', child: Text(
':',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 16, fontSize: 16,
color: Colors.white)), color: Colors.white),
),
), ),
], ],
), ),
@ -270,8 +289,7 @@ class SendCardState extends State<SendCard>
BaseTextFormField( BaseTextFormField(
focusNode: cryptoAmountFocus, focusNode: cryptoAmountFocus,
controller: cryptoAmountController, controller: cryptoAmountController,
keyboardType: keyboardType: TextInputType.numberWithOptions(
TextInputType.numberWithOptions(
signed: false, decimal: true), signed: false, decimal: true),
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))
@ -287,89 +305,89 @@ class SendCardState extends State<SendCard>
color: Colors.white), color: Colors.white),
placeholderTextStyle: TextStyle( placeholderTextStyle: TextStyle(
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme! .primaryTextTheme.headlineSmall!
.headlineSmall!
.decorationColor!, .decorationColor!,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
fontSize: 14), fontSize: 14),
validator: output.sendAll validator: output.sendAll
? sendViewModel.allAmountValidator ? sendViewModel.allAmountValidator
: sendViewModel : sendViewModel.amountValidator,
.amountValidator), ),
if (!sendViewModel.isBatchSending) Positioned( if (!sendViewModel.isBatchSending)
Positioned(
top: 2, top: 2,
right: 0, right: 0,
child: Container( child: Container(
width: prefixIconWidth, width: prefixIconWidth,
height: prefixIconHeight, height: prefixIconHeight,
child: InkWell( child: InkWell(
onTap: () async => onTap: () async => output.setSendAll(),
output.setSendAll(),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme! .primaryTextTheme.headlineMedium!
.headlineMedium!
.color!, .color!,
borderRadius: borderRadius: BorderRadius.all(
BorderRadius.all( Radius.circular(6),
Radius.circular(6))), ),
),
child: Center( child: Center(
child: Text( child: Text(
S.of(context).all, S.of(context).all,
textAlign: textAlign: TextAlign.center,
TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: fontWeight: FontWeight.bold,
FontWeight.bold, color: Theme.of(context)
color: .primaryTextTheme.headlineMedium!
Theme.of(context) .decorationColor!,
.primaryTextTheme! ),
.headlineMedium! ),
.decorationColor!))), ),
))))]), ),
),
),
),
],
),
), ),
], ],
)
)), )),
Divider(height: 1,color: Theme.of(context) ),
.primaryTextTheme! Divider(
.headlineSmall! height: 1,
.decorationColor!), color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!),
Observer( Observer(
builder: (_) => Padding( builder: (_) => Padding(
padding: EdgeInsets.only(top: 10), padding: EdgeInsets.only(top: 10),
child: Row( child: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Text( child: Text(
S.of(context).available_balance + S.of(context).available_balance + ':',
':',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme! .primaryTextTheme.headlineSmall!
.headlineSmall!
.decorationColor!), .decorationColor!),
)), ),
),
Text( Text(
sendViewModel.balance, sendViewModel.balance,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme! .primaryTextTheme.headlineSmall!
.headlineSmall!
.decorationColor!), .decorationColor!),
) )
], ],
), ),
)), ),
),
if (!sendViewModel.isFiatDisabled) if (!sendViewModel.isFiatDisabled)
Padding( Padding(
padding: const EdgeInsets.only(top: 20), padding: const EdgeInsets.only(top: 20),
@ -377,81 +395,70 @@ class SendCardState extends State<SendCard>
focusNode: fiatAmountFocus, focusNode: fiatAmountFocus,
controller: fiatAmountController, controller: fiatAmountController,
keyboardType: keyboardType:
TextInputType.numberWithOptions( TextInputType.numberWithOptions(signed: false, decimal: true),
signed: false, decimal: true),
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'),
)
], ],
prefixIcon: Padding( prefixIcon: Padding(
padding: EdgeInsets.only(top: 9), padding: EdgeInsets.only(top: 9),
child: child: Text(
Text(sendViewModel.fiat.title + ':', sendViewModel.fiat.title + ':',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.white, color: Colors.white,
)), ),
),
), ),
hintText: '0.00', hintText: '0.00',
borderColor: Theme.of(context) borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
.primaryTextTheme!
.headlineSmall!
.color!,
textStyle: TextStyle( textStyle: TextStyle(
fontSize: 14, fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle( placeholderTextStyle: TextStyle(
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme!.headlineSmall!.decorationColor!, .primaryTextTheme.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
fontSize: 14), fontSize: 14),
)), ),
),
Padding( Padding(
padding: EdgeInsets.only(top: 20), padding: EdgeInsets.only(top: 20),
child: BaseTextFormField( child: BaseTextFormField(
controller: noteController, controller: noteController,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
maxLines: null, maxLines: null,
borderColor: Theme.of(context) borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
.primaryTextTheme!
.headlineSmall!
.color!,
textStyle: TextStyle( textStyle: TextStyle(
fontSize: 14, fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
fontWeight: FontWeight.w500,
color: Colors.white),
hintText: S.of(context).note_optional, hintText: S.of(context).note_optional,
placeholderTextStyle: TextStyle( placeholderTextStyle: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme! .primaryTextTheme.headlineSmall!
.headlineSmall!
.decorationColor!), .decorationColor!),
), ),
), ),
Observer( Observer(
builder: (_) => GestureDetector( builder: (_) => GestureDetector(
onTap: () => onTap: () => _setTransactionPriority(context),
_setTransactionPriority(context),
child: Container( child: Container(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
S S.of(context).send_estimated_fee,
.of(context)
.send_estimated_fee,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: fontWeight: FontWeight.w500,
FontWeight.w500,
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
color: Colors.white)), color: Colors.white),
),
Container( Container(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -461,45 +468,37 @@ class SendCardState extends State<SendCard>
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
output output.estimatedFee.toString() +
.estimatedFee
.toString() +
' ' + ' ' +
sendViewModel sendViewModel.selectedCryptoCurrency.toString(),
.selectedCryptoCurrency.toString(),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: fontWeight: FontWeight.w600,
FontWeight.w600,
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
color: color: Colors.white,
Colors.white)), ),
),
Padding( Padding(
padding: padding: EdgeInsets.only(top: 5),
EdgeInsets.only(top: 5),
child: sendViewModel.isFiatDisabled child: sendViewModel.isFiatDisabled
? const SizedBox(height: 14) ? const SizedBox(height: 14)
: Text(output : Text(
.estimatedFeeFiatAmount output.estimatedFeeFiatAmount +
+ ' ' + ' ' +
sendViewModel sendViewModel.fiat.title,
.fiat.title,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: fontWeight: FontWeight.w600,
FontWeight.w600, color: Theme.of(context)
color: Theme .primaryTextTheme.headlineSmall!
.of(context) .decorationColor!,
.primaryTextTheme! ),
.headlineSmall! ),
.decorationColor!))
), ),
], ],
), ),
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(top: 2, left: 5),
top: 2,
left: 5),
child: Icon( child: Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 12, size: 12,
@ -512,36 +511,38 @@ class SendCardState extends State<SendCard>
], ],
), ),
), ),
)), ),
if (sendViewModel.isElectrumWallet) Padding( ),
if (sendViewModel.isElectrumWallet)
Padding(
padding: EdgeInsets.only(top: 6), padding: EdgeInsets.only(top: 6),
child: GestureDetector( child: GestureDetector(
onTap: () => Navigator.of(context) onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList),
.pushNamed(Routes.unspentCoinsList),
child: Container( child: Container(
color: Colors.transparent, color: Colors.transparent,
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
S.of(context).coin_control, S.of(context).coin_control,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.white)), color: Colors.white),
),
Icon( Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 12, size: 12,
color: Colors.white, color: Colors.white,
) ),
], ],
) ),
) ),
) ),
) ),
], ],
)) ),
),
), ),
), ),
) )
@ -586,7 +587,7 @@ class SendCardState extends State<SendCard>
}); });
noteController.addListener(() { noteController.addListener(() {
final note = noteController.text ?? ''; final note = noteController.text;
if (note != output.note) { if (note != output.note) {
output.note = note; output.note = note;
@ -662,6 +663,7 @@ class SendCardState extends State<SendCard>
final selectedItem = items.indexOf(sendViewModel.transactionPriority); final selectedItem = items.indexOf(sendViewModel.transactionPriority);
await showPopUp<void>( await showPopUp<void>(
context: context,
builder: (_) => Picker( builder: (_) => Picker(
items: items, items: items,
displayItem: sendViewModel.displayFeeRate, displayItem: sendViewModel.displayFeeRate,
@ -671,7 +673,20 @@ class SendCardState extends State<SendCard>
onItemSelected: (TransactionPriority priority) => onItemSelected: (TransactionPriority priority) =>
sendViewModel.setTransactionPriority(priority), sendViewModel.setTransactionPriority(priority),
), ),
context: context); );
}
void _presentPicker(BuildContext context) {
showPopUp<void>(
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 @override

View file

@ -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/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ethereum/ethereum.dart';
@ -43,6 +42,7 @@ abstract class SendViewModelBase with Store {
: state = InitialExecutionState(), : state = InitialExecutionState(),
currencies = _wallet.balance.keys.toList(), currencies = _wallet.balance.keys.toList(),
selectedCryptoCurrency = _wallet.currency, selectedCryptoCurrency = _wallet.currency,
hasMultipleTokens = _wallet.type == WalletType.ethereum,
outputs = ObservableList<Output>(), outputs = ObservableList<Output>(),
fiatFromSettings = _settingsStore.fiatCurrency { fiatFromSettings = _settingsStore.fiatCurrency {
final priority = _settingsStore.priority[_wallet.type]; final priority = _settingsStore.priority[_wallet.type];
@ -127,14 +127,14 @@ abstract class SendViewModelBase with Store {
CryptoCurrency get currency => _wallet.currency; CryptoCurrency get currency => _wallet.currency;
Validator get amountValidator => Validator<String> get amountValidator =>
AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type)); AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
Validator get allAmountValidator => AllAmountValidator(); Validator<String> get allAmountValidator => AllAmountValidator();
Validator get addressValidator => AddressValidator(type: selectedCryptoCurrency); Validator<String> get addressValidator => AddressValidator(type: selectedCryptoCurrency);
Validator get textValidator => TextValidator(); Validator<String> get textValidator => TextValidator();
final FiatCurrency fiatFromSettings; final FiatCurrency fiatFromSettings;
@ -142,7 +142,7 @@ abstract class SendViewModelBase with Store {
PendingTransaction? pendingTransaction; PendingTransaction? pendingTransaction;
@computed @computed
String get balance => balanceViewModel.availableBalance; String get balance => _wallet.balance[selectedCryptoCurrency]!.formattedAvailableBalance;
@computed @computed
bool get isFiatDisabled => balanceViewModel.isFiatDisabled; bool get isFiatDisabled => balanceViewModel.isFiatDisabled;
@ -196,6 +196,7 @@ abstract class SendViewModelBase with Store {
final BalanceViewModel balanceViewModel; final BalanceViewModel balanceViewModel;
final FiatConversionStore _fiatConversationStore; final FiatConversionStore _fiatConversationStore;
final Box<TransactionDescription> transactionDescriptionBox; final Box<TransactionDescription> transactionDescriptionBox;
final bool hasMultipleTokens;
@action @action
Future<void> createTransaction() async { Future<void> createTransaction() async {
@ -295,7 +296,8 @@ abstract class SendViewModelBase with Store {
throw Exception('Priority is null for wallet type: ${_wallet.type}'); 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: default:
throw Exception('Unexpected wallet type: ${_wallet.type}'); throw Exception('Unexpected wallet type: ${_wallet.type}');
} }

View file

@ -508,10 +508,22 @@ abstract class Ethereum {
TransactionPriority deserializeEthereumTransactionPriority(int raw); TransactionPriority deserializeEthereumTransactionPriority(int raw);
int getEstimatedFee(Object wallet, TransactionPriority priority); int getEstimatedFee(Object wallet, TransactionPriority priority);
Object createEthereumTransactionCredentials(List<Output> outputs, {required TransactionPriority priority, int? feeRate}); Object createEthereumTransactionCredentials(
Object createEthereumTransactionCredentialsRaw(List<OutputInfo> outputs, {TransactionPriority? priority, required int feeRate}); List<Output> outputs, {
required TransactionPriority priority,
required CryptoCurrency currency,
int? feeRate,
});
Object createEthereumTransactionCredentialsRaw(
List<OutputInfo> outputs, {
TransactionPriority? priority,
required CryptoCurrency currency,
required int feeRate,
});
int formatterEthereumParseAmount(String amount); int formatterEthereumParseAmount(String amount);
List<CryptoCurrency> getERC20Currencies(Object wallet);
} }
"""; """;