Ensure plain addresses are parsed from qr codes. Use uri parsing everywhere with a couple small tweaks.

This commit is contained in:
julian 2024-11-18 14:14:25 -06:00 committed by julian-CStack
parent f15d051108
commit 4594801cf3
12 changed files with 425 additions and 630 deletions

View file

@ -66,6 +66,62 @@ class _NewContactAddressEntryFormState
List<CryptoCurrency> coins = [];
void _onQrTapped() async {
try {
// ref
// .read(shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = false;
final qrResult = await widget.barcodeScanner.scan();
// Future<void>.delayed(
// const Duration(seconds: 2),
// () => ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true,
// );
final paymentData = AddressUtils.parsePaymentUri(
qrResult.rawContent,
logging: Logging.instance,
);
if (paymentData != null) {
addressController.text = paymentData.address;
ref.read(addressEntryDataProvider(widget.id)).address =
addressController.text.isEmpty ? null : addressController.text;
addressLabelController.text =
paymentData.label ?? addressLabelController.text;
ref.read(addressEntryDataProvider(widget.id)).addressLabel =
addressLabelController.text.isEmpty
? null
: addressLabelController.text;
// now check for non standard encoded basic address
} else if (ref.read(addressEntryDataProvider(widget.id)).coin != null) {
if (ref.read(addressEntryDataProvider(widget.id)).coin!.validateAddress(
qrResult.rawContent,
)) {
addressController.text = qrResult.rawContent;
ref.read(addressEntryDataProvider(widget.id)).address =
qrResult.rawContent;
}
}
} on PlatformException catch (e, s) {
// ref
// .read(shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true;
Logging.instance.log(
"Failed to get camera permissions to scan address qr code: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
void initState() {
addressLabelController = TextEditingController()
@ -404,71 +460,7 @@ class _NewContactAddressEntryFormState
null)
TextFieldIconButton(
key: const Key("addAddressBookEntryScanQrButtonKey"),
onTap: () async {
try {
// ref
// .read(shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = false;
final qrResult = await widget.barcodeScanner.scan();
// Future<void>.delayed(
// const Duration(seconds: 2),
// () => ref
// .read(
// shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true,
// );
final results =
AddressUtils.parseUri(qrResult.rawContent);
if (results.isNotEmpty) {
addressController.text = results["address"] ?? "";
ref
.read(addressEntryDataProvider(widget.id))
.address =
addressController.text.isEmpty
? null
: addressController.text;
addressLabelController.text = results["label"] ??
addressLabelController.text;
ref
.read(addressEntryDataProvider(widget.id))
.addressLabel =
addressLabelController.text.isEmpty
? null
: addressLabelController.text;
// now check for non standard encoded basic address
} else if (ref
.read(addressEntryDataProvider(widget.id))
.coin !=
null) {
if (ref
.read(addressEntryDataProvider(widget.id))
.coin!
.validateAddress(
qrResult.rawContent,
)) {
addressController.text = qrResult.rawContent;
ref
.read(addressEntryDataProvider(widget.id))
.address = qrResult.rawContent;
}
}
} on PlatformException catch (e, s) {
// ref
// .read(shouldShowLockscreenOnResumeStateProvider
// .state)
// .state = true;
Logging.instance.log(
"Failed to get camera permissions to scan address qr code: $e\n$s",
level: LogLevel.Warning,
);
}
},
onTap: _onQrTapped,
child: const QrCodeIcon(),
),
const SizedBox(

View file

@ -713,6 +713,60 @@ class _BuyFormState extends ConsumerState<BuyForm> {
}
}
void _onQrTapped() async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75),
);
}
final qrResult = await scanner.scan();
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info,
);
final paymentData = AddressUtils.parsePaymentUri(
qrResult.rawContent,
logging: Logging.instance,
);
Logging.instance.log(
"qrResult parsed: $paymentData",
level: LogLevel.Info,
);
if (paymentData != null) {
// auto fill address
_address = paymentData.address;
_receiveAddressController.text = _address!;
setState(() {
_addressToggleFlag = _receiveAddressController.text.isNotEmpty;
});
// now check for non standard encoded basic address
} else {
_address = qrResult.rawContent;
_receiveAddressController.text = _address ?? "";
setState(() {
_addressToggleFlag = _receiveAddressController.text.isNotEmpty;
});
}
} on PlatformException catch (e, s) {
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
void initState() {
_receiveAddressController = TextEditingController();
@ -1375,63 +1429,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
!isDesktop)
TextFieldIconButton(
key: const Key("buyViewScanQrButtonKey"),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(milliseconds: 75),
);
}
final qrResult = await scanner.scan();
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info,
);
final results = AddressUtils.parseUri(
qrResult.rawContent,
);
Logging.instance.log(
"qrResult parsed: $results",
level: LogLevel.Info,
);
if (results.isNotEmpty) {
// auto fill address
_address = results["address"] ?? "";
_receiveAddressController.text = _address!;
setState(() {
_addressToggleFlag =
_receiveAddressController
.text.isNotEmpty;
});
// now check for non standard encoded basic address
} else {
_address = qrResult.rawContent;
_receiveAddressController.text =
_address ?? "";
setState(() {
_addressToggleFlag =
_receiveAddressController
.text.isNotEmpty;
});
}
} on PlatformException catch (e, s) {
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
},
onTap: _onQrTapped,
child: const QrCodeIcon(),
),
],

View file

@ -70,6 +70,78 @@ class _Step2ViewState extends ConsumerState<Step2View> {
bool enableNext = false;
void _onRefundQrTapped() async {
try {
final qrResult = await scanner.scan();
final paymentData = AddressUtils.parsePaymentUri(
qrResult.rawContent,
logging: Logging.instance,
);
if (paymentData != null) {
// auto fill address
_refundController.text = paymentData.address;
model.refundAddress = _refundController.text;
setState(() {
enableNext = _toController.text.isNotEmpty &&
_refundController.text.isNotEmpty;
});
} else {
_refundController.text = qrResult.rawContent;
model.refundAddress = _refundController.text;
setState(() {
enableNext = _toController.text.isNotEmpty &&
_refundController.text.isNotEmpty;
});
}
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
}
void _onToQrTapped() async {
try {
final qrResult = await scanner.scan();
final paymentData = AddressUtils.parsePaymentUri(
qrResult.rawContent,
logging: Logging.instance,
);
if (paymentData != null) {
// auto fill address
_toController.text = paymentData.address;
model.recipientAddress = _toController.text;
setState(() {
enableNext = _toController.text.isNotEmpty &&
(_refundController.text.isNotEmpty ||
!ref.read(efExchangeProvider).supportsRefundAddress);
});
} else {
_toController.text = qrResult.rawContent;
model.recipientAddress = _toController.text;
setState(() {
enableNext = _toController.text.isNotEmpty &&
(_refundController.text.isNotEmpty ||
!!ref.read(efExchangeProvider).supportsRefundAddress);
});
}
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
void initState() {
model = widget.model;
@ -137,7 +209,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
if (mounted) {
if (context.mounted) {
Navigator.of(context).pop();
}
},
@ -405,50 +477,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
key: const Key(
"sendViewScanQrButtonKey",
),
onTap: () async {
try {
final qrResult =
await scanner.scan();
final results =
AddressUtils.parseUri(
qrResult.rawContent,
);
if (results.isNotEmpty) {
// auto fill address
_toController.text =
results["address"] ?? "";
model.recipientAddress =
_toController.text;
setState(() {
enableNext = _toController
.text.isNotEmpty &&
(_refundController.text
.isNotEmpty ||
!supportsRefund);
});
} else {
_toController.text =
qrResult.rawContent;
model.recipientAddress =
_toController.text;
setState(() {
enableNext = _toController
.text.isNotEmpty &&
(_refundController.text
.isNotEmpty ||
!supportsRefund);
});
}
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
},
onTap: _onToQrTapped,
child: const QrCodeIcon(),
),
],
@ -685,51 +714,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
key: const Key(
"sendViewScanQrButtonKey",
),
onTap: () async {
try {
final qrResult =
await scanner.scan();
final results =
AddressUtils.parseUri(
qrResult.rawContent,
);
if (results.isNotEmpty) {
// auto fill address
_refundController.text =
results["address"] ??
"";
model.refundAddress =
_refundController.text;
setState(() {
enableNext = _toController
.text
.isNotEmpty &&
_refundController
.text.isNotEmpty;
});
} else {
_refundController.text =
qrResult.rawContent;
model.refundAddress =
_refundController.text;
setState(() {
enableNext = _toController
.text
.isNotEmpty &&
_refundController
.text.isNotEmpty;
});
}
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
},
onTap: _onRefundQrTapped,
child: const QrCodeIcon(),
),
],

View file

@ -97,7 +97,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
initialDirectory: dir.path,
);
if (path != null) {
if (path != null && mounted) {
final file = File(path);
if (file.existsSync()) {
unawaited(
@ -109,6 +109,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
);
} else {
await file.writeAsBytes(pngBytes);
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
@ -118,6 +119,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
);
}
}
}
} else {
// await DocumentFileSavePlus.saveFile(
// pngBytes,
@ -167,7 +169,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
final Map<String, String> queryParams = {};
if (amountString.isNotEmpty) {
queryParams["amount"] = amountString;
queryParams["amount"] = amount.toString();
}
if (noteString.isNotEmpty) {
queryParams["message"] = noteString;
@ -311,7 +313,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 70));
}
if (mounted) {
if (context.mounted) {
Navigator.of(context).pop();
}
},

View file

@ -120,6 +120,69 @@ class _RecipientState extends ConsumerState<Recipient> {
}
}
void _onQrTapped() async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75,
),
);
}
final qrResult = await ref.read(pBarcodeScanner).scan();
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info,
);
final paymentData = AddressUtils.parsePaymentUri(
qrResult.rawContent,
logging: Logging.instance,
);
Logging.instance.log(
"qrResult parsed: $paymentData",
level: LogLevel.Info,
);
if (paymentData != null &&
paymentData.coin?.uriScheme == widget.coin.uriScheme) {
// auto fill address
addressController.text = paymentData.address.trim();
// autofill amount field
if (paymentData.amount != null) {
final Amount amount = Decimal.parse(paymentData.amount!).toAmount(
fractionDigits: widget.coin.fractionDigits,
);
amountController.text =
ref.read(pAmountFormatter(widget.coin)).format(
amount,
withUnitName: false,
);
}
} else {
addressController.text = qrResult.rawContent.trim();
}
setState(() {
_addressIsEmpty = addressController.text.isEmpty;
});
_updateRecipientData();
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while "
"trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
void initState() {
addressController = TextEditingController();
@ -289,76 +352,7 @@ class _RecipientState extends ConsumerState<Recipient> {
key: const Key(
"sendViewScanQrButtonKey",
),
onTap: () async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(
const Duration(
milliseconds: 75,
),
);
}
final qrResult =
await ref.read(pBarcodeScanner).scan();
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info,
);
/// TODO: deal with address utils
final results =
AddressUtils.parseUri(qrResult.rawContent);
Logging.instance.log(
"qrResult parsed: $results",
level: LogLevel.Info,
);
if (results.isNotEmpty &&
results["scheme"] ==
widget.coin.uriScheme) {
// auto fill address
addressController.text =
(results["address"] ?? "").trim();
// autofill amount field
if (results["amount"] != null) {
final Amount amount =
Decimal.parse(results["amount"]!)
.toAmount(
fractionDigits:
widget.coin.fractionDigits,
);
amountController.text = ref
.read(pAmountFormatter(widget.coin))
.format(
amount,
withUnitName: false,
);
}
} else {
addressController.text =
qrResult.rawContent.trim();
}
setState(() {
_addressIsEmpty =
addressController.text.isEmpty;
});
_updateRecipientData();
} on PlatformException catch (e, s) {
Logging.instance.log(
"Failed to get camera permissions while "
"trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
},
onTap: _onQrTapped,
child: const QrCodeIcon(),
),
],

View file

@ -11,10 +11,10 @@
import 'dart:async';
import 'dart:io';
import 'package:cs_monero/cs_monero.dart' as lib_monero;
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cs_monero/cs_monero.dart' as lib_monero;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tuple/tuple.dart';
@ -163,9 +163,13 @@ class _SendViewState extends ConsumerState<SendView> {
level: LogLevel.Info,
);
final paymentData = AddressUtils.parsePaymentUri(qrResult.rawContent);
final paymentData = AddressUtils.parsePaymentUri(
qrResult.rawContent,
logging: Logging.instance,
);
if (paymentData.coin.uriScheme == coin.uriScheme) {
if (paymentData != null &&
paymentData.coin?.uriScheme == coin.uriScheme) {
// auto fill address
_address = paymentData.address.trim();
sendToController.text = _address!;
@ -195,12 +199,8 @@ class _SendViewState extends ConsumerState<SendView> {
});
// now check for non standard encoded basic address
} else if (ref
.read(pWallets)
.getWallet(walletId)
.cryptoCurrency
.validateAddress(qrResult.rawContent)) {
_address = qrResult.rawContent.trim();
} else {
_address = qrResult.rawContent.split("\n").first.trim();
sendToController.text = _address ?? "";
_setValidAddressProviders(_address);

View file

@ -163,25 +163,30 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
level: LogLevel.Info,
);
final results = AddressUtils.parseUri(qrResult.rawContent);
final paymentData = AddressUtils.parsePaymentUri(
qrResult.rawContent,
logging: Logging.instance,
);
Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info);
Logging.instance
.log("qrResult parsed: $paymentData", level: LogLevel.Info);
if (results.isNotEmpty && results["scheme"] == coin.uriScheme) {
if (paymentData != null &&
paymentData.coin?.uriScheme == coin.uriScheme) {
// auto fill address
_address = (results["address"] ?? "").trim();
_address = paymentData.address.trim();
sendToController.text = _address!;
// autofill notes field
if (results["message"] != null) {
noteController.text = results["message"]!;
} else if (results["label"] != null) {
noteController.text = results["label"]!;
if (paymentData.message != null) {
noteController.text = paymentData.message!;
} else if (paymentData.label != null) {
noteController.text = paymentData.label!;
}
// autofill amount field
if (results["amount"] != null) {
final Amount amount = Decimal.parse(results["amount"]!).toAmount(
if (paymentData.amount != null) {
final Amount amount = Decimal.parse(paymentData.amount!).toAmount(
fractionDigits: tokenContract.decimals,
);
cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
@ -198,12 +203,8 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
});
// now check for non standard encoded basic address
} else if (ref
.read(pWallets)
.getWallet(walletId)
.cryptoCurrency
.validateAddress(qrResult.rawContent)) {
_address = qrResult.rawContent.trim();
} else {
_address = qrResult.rawContent.split("\n").first.trim();
sendToController.text = _address ?? "";
_updatePreviewButtonState(_address, _amountToSend);

View file

@ -10,11 +10,11 @@
import 'dart:async';
import 'package:cs_monero/cs_monero.dart' as lib_monero;
import 'package:decimal/decimal.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cs_monero/cs_monero.dart' as lib_monero;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
@ -145,7 +145,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
Future<void> scanWebcam() async {
try {
await showDialog(
await showDialog<void>(
context: context,
builder: (context) {
return QrCodeScannerDialog(
@ -153,16 +153,20 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
try {
_processQrCodeData(qrCodeData);
} catch (e, s) {
Logging.instance.log("Error processing QR code data: $e\n$s",
level: LogLevel.Error);
Logging.instance.log(
"Error processing QR code data: $e\n$s",
level: LogLevel.Error,
);
}
},
);
},
);
} catch (e, s) {
Logging.instance.log("Error opening QR code scanner dialog: $e\n$s",
level: LogLevel.Error);
Logging.instance.log(
"Error opening QR code scanner dialog: $e\n$s",
level: LogLevel.Error,
);
}
}
@ -655,84 +659,15 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
// return null;
// }
Future<void> scanQr() async {
try {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75));
}
final qrResult = await scanner.scan();
Logging.instance.log(
"qrResult content: ${qrResult.rawContent}",
level: LogLevel.Info,
);
final results = AddressUtils.parseUri(qrResult.rawContent);
Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info);
if (results.isNotEmpty && results["scheme"] == coin.uriScheme) {
// auto fill address
_address = results["address"] ?? "";
sendToController.text = _address!;
// autofill notes field
if (results["message"] != null) {
_note = results["message"]!;
} else if (results["label"] != null) {
_note = results["label"]!;
}
// autofill amount field
if (results["amount"] != null) {
final amount = Decimal.parse(results["amount"]!).toAmount(
fractionDigits: coin.fractionDigits,
);
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(amount, withUnitName: false);
ref.read(pSendAmount.notifier).state = amount;
}
setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty;
});
// now check for non standard encoded basic address
} else if (ref
.read(pWallets)
.getWallet(walletId)
.cryptoCurrency
.validateAddress(qrResult.rawContent)) {
_address = qrResult.rawContent;
sendToController.text = _address ?? "";
_setValidAddressProviders(_address);
setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty;
});
}
} on PlatformException catch (e, s) {
// here we ignore the exception caused by not giving permission
// to use the camera to scan a qr code
Logging.instance.log(
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
} catch (e, s) {
Logging.instance.log(
"Failed to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
}
void _processQrCodeData(String qrCodeData) {
try {
final paymentData = AddressUtils.parsePaymentUri(qrCodeData);
if (paymentData.coin.uriScheme == coin.uriScheme) {
final paymentData = AddressUtils.parsePaymentUri(
qrCodeData,
logging: Logging.instance,
);
if (paymentData != null &&
paymentData.coin?.uriScheme == coin.uriScheme) {
// Auto fill address.
_address = paymentData.address.trim();
sendToController.text = _address!;
@ -756,6 +691,14 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
_note = paymentData.label;
}
_setValidAddressProviders(_address);
setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty;
});
} else {
_address = qrCodeData.split("\n").first.trim();
sendToController.text = _address ?? "";
_setValidAddressProviders(_address);
setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty;
@ -807,8 +750,12 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
}
try {
final paymentData = AddressUtils.parsePaymentUri(content);
if (paymentData.coin.uriScheme == coin.uriScheme) {
final paymentData = AddressUtils.parsePaymentUri(
content,
logging: Logging.instance,
);
if (paymentData != null &&
paymentData.coin?.uriScheme == coin.uriScheme) {
// auto fill address
_address = paymentData.address;
sendToController.text = _address!;
@ -837,6 +784,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
_addressToggleFlag = sendToController.text.isNotEmpty;
});
} else {
content = content.split("\n").first.trim();
if (coin is Epiccash) {
content = AddressUtils().formatAddress(content);
}

View file

@ -14,14 +14,12 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../models/isar/models/contact_entry.dart';
import '../../../../models/paynym/paynym_account_lite.dart';
import '../../../../models/send_view_auto_fill_data.dart';
import '../../../../pages/send_view/confirm_transaction_view.dart';
import '../../../../pages/send_view/sub_widgets/building_transaction_dialog.dart';
import '../../../desktop_home_view.dart';
import 'address_book_address_chooser/address_book_address_chooser.dart';
import 'desktop_fee_dropdown.dart';
import '../../../../providers/providers.dart';
import '../../../../providers/ui/fee_rate_type_state_provider.dart';
import '../../../../providers/ui/preview_tx_button_state_provider.dart';
@ -51,6 +49,9 @@ import '../../../../widgets/icon_widgets/clipboard_icon.dart';
import '../../../../widgets/icon_widgets/x_icon.dart';
import '../../../../widgets/stack_text_field.dart';
import '../../../../widgets/textfield_icon_button.dart';
import '../../../desktop_home_view.dart';
import 'address_book_address_chooser/address_book_address_chooser.dart';
import 'desktop_fee_dropdown.dart';
// const _kCryptoAmountRegex = r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$';
@ -480,25 +481,30 @@ class _DesktopTokenSendState extends ConsumerState<DesktopTokenSend> {
level: LogLevel.Info,
);
final results = AddressUtils.parseUri(qrResult.rawContent);
final paymentData = AddressUtils.parsePaymentUri(
qrResult.rawContent,
logging: Logging.instance,
);
Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info);
Logging.instance
.log("qrResult parsed: $paymentData", level: LogLevel.Info);
if (results.isNotEmpty && results["scheme"] == coin.uriScheme) {
if (paymentData != null &&
paymentData.coin?.uriScheme == coin.uriScheme) {
// auto fill address
_address = results["address"] ?? "";
_address = paymentData.address.trim();
sendToController.text = _address!;
// autofill notes field
if (results["message"] != null) {
_note = results["message"]!;
} else if (results["label"] != null) {
_note = results["label"]!;
if (paymentData.message != null) {
_note = paymentData.message!;
} else if (paymentData.label != null) {
_note = paymentData.label!;
}
// autofill amount field
if (results["amount"] != null) {
final amount = Decimal.parse(results["amount"]!).toAmount(
if (paymentData.amount != null) {
final Amount amount = Decimal.parse(paymentData.amount!).toAmount(
fractionDigits:
ref.read(pCurrentTokenWallet)!.tokenContract.decimals,
);
@ -516,12 +522,8 @@ class _DesktopTokenSendState extends ConsumerState<DesktopTokenSend> {
});
// now check for non standard encoded basic address
} else if (ref
.read(pWallets)
.getWallet(walletId)
.cryptoCurrency
.validateAddress(qrResult.rawContent)) {
_address = qrResult.rawContent;
} else {
_address = qrResult.rawContent.split("\n").first.trim();
sendToController.text = _address ?? "";
_updatePreviewButtonState(_address, _amountToSend);

View file

@ -12,6 +12,7 @@ import 'dart:convert';
import '../app_config.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import 'logger.dart';
class AddressUtils {
static final Set<String> recognizedParams = {
@ -29,166 +30,16 @@ class AddressUtils {
return '${address.substring(0, 5)}...${address.substring(address.length - 5)}';
}
// static bool validateAddress(String address, Coin coin) {
// //This calls the validate address for each crypto coin, validateAddress is
// //only used in 2 places, so I just replaced the old functionality here
// switch (coin) {
// case Coin.bitcoin:
// return Bitcoin(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.bitcoinFrost:
// return BitcoinFrost(CryptoCurrencyNetwork.main)
// .validateAddress(address);
// case Coin.litecoin:
// return Litecoin(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.bitcoincash:
// return Bitcoincash(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.dogecoin:
// return Dogecoin(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.epicCash:
// return Epiccash(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.ethereum:
// return Ethereum(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.firo:
// return Firo(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.eCash:
// return Ecash(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.monero:
// return Monero(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.wownero:
// return Wownero(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.namecoin:
// return Namecoin(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.particl:
// return Particl(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.peercoin:
// return Peercoin(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.solana:
// return Solana(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.stellar:
// return Stellar(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.nano:
// return Nano(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.banano:
// return Banano(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.tezos:
// return Tezos(CryptoCurrencyNetwork.main).validateAddress(address);
// case Coin.bitcoinTestNet:
// return Bitcoin(CryptoCurrencyNetwork.test).validateAddress(address);
// case Coin.bitcoinFrostTestNet:
// return BitcoinFrost(CryptoCurrencyNetwork.test)
// .validateAddress(address);
// case Coin.litecoinTestNet:
// return Litecoin(CryptoCurrencyNetwork.test).validateAddress(address);
// case Coin.bitcoincashTestnet:
// return Bitcoincash(CryptoCurrencyNetwork.test).validateAddress(address);
// case Coin.firoTestNet:
// return Firo(CryptoCurrencyNetwork.test).validateAddress(address);
// case Coin.dogecoinTestNet:
// return Dogecoin(CryptoCurrencyNetwork.test).validateAddress(address);
// case Coin.peercoinTestNet:
// return Peercoin(CryptoCurrencyNetwork.test).validateAddress(address);
// case Coin.stellarTestnet:
// return Stellar(CryptoCurrencyNetwork.test).validateAddress(address);
// }
// // throw Exception("moved");
// // switch (coin) {
// // case Coin.bitcoin:
// // return Address.validateAddress(address, bitcoin);
// // case Coin.litecoin:
// // return Address.validateAddress(address, litecoin);
// // case Coin.bitcoincash:
// // try {
// // // 0 for bitcoincash: address scheme, 1 for legacy address
// // final format = bitbox.Address.detectFormat(address);
// //
// // if (coin == Coin.bitcoincashTestnet) {
// // return true;
// // }
// //
// // if (format == bitbox.Address.formatCashAddr) {
// // String addr = address;
// // if (addr.contains(":")) {
// // addr = addr.split(":").last;
// // }
// //
// // return addr.startsWith("q");
// // } else {
// // return address.startsWith("1");
// // }
// // } catch (e) {
// // return false;
// // }
// // case Coin.dogecoin:
// // return Address.validateAddress(address, dogecoin);
// // case Coin.epicCash:
// // return validateSendAddress(address) == "1";
// // case Coin.ethereum:
// // return true; //TODO - validate ETH address
// // case Coin.firo:
// // return Address.validateAddress(address, firoNetwork);
// // case Coin.eCash:
// // return Address.validateAddress(address, eCashNetwork);
// // case Coin.monero:
// // return RegExp("[a-zA-Z0-9]{95}").hasMatch(address) ||
// // RegExp("[a-zA-Z0-9]{106}").hasMatch(address);
// // case Coin.wownero:
// // return RegExp("[a-zA-Z0-9]{95}").hasMatch(address) ||
// // RegExp("[a-zA-Z0-9]{106}").hasMatch(address);
// // case Coin.namecoin:
// // return Address.validateAddress(address, namecoin, namecoin.bech32!);
// // case Coin.particl:
// // return Address.validateAddress(address, particl);
// // case Coin.stellar:
// // return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address);
// // case Coin.nano:
// // return NanoAccounts.isValid(NanoAccountType.NANO, address);
// // case Coin.banano:
// // return NanoAccounts.isValid(NanoAccountType.BANANO, address);
// // case Coin.tezos:
// // return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address);
// // case Coin.bitcoinTestNet:
// // return Address.validateAddress(address, testnet);
// // case Coin.litecoinTestNet:
// // return Address.validateAddress(address, litecointestnet);
// // case Coin.bitcoincashTestnet:
// // try {
// // // 0 for bitcoincash: address scheme, 1 for legacy address
// // final format = bitbox.Address.detectFormat(address);
// //
// // if (coin == Coin.bitcoincashTestnet) {
// // return true;
// // }
// //
// // if (format == bitbox.Address.formatCashAddr) {
// // String addr = address;
// // if (addr.contains(":")) {
// // addr = addr.split(":").last;
// // }
// //
// // return addr.startsWith("q");
// // } else {
// // return address.startsWith("1");
// // }
// // } catch (e) {
// // return false;
// // }
// // case Coin.firoTestNet:
// // return Address.validateAddress(address, firoTestNetwork);
// // case Coin.dogecoinTestNet:
// // return Address.validateAddress(address, dogecointestnet);
// // case Coin.stellarTestnet:
// // return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address);
// // }
// }
/// Return only recognized parameters.
static Map<String, String> filterParams(Map<String, String> params) {
return Map.fromEntries(params.entries
.where((entry) => recognizedParams.contains(entry.key.toLowerCase())));
static Map<String, String> _filterParams(Map<String, String> params) {
return Map.fromEntries(
params.entries
.where((entry) => recognizedParams.contains(entry.key.toLowerCase())),
);
}
/// Parses a URI string and returns a map with parsed components.
static Map<String, String> parseUri(String uri) {
static Map<String, String> _parseUri(String uri) {
final Map<String, String> result = {};
try {
final u = Uri.parse(uri);
@ -273,28 +124,36 @@ class AddressUtils {
}
/// Centralized method to handle various cryptocurrency URIs and return a common object.
static PaymentUriData parsePaymentUri(String uri) {
final Map<String, String> parsedData = parseUri(uri);
///
/// Returns null on failure to parse
static PaymentUriData? parsePaymentUri(
String uri, {
Logging? logging,
}) {
try {
final Map<String, String> parsedData = _parseUri(uri);
// Normalize the URI scheme.
final String scheme = parsedData['scheme'] ?? '';
parsedData.remove('scheme');
// Determine the coin type based on the URI scheme.
final CryptoCurrency coin = _getCryptoCurrencyByScheme(scheme);
// Filter out unrecognized parameters.
final filteredParams = filterParams(parsedData);
final filteredParams = _filterParams(parsedData);
return PaymentUriData(
coin: coin,
address: parsedData['address'] ?? '',
scheme: scheme,
address: parsedData['address']!.trim(),
amount: filteredParams['amount'] ?? filteredParams['tx_amount'],
label: filteredParams['label'] ?? filteredParams['recipient_name'],
message: filteredParams['message'] ?? filteredParams['tx_description'],
paymentId: filteredParams['tx_payment_id'], // Specific to Monero
paymentId: filteredParams['tx_payment_id'],
// Specific to Monero
additionalParams: filteredParams,
);
} catch (e, s) {
logging?.log("$e\n$s", level: LogLevel.Error);
return null;
}
}
/// Builds a uri string with the given address and query parameters (if any)
@ -304,7 +163,7 @@ class AddressUtils {
Map<String, String> params,
) {
// Filter unrecognized parameters.
final filteredParams = filterParams(params);
final filteredParams = _filterParams(params);
String uriString = "$scheme:$address";
if (scheme.toLowerCase() == "monero") {
@ -345,11 +204,12 @@ class AddressUtils {
}
/// Method to get CryptoCurrency based on URI scheme.
static CryptoCurrency _getCryptoCurrencyByScheme(String scheme) {
static CryptoCurrency? _getCryptoCurrencyByScheme(String scheme) {
if (AppConfig.coins.map((e) => e.uriScheme).toSet().contains(scheme)) {
return AppConfig.coins.firstWhere((e) => e.uriScheme == scheme);
} else {
throw UnsupportedError('Unsupported URI scheme: $scheme');
return null;
// throw UnsupportedError('Unsupported URI scheme: $scheme');
}
}
@ -375,21 +235,37 @@ class AddressUtils {
}
class PaymentUriData {
final CryptoCurrency coin;
final String address;
final String? scheme;
final String? amount;
final String? label;
final String? message;
final String? paymentId; // Specific to Monero.
final Map<String, String> additionalParams;
CryptoCurrency? get coin => AddressUtils._getCryptoCurrencyByScheme(
scheme ?? "", // empty will just return null
);
PaymentUriData({
required this.coin,
required this.address,
this.scheme,
this.amount,
this.label,
this.message,
this.paymentId,
required this.additionalParams,
});
@override
String toString() => "PaymentUriData { "
"coin: $coin, "
"address: $address, "
"amount: $amount, "
"scheme: $scheme, "
"label: $label, "
"message: $message, "
"paymentId: $paymentId, "
"additionalParams: $additionalParams"
" }";
}

View file

@ -1809,18 +1809,18 @@ packages:
dependency: "direct main"
description:
name: sqlite3
sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb
sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295
url: "https://pub.dev"
source: hosted
version: "2.4.5"
version: "2.4.3"
sqlite3_flutter_libs:
dependency: "direct main"
description:
name: sqlite3_flutter_libs
sha256: "7ae52b23366e5295005022e62fa093f64bfe190810223ea0ebf733a4cd140bce"
sha256: "1e62698dc1ab396152ccaf3b3990d826244e9f3c8c39b51805f209adcd6dbea3"
url: "https://pub.dev"
source: hosted
version: "0.5.26"
version: "0.5.22"
stack_trace:
dependency: transitive
description:

View file

@ -12,52 +12,49 @@ void main() {
test("parse a valid uri string A", () {
const uri = "dogecoin:$firoAddress?amount=50&label=eggs";
final result = AddressUtils.parseUri(uri);
expect(result, {
"scheme": "dogecoin",
"address": firoAddress,
"amount": "50",
"label": "eggs",
});
final result = AddressUtils.parsePaymentUri(uri);
expect(result, isNotNull);
expect(result!.scheme, "dogecoin");
expect(result.address, firoAddress);
expect(result.amount, "50");
expect(result.label, "eggs");
});
test("parse a valid uri string B", () {
const uri = "firo:$firoAddress?amount=50&message=eggs+are+good";
final result = AddressUtils.parseUri(uri);
expect(result, {
"scheme": "firo",
"address": firoAddress,
"amount": "50",
"message": "eggs are good",
});
final result = AddressUtils.parsePaymentUri(uri);
expect(result, isNotNull);
expect(result!.scheme, "firo");
expect(result.address, firoAddress);
expect(result.amount, "50");
expect(result.message, "eggs are good");
});
test("parse a valid uri string C", () {
const uri = "bitcoin:$firoAddress?amount=50.1&message=eggs%20are%20good%21";
final result = AddressUtils.parseUri(uri);
expect(result, {
"scheme": "bitcoin",
"address": firoAddress,
"amount": "50.1",
"message": "eggs are good!",
});
final result = AddressUtils.parsePaymentUri(uri);
expect(result, isNotNull);
expect(result!.scheme, "bitcoin");
expect(result.address, firoAddress);
expect(result.amount, "50.1");
expect(result.message, "eggs are good!");
});
test("parse an invalid uri string", () {
const uri = "firo$firoAddress?amount=50&label=eggs";
final result = AddressUtils.parseUri(uri);
expect(result, {});
final result = AddressUtils.parsePaymentUri(uri);
expect(result, isNull);
});
test("parse an invalid string", () {
const uri = "$firoAddress?amount=50&label=eggs";
final result = AddressUtils.parseUri(uri);
expect(result, {});
final result = AddressUtils.parsePaymentUri(uri);
expect(result, isNull);
});
test("parse an invalid uri string", () {
const uri = "::: 8 \\ %23";
expect(AddressUtils.parseUri(uri), {});
expect(AddressUtils.parsePaymentUri(uri), isNull);
});
test("encode a list of (mnemonic) words/strings as a json object", () {