feat: add BIP21 support (#414)

Co-authored-by: Erdem Yerebasmaz <erdem@yerebasmaz.com>
Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
This commit is contained in:
yse
2024-08-22 12:23:36 +02:00
committed by GitHub
parent 5248dfc235
commit 1a89bcd6c1
44 changed files with 5039 additions and 3089 deletions

View File

@@ -80,17 +80,21 @@ class _ConnectPageState extends State<ConnectPage> {
debugPrint("${mnemonic.isEmpty ? "Creating" : "Restoring"} wallet with $walletMnemonic");
return await initializeWallet(mnemonic: walletMnemonic).then(
(liquidSDK) async {
await widget.credentialsManager.storeMnemonic(mnemonic: walletMnemonic).then((_) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (BuildContext context) => HomePage(
liquidSDK: widget.liquidSDK,
credentialsManager: widget.credentialsManager,
),
),
);
});
await widget.credentialsManager.storeMnemonic(mnemonic: walletMnemonic).then(
(_) {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (BuildContext context) => HomePage(
liquidSDK: widget.liquidSDK,
credentialsManager: widget.credentialsManager,
),
),
);
}
},
);
},
);
}

View File

@@ -7,7 +7,11 @@ class HomePageDrawer extends StatefulWidget {
final BindingLiquidSdk liquidSDK;
final CredentialsManager credentialsManager;
const HomePageDrawer({super.key, required this.liquidSDK, required this.credentialsManager});
const HomePageDrawer({
super.key,
required this.liquidSDK,
required this.credentialsManager,
});
@override
State<HomePageDrawer> createState() => _HomePageDrawerState();
@@ -32,8 +36,11 @@ class _HomePageDrawerState extends State<HomePageDrawer> {
enabled: false,
leading: const Icon(Icons.backup_outlined),
title: const Text('Backup'),
titleTextStyle:
const TextStyle(fontSize: 16.0, color: Colors.white, decoration: TextDecoration.lineThrough),
titleTextStyle: const TextStyle(
fontSize: 16.0,
color: Colors.white,
decoration: TextDecoration.lineThrough,
),
onTap: () async {
try {
debugPrint("Creating backup.");
@@ -44,7 +51,10 @@ class _HomePageDrawerState extends State<HomePageDrawer> {
final errMsg = "Failed to create backup. $e";
debugPrint(errMsg);
if (context.mounted) {
final snackBar = SnackBar(behavior: SnackBarBehavior.floating, content: Text(errMsg));
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(errMsg),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
@@ -54,8 +64,11 @@ class _HomePageDrawerState extends State<HomePageDrawer> {
enabled: false,
leading: const Icon(Icons.restore),
title: const Text('Restore'),
titleTextStyle:
const TextStyle(fontSize: 16.0, color: Colors.white, decoration: TextDecoration.lineThrough),
titleTextStyle: const TextStyle(
fontSize: 16.0,
color: Colors.white,
decoration: TextDecoration.lineThrough,
),
onTap: () async {
try {
debugPrint("Restoring backup.");
@@ -67,7 +80,10 @@ class _HomePageDrawerState extends State<HomePageDrawer> {
final errMsg = "Failed to restore backup. $e";
debugPrint(errMsg);
if (context.mounted) {
final snackBar = SnackBar(behavior: SnackBarBehavior.floating, content: Text(errMsg));
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(errMsg),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
@@ -86,7 +102,10 @@ class _HomePageDrawerState extends State<HomePageDrawer> {
final errMsg = "Failed to empty wallet cache. $e";
debugPrint(errMsg);
if (context.mounted) {
final snackBar = SnackBar(behavior: SnackBarBehavior.floating, content: Text(errMsg));
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(errMsg),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
@@ -98,19 +117,26 @@ class _HomePageDrawerState extends State<HomePageDrawer> {
titleTextStyle: const TextStyle(fontSize: 16.0, color: Colors.white),
onTap: () async {
try {
await widget.credentialsManager.restoreMnemonic().then((mnemonics) {
showDialog(
context: context,
builder: (context) => MnemonicsDialog(
mnemonics: mnemonics.split(" "),
),
);
});
await widget.credentialsManager.restoreMnemonic().then(
(mnemonics) {
if (context.mounted) {
showDialog(
context: context,
builder: (context) => MnemonicsDialog(
mnemonics: mnemonics.split(" "),
),
);
}
},
);
} on Exception catch (e) {
final errMsg = "Failed to display mnemonics. $e";
debugPrint(errMsg);
if (context.mounted) {
final snackBar = SnackBar(behavior: SnackBarBehavior.floating, content: Text(errMsg));
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(errMsg),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}

View File

@@ -10,43 +10,7 @@ class PaymentItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListTile(
onLongPress: item.preimage != null
? () {
try {
debugPrint("Store payment preimage on clipboard. Preimage: ${item.preimage!}");
Clipboard.setData(ClipboardData(text: item.preimage!));
const snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Copied payment preimage to clipboard.'),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} catch (e) {
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Failed to copy payment preimage to clipboard. $e'),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
: item.swapId != null
? () {
try {
debugPrint("Store swap ID on clipboard. Swap ID: ${item.swapId!}");
Clipboard.setData(ClipboardData(text: item.swapId!));
const snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Copied swap ID to clipboard.'),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} catch (e) {
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Failed to copy payment preimage to clipboard. $e'),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
: null,
onLongPress: () => _onLongPress(context),
title: Text(_paymentTitle(item)),
subtitle: Text(
DateFormat('dd/MM/yyyy, HH:mm').format(
@@ -70,6 +34,67 @@ class PaymentItem extends StatelessWidget {
);
}
void _onLongPress(BuildContext context) {
final details = item.details;
if (details == null) return;
if (details is PaymentDetails_Lightning && details.preimage != null) {
try {
debugPrint("Store payment preimage on clipboard. Preimage: ${details.preimage!}");
Clipboard.setData(ClipboardData(text: details.preimage!));
const snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Copied payment preimage to clipboard.'),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} catch (e) {
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Failed to copy payment preimage to clipboard. $e'),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
if (details is PaymentDetails_Bitcoin) {
try {
debugPrint("Store swap ID on clipboard. Swap ID: ${details.swapId}");
Clipboard.setData(ClipboardData(text: details.swapId));
const snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Copied swap ID to clipboard.'),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} catch (e) {
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Failed to copy payment swap ID to clipboard. $e'),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
if (details is PaymentDetails_Liquid) {
try {
debugPrint("Store Liquid Address on clipboard. Liquid Address: ${details.destination}");
Clipboard.setData(ClipboardData(text: details.destination));
const snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Copied Liquid Address to clipboard.'),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} catch (e) {
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Failed to copy payment Liquid Address to clipboard. $e'),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}
return;
}
String _paymentTitle(Payment payment) {
final paymentType = payment.paymentType;

View File

@@ -153,7 +153,13 @@ class CancelScanButton extends StatelessWidget {
),
),
onPressed: () async {
controller.stop().then((_) => Navigator.of(context).pop());
controller.stop().then(
(_) {
if (context.mounted) {
Navigator.of(context).pop();
}
},
);
},
child: const Text(
"CANCEL",

View File

@@ -34,10 +34,12 @@ class QrActionButton extends StatelessWidget {
).then((barcode) {
if (barcode == null || barcode.isEmpty) return;
debugPrint("Scanned string: '$barcode'");
showDialog(
context: context,
builder: (context) => SendPaymentDialog(barcodeValue: barcode, liquidSdk: liquidSDK),
);
if (context.mounted) {
showDialog(
context: context,
builder: (context) => SendPaymentDialog(barcodeValue: barcode, liquidSdk: liquidSDK),
);
}
});
}
}

View File

@@ -22,8 +22,7 @@ class _ReceivePaymentDialogState extends State<ReceivePaymentDialog> {
int? feesSat;
bool creatingInvoice = false;
String? invoice;
String? invoiceId;
String? invoiceDestination;
StreamSubscription<List<Payment>>? streamSubscription;
@@ -31,10 +30,19 @@ class _ReceivePaymentDialogState extends State<ReceivePaymentDialog> {
void initState() {
super.initState();
streamSubscription = widget.paymentsStream.listen((paymentList) {
if (invoiceId != null && invoiceId!.isNotEmpty) {
if (paymentList.any((e) => e.swapId == invoiceId!)) {
debugPrint("Payment Received! Id: $invoiceId");
if (context.mounted) {
if (invoiceDestination != null && invoiceDestination!.isNotEmpty) {
if (paymentList.any(
(e) {
final details = e.details;
if (details == null) return false;
if (details is PaymentDetails_Lightning && details.preimage != null) {
return details.preimage! == invoiceDestination!;
}
return false;
},
)) {
debugPrint("Payment Received! Id: $invoiceDestination");
if (mounted) {
Navigator.of(context).pop();
}
}
@@ -51,13 +59,13 @@ class _ReceivePaymentDialogState extends State<ReceivePaymentDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: creatingInvoice ? null : Text(invoice != null ? "Invoice" : "Receive Payment"),
content: creatingInvoice || invoice != null
title: creatingInvoice ? null : Text(invoiceDestination != null ? "Invoice" : "Receive Payment"),
content: creatingInvoice || invoiceDestination != null
? Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (invoice != null) ...[
if (invoiceDestination != null) ...[
AspectRatio(
aspectRatio: 1,
child: SizedBox(
@@ -65,7 +73,7 @@ class _ReceivePaymentDialogState extends State<ReceivePaymentDialog> {
height: 200.0,
child: QrImageView(
embeddedImage: const AssetImage("assets/icons/app_icon.png"),
data: invoice!.toUpperCase(),
data: invoiceDestination!.toUpperCase(),
size: 200.0,
),
),
@@ -128,34 +136,36 @@ class _ReceivePaymentDialogState extends State<ReceivePaymentDialog> {
Navigator.of(context).pop();
},
),
if (invoice == null) ...[
if (invoiceDestination == null) ...[
TextButton(
onPressed: () async {
try {
setState(() => creatingInvoice = true);
int amountSat = int.parse(payerAmountController.text);
PrepareReceivePaymentRequest prepareReceiveReq =
PrepareReceivePaymentRequest(payerAmountSat: BigInt.from(amountSat));
PrepareReceivePaymentResponse prepareRes = await widget.liquidSDK.prepareReceivePayment(
PrepareReceiveRequest prepareReceiveReq = PrepareReceiveRequest(
paymentMethod: PaymentMethod.lightning,
payerAmountSat: BigInt.from(amountSat),
);
PrepareReceiveResponse prepareResponse = await widget.liquidSDK.prepareReceivePayment(
req: prepareReceiveReq,
);
setState(() {
payerAmountSat = prepareRes.payerAmountSat.toInt();
feesSat = prepareRes.feesSat.toInt();
payerAmountSat = prepareResponse.payerAmountSat?.toInt();
feesSat = prepareResponse.feesSat.toInt();
});
ReceivePaymentRequest receiveReq = ReceivePaymentRequest(prepareRes: prepareRes);
ReceivePaymentRequest receiveReq = ReceivePaymentRequest(
prepareResponse: prepareResponse,
);
ReceivePaymentResponse resp = await widget.liquidSDK.receivePayment(req: receiveReq);
debugPrint(
"Created Invoice for $payerAmountSat sats with $feesSat sats fees.\nInvoice:${resp.invoice}",
"Created Invoice for $payerAmountSat sats with $feesSat sats fees.\nDestination:${resp.destination}",
);
setState(() => invoice = resp.invoice);
setState(() => invoiceId = resp.id);
setState(() => invoiceDestination = resp.destination);
} catch (e) {
setState(() {
payerAmountSat = null;
feesSat = null;
invoice = null;
invoiceId = null;
invoiceDestination = null;
});
final errMsg = "Error receiving payment: $e";
debugPrint(errMsg);

View File

@@ -17,7 +17,7 @@ class _SendPaymentDialogState extends State<SendPaymentDialog> {
bool paymentInProgress = false;
PrepareSendResponse? sendPaymentReq;
SendPaymentRequest? sendPaymentReq;
@override
void initState() {
@@ -50,7 +50,7 @@ class _SendPaymentDialogState extends State<SendPaymentDialog> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Please confirm that you agree to the payment fee of ${sendPaymentReq!.feesSat} sats.',
'Please confirm that you agree to the payment fee of ${sendPaymentReq!.prepareResponse.feesSat} sats.',
),
],
),
@@ -84,12 +84,16 @@ class _SendPaymentDialogState extends State<SendPaymentDialog> {
onPressed: () async {
try {
setState(() => paymentInProgress = true);
PrepareSendRequest prepareSendReq =
PrepareSendRequest(invoice: invoiceController.text);
PrepareSendResponse req =
await widget.liquidSdk.prepareSendPayment(req: prepareSendReq);
debugPrint("PrepareSendResponse for ${req.invoice}, fees: ${req.feesSat}");
setState(() => sendPaymentReq = req);
PrepareSendRequest prepareSendReq = PrepareSendRequest(
destination: invoiceController.text,
);
PrepareSendResponse req = await widget.liquidSdk.prepareSendPayment(
req: prepareSendReq,
);
debugPrint(
"PrepareSendResponse for ${req.destination}, fees: ${req.feesSat}",
);
setState(() => sendPaymentReq = SendPaymentRequest(prepareResponse: req));
} catch (e) {
final errMsg = "Error preparing payment: $e";
debugPrint(errMsg);