litecoin fixes, sign form fixes, use new walletAddressPicker

This commit is contained in:
Matthew Fosse 2024-04-18 13:04:05 -07:00
parent b08bac577d
commit dd486a29e7
11 changed files with 354 additions and 213 deletions

View file

@ -9,6 +9,7 @@ import 'package:blockchain_utils/exception/exception.dart';
import 'package:blockchain_utils/hex/hex.dart';
import 'package:blockchain_utils/numbers/bigint_utils.dart';
import 'package:blockchain_utils/signer/bitcoin_signer.dart';
import 'package:blockchain_utils/signer/ecdsa_signing_key.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_core/crypto_currency.dart';
@ -137,7 +138,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
final HD = index == null ? hd : hd.derive(index);
final priv = ECPrivate.fromHex(HD.privKey!);
String messagePrefix = '\x19Litecoin Signed Message:\n';
return priv.signMessage(utf8.encode(message), messagePrefix: messagePrefix);
final privateKey = ECDSAPrivateKey.fromBytes(
priv.toBytes(),
Curves.generatorSecp256k1,
);
final signature =
signLitecoinMessage(utf8.encode(message), messagePrefix, privateKey: privateKey);
return BytesUtils.toHexString(signature);
}
List<int> _magicPrefix(List<int> message, List<int> messagePrefix) {
@ -146,6 +154,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return [...messagePrefix, ...encodeLength, ...message];
}
List<int> signLitecoinMessage(List<int> message, String messagePrefix,
{required ECDSAPrivateKey privateKey}) {
final messgaeHash = QuickCrypto.sha256Hash(magicMessage(message, messagePrefix));
final signingKey = EcdsaSigningKey(privateKey);
ECDSASignature ecdsaSign =
signingKey.signDigestDeterminstic(digest: messgaeHash, hashFunc: () => SHA256());
final n = Curves.generatorSecp256k1.order! >> 1;
BigInt newS;
if (ecdsaSign.s.compareTo(n) > 0) {
newS = Curves.generatorSecp256k1.order! - ecdsaSign.s;
} else {
newS = ecdsaSign.s;
}
final newSignature = ECDSASignature(ecdsaSign.r, newS);
return newSignature.toBytes(BitcoinSignerUtils.baselen);
}
List<int> magicMessage(List<int> message, String messagePrefix) {
final prefixBytes = StringUtils.encode(messagePrefix);
final magic = _magicPrefix(message, prefixBytes);

View file

@ -14,6 +14,7 @@ import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cw_core/receive_page_option.dart';
@ -757,6 +758,8 @@ Future<void> setup({
getIt.registerFactoryParam<ContactPage, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactPage(getIt.get<ContactViewModel>(param1: contact)));
getIt.registerFactory(() => AddressListPage(getIt.get<WalletAddressListViewModel>()));
getIt.registerFactory(() {
final appStore = getIt.get<AppStore>();
return NodeListViewModel(_nodeSource, appStore);

View file

@ -19,6 +19,7 @@ import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart';
import 'package:cake_wallet/src/screens/nano_accounts/nano_account_edit_or_create_page.dart';
import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
@ -377,6 +378,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (_) => getIt.get<ContactListPage>(param1: selectedCurrency));
case Routes.pickerWalletAddress:
return MaterialPageRoute<void>(builder: (_) => getIt.get<AddressListPage>());
case Routes.addressBookAddContact:
return CupertinoPageRoute<void>(
builder: (_) => getIt.get<ContactPage>(param1: settings.arguments as ContactRecord?));

View file

@ -29,6 +29,7 @@ class Routes {
static const nanoAccountCreation = '/nano_account_new';
static const addressBook = '/address_book';
static const pickerAddressBook = '/picker_address_book';
static const pickerWalletAddress = '/picker_wallet_address';
static const addressBookAddContact = '/address_book_add_contact';
static const showKeys = '/show_keys';
static const exchangeConfirm = '/exchange_confirm';

View file

@ -51,59 +51,11 @@ class SignPage extends BasePage {
final List<Widget> _pages;
final GlobalKey<SignFormState> signFormKey;
final GlobalKey<VerifyFormState> verifyFormKey;
bool _isEffectsInstalled = false;
@override
Widget body(BuildContext context) {
reaction((_) => signViewModel.state, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (_) {
return AlertWithOneAction(
alertTitle: S.current.error,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
});
});
}
if (state is ExecutedSuccessfullyState) {
if (signViewModel.isSigning) {
signFormKey.currentState!.signatureController.text = state.payload as String;
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (_) {
return AlertWithOneAction(
alertTitle: S.current.successful,
alertContent: S.current.message_verified,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
});
});
}
}
});
// reaction((_) => walletRestoreViewModel.mode, (WalletRestoreMode mode) {
// walletRestoreViewModel.isButtonEnabled = false;
// walletRestoreFromSeedFormKey
// .currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = '';
// walletRestoreFromSeedFormKey
// .currentState!.blockchainHeightKey.currentState!.dateController.text = '';
// walletRestoreFromSeedFormKey.currentState!.nameTextEditingController.text = '';
// walletRestoreFromKeysFormKey
// .currentState!.blockchainHeightKey.currentState!.restoreHeightController.text = '';
// walletRestoreFromKeysFormKey
// .currentState!.blockchainHeightKey.currentState!.dateController.text = '';
// walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text = '';
// });
_setEffects(context);
return KeyboardActions(
config: KeyboardActionsConfig(
@ -173,7 +125,7 @@ class SignPage extends BasePage {
.extension<WalletListTheme>()!
.restoreWalletButtonTextColor,
isLoading: signViewModel.state is IsExecutingState,
// isDisabled: !signViewModel.isButtonEnabled,
isDisabled: signViewModel.state is IsExecutingState,
);
},
),
@ -188,6 +140,48 @@ class SignPage extends BasePage {
);
}
void _setEffects(BuildContext context) async {
if (_isEffectsInstalled) {
return;
}
_isEffectsInstalled = true;
reaction((_) => signViewModel.state, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (_) {
return AlertWithOneAction(
alertTitle: S.current.error,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
});
});
}
if (state is ExecutedSuccessfullyState) {
if (signViewModel.isSigning) {
signFormKey.currentState!.signatureController.text = state.payload as String;
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (_) {
return AlertWithOneAction(
alertTitle: S.current.successful,
alertContent: S.current.message_verified,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
});
});
}
}
});
}
Future<void> _confirmForm(BuildContext context) async {
FocusManager.instance.primaryFocus?.unfocus();

View file

@ -75,7 +75,7 @@ class SignFormState extends State<SignForm> {
const SizedBox(height: 20),
AddressTextField(
controller: addressController,
options: [AddressTextFieldOption.paste, AddressTextFieldOption.addressBook],
options: [AddressTextFieldOption.paste, AddressTextFieldOption.walletAddresses],
buttonColor: Theme.of(context).hintColor,
onSelectedContact: (contact) {
addressController.text = contact.address;

View file

@ -70,7 +70,7 @@ class VerifyFormState extends State<VerifyForm> {
const SizedBox(height: 20),
AddressTextField(
controller: addressController,
options: [AddressTextFieldOption.paste, AddressTextFieldOption.addressBook],
options: [AddressTextFieldOption.paste, AddressTextFieldOption.walletAddresses],
buttonColor: Theme.of(context).hintColor,
onSelectedContact: (contact) {
addressController.text = contact.address;

View file

@ -0,0 +1,31 @@
import 'package:cake_wallet/src/screens/receive/widgets/address_list.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
class AddressListPage extends BasePage {
AddressListPage(this.addressListViewModel);
final WalletAddressListViewModel addressListViewModel;
@override
String get title => S.current.accounts_subaddresses;
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
AddressList(
addressListViewModel: addressListViewModel,
onSelect: (String address) async {
Navigator.of(context).pop(address);
},
),
],
),
);
}
}

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/src/screens/nano_accounts/nano_account_list_page.dart';
import 'package:cake_wallet/src/screens/receive/widgets/address_list.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
@ -123,89 +124,10 @@ class ReceivePage extends BasePage {
amountController: _amountController,
isLight: currentTheme.type == ThemeType.light),
),
Observer(
builder: (_) => ListView.separated(
padding: EdgeInsets.all(0),
separatorBuilder: (context, _) => const HorizontalSectionDivider(),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: addressListViewModel.items.length,
itemBuilder: (context, index) {
final item = addressListViewModel.items[index];
Widget cell = Container();
AddressList(
addressListViewModel: addressListViewModel,
),
if (item is WalletAccountListHeader) {
cell = HeaderTile(
showTrailingButton: true,
walletAddressListViewModel: addressListViewModel,
trailingButtonTap: () async {
if (addressListViewModel.type == WalletType.monero ||
addressListViewModel.type == WalletType.haven) {
await showPopUp<void>(
context: context,
builder: (_) => getIt.get<MoneroAccountListPage>());
} else {
await showPopUp<void>(
context: context,
builder: (_) => getIt.get<NanoAccountListPage>());
}
},
title: S.of(context).accounts,
trailingIcon: Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
));
}
if (item is WalletAddressListHeader) {
cell = HeaderTile(
title: S.of(context).addresses,
walletAddressListViewModel: addressListViewModel,
showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled,
showSearchButton: true,
trailingButtonTap: () =>
Navigator.of(context).pushNamed(Routes.newSubaddress),
trailingIcon: Icon(
Icons.add,
size: 20,
color: Theme.of(context)
.extension<ReceivePageTheme>()!
.iconsColor,
));
}
if (item is WalletAddressListItem) {
cell = Observer(builder: (_) {
final isCurrent =
item.address == addressListViewModel.address.address;
final backgroundColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor;
final textColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileTextColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor;
return AddressCell.fromItem(item,
isCurrent: isCurrent,
hasBalance: addressListViewModel.isElectrumWallet,
backgroundColor: backgroundColor,
textColor: textColor,
onTap: (_) => addressListViewModel.setAddress(item),
onEdit: () => Navigator.of(context)
.pushNamed(Routes.newSubaddress, arguments: item));
});
}
return index != 0
? cell
: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30)),
child: cell,
);
})),
],
),
));

View file

@ -0,0 +1,121 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart';
import 'package:cake_wallet/src/screens/nano_accounts/nano_account_list_page.dart';
import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart';
import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart';
import 'package:cake_wallet/src/widgets/section_divider.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class AddressList extends StatelessWidget {
const AddressList({
super.key,
// required this.onTapPicker,
required this.addressListViewModel,
this.onSelect,
});
final WalletAddressListViewModel addressListViewModel;
final Function(String)? onSelect;
@override
Widget build(BuildContext context) {
bool editable = onSelect == null;
return Observer(
builder: (_) => ListView.separated(
padding: EdgeInsets.all(0),
separatorBuilder: (context, _) => const HorizontalSectionDivider(),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: addressListViewModel.items.length,
itemBuilder: (context, index) {
final item = addressListViewModel.items[index];
Widget cell = Container();
if (item is WalletAccountListHeader) {
cell = HeaderTile(
showTrailingButton: editable,
walletAddressListViewModel: addressListViewModel,
trailingButtonTap: () async {
if (addressListViewModel.type == WalletType.monero ||
addressListViewModel.type == WalletType.haven) {
await showPopUp<void>(
context: context, builder: (_) => getIt.get<MoneroAccountListPage>());
} else {
await showPopUp<void>(
context: context, builder: (_) => getIt.get<NanoAccountListPage>());
}
},
title: S.of(context).accounts,
trailingIcon: Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
));
}
if (item is WalletAddressListHeader) {
cell = HeaderTile(
title: S.of(context).addresses,
walletAddressListViewModel: addressListViewModel,
showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled,
showSearchButton: true,
trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress),
trailingIcon: Icon(
Icons.add,
size: 20,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
));
}
if (item is WalletAddressListItem) {
cell = Observer(builder: (_) {
final isCurrent = item.address == addressListViewModel.address.address && editable;
final backgroundColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor;
final textColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileTextColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor;
return AddressCell.fromItem(
item,
isCurrent: isCurrent,
hasBalance: addressListViewModel.isElectrumWallet,
backgroundColor: backgroundColor,
textColor: textColor,
onTap: (_) {
if (onSelect != null) {
onSelect!(item.address);
return;
}
addressListViewModel.setAddress(item);
},
onEdit: editable
? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item)
: null,
);
});
}
return index != 0
? cell
: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30), topRight: Radius.circular(30)),
child: cell,
);
},
),
);
}
}

View file

@ -12,7 +12,7 @@ import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:permission_handler/permission_handler.dart';
enum AddressTextFieldOption { paste, qrCode, addressBook }
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
class AddressTextField extends StatelessWidget {
AddressTextField(
@ -31,6 +31,7 @@ class AddressTextField extends StatelessWidget {
this.validator,
this.onPushPasteButton,
this.onPushAddressBookButton,
this.onPushAddressPickerButton,
this.onSelectedContact,
this.selectedCurrency});
@ -53,6 +54,7 @@ class AddressTextField extends StatelessWidget {
final FocusNode? focusNode;
final Function(BuildContext context)? onPushPasteButton;
final Function(BuildContext context)? onPushAddressBookButton;
final Function(BuildContext context)? onPushAddressPickerButton;
final Function(ContactBase contact)? onSelectedContact;
final CryptoCurrency? selectedCurrency;
@ -67,16 +69,13 @@ class AddressTextField extends StatelessWidget {
enabled: isActive,
controller: controller,
focusNode: focusNode,
style: textStyle ??
TextStyle(
fontSize: 16, color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
decoration: InputDecoration(
suffixIcon: SizedBox(
width: prefixIconWidth * options.length + (spaceBetweenPrefixIcons * options.length),
),
hintStyle: hintStyle ?? TextStyle(fontSize: 16, color: Theme.of(context).hintColor),
hintText: placeholder ?? S.current.widgets_address,
focusedBorder: isBorderExist
@ -101,90 +100,122 @@ class AddressTextField extends StatelessWidget {
top: 2,
right: 0,
child: SizedBox(
width: prefixIconWidth * options.length + (spaceBetweenPrefixIcons * options.length),
width:
(prefixIconWidth * options.length) + (spaceBetweenPrefixIcons * options.length),
child: Row(
mainAxisAlignment: responsiveLayoutUtil.shouldRenderMobileUI
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.end,
children: [
SizedBox(width: 5),
if (this.options.contains(AddressTextFieldOption.paste)) ...[
SizedBox(width: 5),
Container(
width: prefixIconWidth,
height: prefixIconHeight,
padding: EdgeInsets.only(top: 0),
child: Semantics(
label: S.of(context).paste,
child: InkWell(
onTap: () async => _pasteAddress(context),
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: buttonColor ??
Theme.of(context).dialogTheme.backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/paste_ios.png',
color: iconColor ??
Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor,
)),
),
)),
width: prefixIconWidth,
height: prefixIconHeight,
padding: EdgeInsets.only(top: 0),
child: Semantics(
label: S.of(context).paste,
child: InkWell(
onTap: () async => _pasteAddress(context),
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color:
buttonColor ?? Theme.of(context).dialogTheme.backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/paste_ios.png',
color: iconColor ??
Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor,
)),
),
),
),
],
if (this.options.contains(AddressTextFieldOption.qrCode) &&
DeviceInfo.instance.isMobile) ...[
Container(
width: prefixIconWidth,
height: prefixIconHeight,
padding: EdgeInsets.only(top: 0),
child: Semantics(
label: S.of(context).scan_qr_code,
child: InkWell(
onTap: () async => _presentQRScanner(context),
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: buttonColor ??
Theme.of(context).dialogTheme.backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/qr_code_icon.png',
color: iconColor ??
Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor,
)),
),
))
] else
SizedBox(width: 5),
if (this.options.contains(AddressTextFieldOption.addressBook)) ...[
Container(
width: prefixIconWidth,
height: prefixIconHeight,
padding: EdgeInsets.only(top: 0),
child: Semantics(
label: S.of(context).address_book,
child: InkWell(
onTap: () async => _presetAddressBookPicker(context),
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: buttonColor ??
Theme.of(context).dialogTheme.backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/open_book.png',
color: iconColor ??
Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor,
)),
),
))
]
width: prefixIconWidth,
height: prefixIconHeight,
padding: EdgeInsets.only(top: 0),
child: Semantics(
label: S.of(context).scan_qr_code,
child: InkWell(
onTap: () async => _presentQRScanner(context),
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color:
buttonColor ?? Theme.of(context).dialogTheme.backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/qr_code_icon.png',
color: iconColor ??
Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor,
)),
),
),
),
],
if (this.options.contains(AddressTextFieldOption.addressBook)) ...[
SizedBox(width: 5),
Container(
width: prefixIconWidth,
height: prefixIconHeight,
padding: EdgeInsets.only(top: 0),
child: Semantics(
label: S.of(context).address_book,
child: InkWell(
onTap: () async => _presetAddressBookPicker(context),
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color:
buttonColor ?? Theme.of(context).dialogTheme.backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/open_book.png',
color: iconColor ??
Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor,
)),
),
),
),
],
if (this.options.contains(AddressTextFieldOption.walletAddresses)) ...[
SizedBox(width: 5),
Container(
width: prefixIconWidth,
height: prefixIconHeight,
padding: EdgeInsets.only(top: 0),
child: Semantics(
label: S.of(context).address_book,
child: InkWell(
onTap: () async => _presetWalletAddressPicker(context),
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color:
buttonColor ?? Theme.of(context).dialogTheme.backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/open_book.png',
color: iconColor ??
Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor,
)),
),
),
)
],
],
),
))
@ -194,7 +225,7 @@ class AddressTextField extends StatelessWidget {
Future<void> _presentQRScanner(BuildContext context) async {
bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return;
final code = await presentQRScanner();
if (code.isEmpty) {
@ -221,6 +252,15 @@ class AddressTextField extends StatelessWidget {
}
}
Future<void> _presetWalletAddressPicker(BuildContext context) async {
final address = await Navigator.of(context).pushNamed(Routes.pickerWalletAddress);
if (address is String) {
controller?.text = address;
onPushAddressPickerButton?.call(context);
}
}
Future<void> _pasteAddress(BuildContext context) async {
final clipboard = await Clipboard.getData('text/plain');
final address = clipboard?.text ?? '';