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 = []; 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 @override
void initState() { void initState() {
addressLabelController = TextEditingController() addressLabelController = TextEditingController()
@ -404,71 +460,7 @@ class _NewContactAddressEntryFormState
null) null)
TextFieldIconButton( TextFieldIconButton(
key: const Key("addAddressBookEntryScanQrButtonKey"), key: const Key("addAddressBookEntryScanQrButtonKey"),
onTap: () async { onTap: _onQrTapped,
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,
);
}
},
child: const QrCodeIcon(), child: const QrCodeIcon(),
), ),
const SizedBox( 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 @override
void initState() { void initState() {
_receiveAddressController = TextEditingController(); _receiveAddressController = TextEditingController();
@ -1375,63 +1429,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
!isDesktop) !isDesktop)
TextFieldIconButton( TextFieldIconButton(
key: const Key("buyViewScanQrButtonKey"), key: const Key("buyViewScanQrButtonKey"),
onTap: () async { onTap: _onQrTapped,
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,
);
}
},
child: const QrCodeIcon(), child: const QrCodeIcon(),
), ),
], ],

View file

@ -70,6 +70,78 @@ class _Step2ViewState extends ConsumerState<Step2View> {
bool enableNext = false; 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 @override
void initState() { void initState() {
model = widget.model; model = widget.model;
@ -137,7 +209,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 75)); await Future<void>.delayed(const Duration(milliseconds: 75));
} }
if (mounted) { if (context.mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
}, },
@ -405,50 +477,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
key: const Key( key: const Key(
"sendViewScanQrButtonKey", "sendViewScanQrButtonKey",
), ),
onTap: () async { onTap: _onToQrTapped,
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,
);
}
},
child: const QrCodeIcon(), child: const QrCodeIcon(),
), ),
], ],
@ -685,51 +714,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
key: const Key( key: const Key(
"sendViewScanQrButtonKey", "sendViewScanQrButtonKey",
), ),
onTap: () async { onTap: _onRefundQrTapped,
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,
);
}
},
child: const QrCodeIcon(), child: const QrCodeIcon(),
), ),
], ],

View file

@ -97,7 +97,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
initialDirectory: dir.path, initialDirectory: dir.path,
); );
if (path != null) { if (path != null && mounted) {
final file = File(path); final file = File(path);
if (file.existsSync()) { if (file.existsSync()) {
unawaited( unawaited(
@ -109,13 +109,15 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
); );
} else { } else {
await file.writeAsBytes(pngBytes); await file.writeAsBytes(pngBytes);
unawaited( if (mounted) {
showFloatingFlushBar( unawaited(
type: FlushBarType.success, showFloatingFlushBar(
message: "$path saved!", type: FlushBarType.success,
context: context, message: "$path saved!",
), context: context,
); ),
);
}
} }
} }
} else { } else {
@ -167,7 +169,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
final Map<String, String> queryParams = {}; final Map<String, String> queryParams = {};
if (amountString.isNotEmpty) { if (amountString.isNotEmpty) {
queryParams["amount"] = amountString; queryParams["amount"] = amount.toString();
} }
if (noteString.isNotEmpty) { if (noteString.isNotEmpty) {
queryParams["message"] = noteString; queryParams["message"] = noteString;
@ -311,7 +313,7 @@ class _GenerateUriQrCodeViewState extends State<GenerateUriQrCodeView> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
await Future<void>.delayed(const Duration(milliseconds: 70)); await Future<void>.delayed(const Duration(milliseconds: 70));
} }
if (mounted) { if (context.mounted) {
Navigator.of(context).pop(); 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 @override
void initState() { void initState() {
addressController = TextEditingController(); addressController = TextEditingController();
@ -289,76 +352,7 @@ class _RecipientState extends ConsumerState<Recipient> {
key: const Key( key: const Key(
"sendViewScanQrButtonKey", "sendViewScanQrButtonKey",
), ),
onTap: () async { onTap: _onQrTapped,
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,
);
}
},
child: const QrCodeIcon(), child: const QrCodeIcon(),
), ),
], ],

View file

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

View file

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

View file

@ -10,11 +10,11 @@
import 'dart:async'; import 'dart:async';
import 'package:cs_monero/cs_monero.dart' as lib_monero;
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
@ -145,7 +145,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
Future<void> scanWebcam() async { Future<void> scanWebcam() async {
try { try {
await showDialog( await showDialog<void>(
context: context, context: context,
builder: (context) { builder: (context) {
return QrCodeScannerDialog( return QrCodeScannerDialog(
@ -153,16 +153,20 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
try { try {
_processQrCodeData(qrCodeData); _processQrCodeData(qrCodeData);
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Error processing QR code data: $e\n$s", Logging.instance.log(
level: LogLevel.Error); "Error processing QR code data: $e\n$s",
level: LogLevel.Error,
);
} }
}, },
); );
}, },
); );
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Error opening QR code scanner dialog: $e\n$s", Logging.instance.log(
level: LogLevel.Error); "Error opening QR code scanner dialog: $e\n$s",
level: LogLevel.Error,
);
} }
} }
@ -655,84 +659,15 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
// return null; // 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) { void _processQrCodeData(String qrCodeData) {
try { try {
final paymentData = AddressUtils.parsePaymentUri(qrCodeData); final paymentData = AddressUtils.parsePaymentUri(
if (paymentData.coin.uriScheme == coin.uriScheme) { qrCodeData,
logging: Logging.instance,
);
if (paymentData != null &&
paymentData.coin?.uriScheme == coin.uriScheme) {
// Auto fill address. // Auto fill address.
_address = paymentData.address.trim(); _address = paymentData.address.trim();
sendToController.text = _address!; sendToController.text = _address!;
@ -756,6 +691,14 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
_note = paymentData.label; _note = paymentData.label;
} }
_setValidAddressProviders(_address);
setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty;
});
} else {
_address = qrCodeData.split("\n").first.trim();
sendToController.text = _address ?? "";
_setValidAddressProviders(_address); _setValidAddressProviders(_address);
setState(() { setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty; _addressToggleFlag = sendToController.text.isNotEmpty;
@ -807,8 +750,12 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
} }
try { try {
final paymentData = AddressUtils.parsePaymentUri(content); final paymentData = AddressUtils.parsePaymentUri(
if (paymentData.coin.uriScheme == coin.uriScheme) { content,
logging: Logging.instance,
);
if (paymentData != null &&
paymentData.coin?.uriScheme == coin.uriScheme) {
// auto fill address // auto fill address
_address = paymentData.address; _address = paymentData.address;
sendToController.text = _address!; sendToController.text = _address!;
@ -837,6 +784,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
_addressToggleFlag = sendToController.text.isNotEmpty; _addressToggleFlag = sendToController.text.isNotEmpty;
}); });
} else { } else {
content = content.split("\n").first.trim();
if (coin is Epiccash) { if (coin is Epiccash) {
content = AddressUtils().formatAddress(content); content = AddressUtils().formatAddress(content);
} }

View file

@ -14,14 +14,12 @@ import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../models/isar/models/contact_entry.dart'; import '../../../../models/isar/models/contact_entry.dart';
import '../../../../models/paynym/paynym_account_lite.dart'; import '../../../../models/paynym/paynym_account_lite.dart';
import '../../../../models/send_view_auto_fill_data.dart'; import '../../../../models/send_view_auto_fill_data.dart';
import '../../../../pages/send_view/confirm_transaction_view.dart'; import '../../../../pages/send_view/confirm_transaction_view.dart';
import '../../../../pages/send_view/sub_widgets/building_transaction_dialog.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/providers.dart';
import '../../../../providers/ui/fee_rate_type_state_provider.dart'; import '../../../../providers/ui/fee_rate_type_state_provider.dart';
import '../../../../providers/ui/preview_tx_button_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/icon_widgets/x_icon.dart';
import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/stack_text_field.dart';
import '../../../../widgets/textfield_icon_button.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})$'; // 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, 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 // auto fill address
_address = results["address"] ?? ""; _address = paymentData.address.trim();
sendToController.text = _address!; sendToController.text = _address!;
// autofill notes field // autofill notes field
if (results["message"] != null) { if (paymentData.message != null) {
_note = results["message"]!; _note = paymentData.message!;
} else if (results["label"] != null) { } else if (paymentData.label != null) {
_note = results["label"]!; _note = paymentData.label!;
} }
// autofill amount field // autofill amount field
if (results["amount"] != null) { if (paymentData.amount != null) {
final amount = Decimal.parse(results["amount"]!).toAmount( final Amount amount = Decimal.parse(paymentData.amount!).toAmount(
fractionDigits: fractionDigits:
ref.read(pCurrentTokenWallet)!.tokenContract.decimals, ref.read(pCurrentTokenWallet)!.tokenContract.decimals,
); );
@ -516,12 +522,8 @@ class _DesktopTokenSendState extends ConsumerState<DesktopTokenSend> {
}); });
// now check for non standard encoded basic address // now check for non standard encoded basic address
} else if (ref } else {
.read(pWallets) _address = qrResult.rawContent.split("\n").first.trim();
.getWallet(walletId)
.cryptoCurrency
.validateAddress(qrResult.rawContent)) {
_address = qrResult.rawContent;
sendToController.text = _address ?? ""; sendToController.text = _address ?? "";
_updatePreviewButtonState(_address, _amountToSend); _updatePreviewButtonState(_address, _amountToSend);

View file

@ -12,6 +12,7 @@ import 'dart:convert';
import '../app_config.dart'; import '../app_config.dart';
import '../wallets/crypto_currency/crypto_currency.dart'; import '../wallets/crypto_currency/crypto_currency.dart';
import 'logger.dart';
class AddressUtils { class AddressUtils {
static final Set<String> recognizedParams = { static final Set<String> recognizedParams = {
@ -29,166 +30,16 @@ class AddressUtils {
return '${address.substring(0, 5)}...${address.substring(address.length - 5)}'; 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. /// Return only recognized parameters.
static Map<String, String> filterParams(Map<String, String> params) { static Map<String, String> _filterParams(Map<String, String> params) {
return Map.fromEntries(params.entries return Map.fromEntries(
.where((entry) => recognizedParams.contains(entry.key.toLowerCase()))); params.entries
.where((entry) => recognizedParams.contains(entry.key.toLowerCase())),
);
} }
/// Parses a URI string and returns a map with parsed components. /// 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 = {}; final Map<String, String> result = {};
try { try {
final u = Uri.parse(uri); final u = Uri.parse(uri);
@ -273,28 +124,36 @@ class AddressUtils {
} }
/// Centralized method to handle various cryptocurrency URIs and return a common object. /// 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. // Normalize the URI scheme.
final String scheme = parsedData['scheme'] ?? ''; final String scheme = parsedData['scheme'] ?? '';
parsedData.remove('scheme'); parsedData.remove('scheme');
// Determine the coin type based on the URI scheme. // Filter out unrecognized parameters.
final CryptoCurrency coin = _getCryptoCurrencyByScheme(scheme); final filteredParams = _filterParams(parsedData);
// Filter out unrecognized parameters. return PaymentUriData(
final filteredParams = filterParams(parsedData); scheme: scheme,
address: parsedData['address']!.trim(),
return PaymentUriData( amount: filteredParams['amount'] ?? filteredParams['tx_amount'],
coin: coin, label: filteredParams['label'] ?? filteredParams['recipient_name'],
address: parsedData['address'] ?? '', message: filteredParams['message'] ?? filteredParams['tx_description'],
amount: filteredParams['amount'] ?? filteredParams['tx_amount'], paymentId: filteredParams['tx_payment_id'],
label: filteredParams['label'] ?? filteredParams['recipient_name'], // Specific to Monero
message: filteredParams['message'] ?? filteredParams['tx_description'], additionalParams: filteredParams,
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) /// Builds a uri string with the given address and query parameters (if any)
@ -304,7 +163,7 @@ class AddressUtils {
Map<String, String> params, Map<String, String> params,
) { ) {
// Filter unrecognized parameters. // Filter unrecognized parameters.
final filteredParams = filterParams(params); final filteredParams = _filterParams(params);
String uriString = "$scheme:$address"; String uriString = "$scheme:$address";
if (scheme.toLowerCase() == "monero") { if (scheme.toLowerCase() == "monero") {
@ -345,11 +204,12 @@ class AddressUtils {
} }
/// Method to get CryptoCurrency based on URI scheme. /// 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)) { if (AppConfig.coins.map((e) => e.uriScheme).toSet().contains(scheme)) {
return AppConfig.coins.firstWhere((e) => e.uriScheme == scheme); return AppConfig.coins.firstWhere((e) => e.uriScheme == scheme);
} else { } else {
throw UnsupportedError('Unsupported URI scheme: $scheme'); return null;
// throw UnsupportedError('Unsupported URI scheme: $scheme');
} }
} }
@ -375,21 +235,37 @@ class AddressUtils {
} }
class PaymentUriData { class PaymentUriData {
final CryptoCurrency coin;
final String address; final String address;
final String? scheme;
final String? amount; final String? amount;
final String? label; final String? label;
final String? message; final String? message;
final String? paymentId; // Specific to Monero. final String? paymentId; // Specific to Monero.
final Map<String, String> additionalParams; final Map<String, String> additionalParams;
CryptoCurrency? get coin => AddressUtils._getCryptoCurrencyByScheme(
scheme ?? "", // empty will just return null
);
PaymentUriData({ PaymentUriData({
required this.coin,
required this.address, required this.address,
this.scheme,
this.amount, this.amount,
this.label, this.label,
this.message, this.message,
this.paymentId, this.paymentId,
required this.additionalParams, 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" dependency: "direct main"
description: description:
name: sqlite3 name: sqlite3
sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.5" version: "2.4.3"
sqlite3_flutter_libs: sqlite3_flutter_libs:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqlite3_flutter_libs name: sqlite3_flutter_libs
sha256: "7ae52b23366e5295005022e62fa093f64bfe190810223ea0ebf733a4cd140bce" sha256: "1e62698dc1ab396152ccaf3b3990d826244e9f3c8c39b51805f209adcd6dbea3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.26" version: "0.5.22"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:

View file

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