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;
from: privateKey.address,
to: EthereumAddress.fromHex(toAddress), if (erc20Currencies.containsKey(currency)) {
maxGas: gas, final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
gasPrice: price, final contractAbi = ContractAbi.fromJson(abi, "ERC20");
value: EtherAmount.inWei(BigInt.parse(amount)),
); 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); 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>(
builder: (_) => CurrencyPicker( context: context,
selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), builder: (_) => CurrencyPicker(
items: widget.currencies, selectedAtIndex: widget.currencies.indexOf(_selectedCurrency),
hintText: S.of(context).search_currency, items: widget.currencies,
isMoneroWallet: _isMoneroWallet, hintText: S.of(context).search_currency,
isConvertFrom: widget.hasRefundAddress, isMoneroWallet: _isMoneroWallet,
onItemSelected: (Currency item) => isConvertFrom: widget.hasRefundAddress,
widget.onCurrencySelected != null onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency),
? 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';
@ -32,18 +35,14 @@ class SendCard extends StatefulWidget {
@override @override
SendCardState createState() => SendCardState( SendCardState createState() => SendCardState(
output: output, output: output,
sendViewModel: sendViewModel, sendViewModel: sendViewModel,
initialPaymentRequest: initialPaymentRequest, initialPaymentRequest: initialPaymentRequest,
); );
} }
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(),
@ -100,40 +99,39 @@ class SendCardState extends State<SendCard>
return Stack( return Stack(
children: [ children: [
KeyboardActions( KeyboardActions(
config: KeyboardActionsConfig( config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS, keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context) keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!,
.accentTextTheme! nextFocus: false,
.bodyLarge! actions: [
.backgroundColor!, KeyboardActionsItem(
nextFocus: false, focusNode: cryptoAmountFocus,
actions: [ toolbarButtons: [(_) => KeyboardDoneButton()],
KeyboardActionsItem( ),
focusNode: cryptoAmountFocus, KeyboardActionsItem(
toolbarButtons: [(_) => KeyboardDoneButton()], focusNode: fiatAmountFocus,
), toolbarButtons: [(_) => KeyboardDoneButton()],
KeyboardActionsItem( )
focusNode: fiatAmountFocus, ],
toolbarButtons: [(_) => KeyboardDoneButton()], ),
) child: Container(
]), height: 0,
child: Container( color: Colors.transparent,
height: 0, ),
color: Colors.transparent, ),
)),
Container( Container(
decoration: ResponsiveLayoutUtil.instance.isMobile(context) ? BoxDecoration( decoration: ResponsiveLayoutUtil.instance.isMobile(context)
borderRadius: BorderRadius.only( ? BoxDecoration(
bottomLeft: Radius.circular(24), borderRadius: BorderRadius.only(
bottomRight: Radius.circular(24)), bottomLeft: Radius.circular(24),
gradient: LinearGradient(colors: [ bottomRight: Radius.circular(24),
Theme.of(context).primaryTextTheme!.titleMedium!.color!, ),
Theme.of(context) gradient: LinearGradient(colors: [
.primaryTextTheme! Theme.of(context).primaryTextTheme.titleMedium!.color!,
.titleMedium! Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!,
.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,181 +185,209 @@ class SendCardState extends State<SendCard>
selectedCurrency: sendViewModel.currency, selectedCurrency: sendViewModel.currency,
); );
}), }),
if (output.isParsedAddress) Padding( if (output.isParsedAddress)
padding: const EdgeInsets.only(top: 20), Padding(
child: BaseTextFormField( padding: const EdgeInsets.only(top: 20),
controller: extractedAddressController, child: BaseTextFormField(
readOnly: true, controller: extractedAddressController,
borderColor: Theme.of(context) readOnly: true,
.primaryTextTheme! borderColor:
.headlineSmall! Theme.of(context).primaryTextTheme.headlineSmall!.color!,
.color!, textStyle: TextStyle(
textStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
fontSize: 14, validator: sendViewModel.addressValidator)),
fontWeight: FontWeight.w500,
color: Colors.white),
validator: sendViewModel.addressValidator
)
),
Observer( Observer(
builder: (_) => Padding( builder: (_) => Padding(
padding: const EdgeInsets.only(top: 20), padding: const EdgeInsets.only(top: 20),
child: Row( child: Row(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.only(bottom: 8.0),
child: Row( child: Row(
children: [ children: [
Text( sendViewModel.hasMultipleTokens
sendViewModel.selectedCryptoCurrency.title, ? Container(
style: TextStyle( padding: EdgeInsets.only(right: 8),
fontSize: 16, height: 32,
fontWeight: FontWeight.w600, child: InkWell(
color: Colors.white, onTap: () => _presentPicker(context),
)), child: Row(
sendViewModel.selectedCryptoCurrency.tag != null ? Padding( mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.fromLTRB(3.0,0,3.0,0), mainAxisSize: MainAxisSize.min,
child: Container( children: <Widget>[
height: 32, Padding(
decoration: BoxDecoration( padding: EdgeInsets.only(right: 5),
color: Theme.of(context) child: Image.asset(
.primaryTextTheme! 'assets/images/arrow_bottom_purple_icon.png',
.headlineMedium! color: Colors.white,
.color!, height: 8,
borderRadius: ),
BorderRadius.all(Radius.circular(6))), ),
child: Center( Text(
child: Padding( sendViewModel.selectedCryptoCurrency.title,
padding: const EdgeInsets.all(6.0), style: TextStyle(
child: Text( sendViewModel.selectedCryptoCurrency.tag!, fontWeight: FontWeight.w600,
style: TextStyle( fontSize: 16,
fontSize: 12, color: Colors.white),
fontWeight: FontWeight.bold, ),
color: Theme.of(context) ],
.primaryTextTheme! ),
.headlineMedium!
.decorationColor!)),
), ),
), )
), : Text(
) : Container(), sendViewModel.selectedCryptoCurrency.title,
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Text(':',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 16, 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), color: Colors.white),
placeholderTextStyle: TextStyle( ),
color: Theme.of(context) sendViewModel.selectedCryptoCurrency.tag != null
.primaryTextTheme! ? Padding(
.headlineSmall! padding: const EdgeInsets.fromLTRB(3.0, 0, 3.0, 0),
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: output.sendAll
? sendViewModel.allAmountValidator
: sendViewModel
.amountValidator),
if (!sendViewModel.isBatchSending) Positioned(
top: 2,
right: 0,
child: Container( child: Container(
width: prefixIconWidth, height: 32,
height: prefixIconHeight, decoration: BoxDecoration(
child: InkWell( color: Theme.of(context)
onTap: () async => .primaryTextTheme.headlineMedium!
output.setSendAll(), .color!,
child: Container( borderRadius: BorderRadius.all(
decoration: BoxDecoration( Radius.circular(6),
color: Theme.of(context) )),
.primaryTextTheme! child: Center(
.headlineMedium! child: Padding(
.color!, padding: const EdgeInsets.all(6.0),
borderRadius: child: Text(
BorderRadius.all( sendViewModel.selectedCryptoCurrency.tag!,
Radius.circular(6))), style: TextStyle(
child: Center( fontSize: 12,
child: Text( fontWeight: FontWeight.bold,
S.of(context).all, color: Theme.of(context)
textAlign: .primaryTextTheme.headlineMedium!
TextAlign.center, .decorationColor!),
style: TextStyle( ),
fontSize: 12, ),
fontWeight: ),
FontWeight.bold, ),
color: )
Theme.of(context) : Container(),
.primaryTextTheme! Padding(
.headlineMedium! padding: const EdgeInsets.only(right: 10.0),
.decorationColor!))), child: Text(
))))]),
),
],
)
)),
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: <Widget>[
Expanded(
child: Text(
S.of(context).available_balance +
':', ':',
style: TextStyle( style: TextStyle(
fontSize: 12, fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, fontSize: 16,
color: Theme.of(context) color: Colors.white),
.primaryTextTheme! ),
.headlineSmall! ),
.decorationColor!), ],
)), ),
Text( ),
sendViewModel.balance, 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: <Widget>[
Expanded(
child: Text(
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(
)), sendViewModel.balance,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Theme.of(context)
.primaryTextTheme.headlineSmall!
.decorationColor!),
)
],
),
),
),
if (!sendViewModel.isFiatDisabled) if (!sendViewModel.isFiatDisabled)
Padding( Padding(
padding: const EdgeInsets.only(top: 20), padding: const EdgeInsets.only(top: 20),
@ -377,171 +395,154 @@ 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.spaceBetween,
mainAxisAlignment: crossAxisAlignment: CrossAxisAlignment.start,
MainAxisAlignment.spaceBetween, children: <Widget>[
crossAxisAlignment: CrossAxisAlignment.start, Text(
children: <Widget>[ S.of(context).send_estimated_fee,
Text( style: TextStyle(
S fontSize: 12,
.of(context) fontWeight: FontWeight.w500,
.send_estimated_fee, //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
style: TextStyle( color: Colors.white),
fontSize: 12, ),
fontWeight: Container(
FontWeight.w500, child: Row(
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, crossAxisAlignment: CrossAxisAlignment.start,
color: Colors.white)), children: <Widget>[
Container( Column(
child: Row( mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[ children: [
Column( Text(
mainAxisAlignment: MainAxisAlignment.start, output.estimatedFee.toString() +
crossAxisAlignment: CrossAxisAlignment.end, ' ' +
children: [ sendViewModel.selectedCryptoCurrency.toString(),
Text( style: TextStyle(
output fontSize: 12,
.estimatedFee fontWeight: FontWeight.w600,
.toString() + //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
' ' + color: Colors.white,
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!))
), ),
],
),
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,7 +551,7 @@ class SendCardState extends State<SendCard>
} }
void _setEffects(BuildContext context) { void _setEffects(BuildContext context) {
if (_effectsInstalled) { if (_effectsInstalled) {
return; return;
} }
@ -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,16 +663,30 @@ class SendCardState extends State<SendCard>
final selectedItem = items.indexOf(sendViewModel.transactionPriority); final selectedItem = items.indexOf(sendViewModel.transactionPriority);
await showPopUp<void>( await showPopUp<void>(
builder: (_) => Picker( context: context,
items: items, builder: (_) => Picker(
displayItem: sendViewModel.displayFeeRate, items: items,
selectedAtIndex: selectedItem, displayItem: sendViewModel.displayFeeRate,
title: S.of(context).please_select, selectedAtIndex: selectedItem,
mainAxisAlignment: MainAxisAlignment.center, title: S.of(context).please_select,
onItemSelected: (TransactionPriority priority) => mainAxisAlignment: MainAxisAlignment.center,
sendViewModel.setTransactionPriority(priority), onItemSelected: (TransactionPriority 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);
} }
"""; """;