From afd485e59bff95deda923508f48e305cd079e22b Mon Sep 17 00:00:00 2001 From: bonomat Date: Sat, 25 Oct 2025 13:19:54 +0200 Subject: [PATCH 1/5] feat: generate ln invoice and add amount --- lib/src/rust/api/ark_api.dart | 40 ++++++- lib/src/rust/ark/client.dart | 6 +- lib/src/rust/frb_generated.dart | 145 +++++++++++++++++++++++-- lib/src/rust/frb_generated.io.dart | 48 ++++++++ lib/src/rust/frb_generated.web.dart | 48 ++++++++ lib/src/ui/screens/receive_screen.dart | 72 +++++++++++- pubspec.lock | 20 ++-- rust/src/api/ark_api.rs | 39 ++++++- rust/src/ark/client.rs | 19 +++- rust/src/ark/mod.rs | 24 +++- rust/src/frb_generated.rs | 103 +++++++++++++++++- rust/src/state.rs | 4 +- 12 files changed, 525 insertions(+), 43 deletions(-) diff --git a/lib/src/rust/api/ark_api.dart b/lib/src/rust/api/ark_api.dart index 2bc0736..ddffa20 100644 --- a/lib/src/rust/api/ark_api.dart +++ b/lib/src/rust/api/ark_api.dart @@ -8,6 +8,8 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:freezed_annotation/freezed_annotation.dart' hide protected; part 'ark_api.freezed.dart'; +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `fmt`, `fmt` + Future walletExists({required String dataDir}) => RustLib.instance.api.crateApiArkApiWalletExists(dataDir: dataDir); @@ -54,7 +56,8 @@ Future restoreWallet( Future balance() => RustLib.instance.api.crateApiArkApiBalance(); -Future address() => RustLib.instance.api.crateApiArkApiAddress(); +Future address({BigInt? amount}) => + RustLib.instance.api.crateApiArkApiAddress(amount: amount); Future> txHistory() => RustLib.instance.api.crateApiArkApiTxHistory(); @@ -77,15 +80,21 @@ class Addresses { final String boarding; final String offchain; final String bip21; + final BoltzSwap? lightning; const Addresses({ required this.boarding, required this.offchain, required this.bip21, + this.lightning, }); @override - int get hashCode => boarding.hashCode ^ offchain.hashCode ^ bip21.hashCode; + int get hashCode => + boarding.hashCode ^ + offchain.hashCode ^ + bip21.hashCode ^ + lightning.hashCode; @override bool operator ==(Object other) => @@ -94,7 +103,8 @@ class Addresses { runtimeType == other.runtimeType && boarding == other.boarding && offchain == other.offchain && - bip21 == other.bip21; + bip21 == other.bip21 && + lightning == other.lightning; } class Balance { @@ -115,6 +125,30 @@ class Balance { offchain == other.offchain; } +class BoltzSwap { + final String swapId; + final BigInt amountSats; + final String invoice; + + const BoltzSwap({ + required this.swapId, + required this.amountSats, + required this.invoice, + }); + + @override + int get hashCode => swapId.hashCode ^ amountSats.hashCode ^ invoice.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BoltzSwap && + runtimeType == other.runtimeType && + swapId == other.swapId && + amountSats == other.amountSats && + invoice == other.invoice; +} + class Info { final String serverPk; final String network; diff --git a/lib/src/rust/ark/client.dart b/lib/src/rust/ark/client.dart index cdd9cfb..d74c0df 100644 --- a/lib/src/rust/ark/client.dart +++ b/lib/src/rust/ark/client.dart @@ -1,10 +1,10 @@ // This file is automatically generated, so please do not edit it. -// @generated by `flutter_rust_bridge`@ 2.9.0. +// @generated by `flutter_rust_bridge`@ 2.11.1. // ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// Rust type: RustOpaqueMoi> -abstract class Balance implements RustOpaqueInterface {} +// Rust type: RustOpaqueMoi> +abstract class BoltzSwap implements RustOpaqueInterface {} diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index 20b899f..083bb83 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -83,7 +83,7 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { - Future crateApiArkApiAddress(); + Future crateApiArkApiAddress({BigInt? amount}); Future crateApiArkApiBalance(); @@ -138,10 +138,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { }); @override - Future crateApiArkApiAddress() { + Future crateApiArkApiAddress({BigInt? amount}) { return handler.executeNormal(NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_opt_box_autoadd_u_64(amount, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1, port: port_); }, @@ -150,14 +151,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeErrorData: sse_decode_AnyhowException, ), constMeta: kCrateApiArkApiAddressConstMeta, - argValues: [], + argValues: [amount], apiImpl: this, )); } TaskConstMeta get kCrateApiArkApiAddressConstMeta => const TaskConstMeta( debugName: "address", - argNames: [], + argNames: ["amount"], ); @override @@ -532,12 +533,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { Addresses dco_decode_addresses(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 3) - throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + if (arr.length != 4) + throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); return Addresses( boarding: dco_decode_String(arr[0]), offchain: dco_decode_String(arr[1]), bip21: dco_decode_String(arr[2]), + lightning: dco_decode_opt_box_autoadd_boltz_swap(arr[3]), ); } @@ -552,18 +554,43 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + BoltzSwap dco_decode_boltz_swap(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 3) + throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + return BoltzSwap( + swapId: dco_decode_String(arr[0]), + amountSats: dco_decode_u_64(arr[1]), + invoice: dco_decode_String(arr[2]), + ); + } + @protected bool dco_decode_bool(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return raw as bool; } + @protected + BoltzSwap dco_decode_box_autoadd_boltz_swap(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_boltz_swap(raw); + } + @protected PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return dco_decode_i_64(raw); } + @protected + BigInt dco_decode_box_autoadd_u_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_u_64(raw); + } + @protected PlatformInt64 dco_decode_i_64(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -624,12 +651,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + BoltzSwap? dco_decode_opt_box_autoadd_boltz_swap(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_boltz_swap(raw); + } + @protected PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return raw == null ? null : dco_decode_box_autoadd_i_64(raw); } + @protected + BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_u_64(raw); + } + @protected Transaction dco_decode_transaction(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -703,8 +742,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_boarding = sse_decode_String(deserializer); var var_offchain = sse_decode_String(deserializer); var var_bip21 = sse_decode_String(deserializer); + var var_lightning = sse_decode_opt_box_autoadd_boltz_swap(deserializer); return Addresses( - boarding: var_boarding, offchain: var_offchain, bip21: var_bip21); + boarding: var_boarding, + offchain: var_offchain, + bip21: var_bip21, + lightning: var_lightning); } @protected @@ -714,18 +757,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return Balance(offchain: var_offchain); } + @protected + BoltzSwap sse_decode_boltz_swap(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_swapId = sse_decode_String(deserializer); + var var_amountSats = sse_decode_u_64(deserializer); + var var_invoice = sse_decode_String(deserializer); + return BoltzSwap( + swapId: var_swapId, amountSats: var_amountSats, invoice: var_invoice); + } + @protected bool sse_decode_bool(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs return deserializer.buffer.getUint8() != 0; } + @protected + BoltzSwap sse_decode_box_autoadd_boltz_swap(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_boltz_swap(deserializer)); + } + @protected PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs return (sse_decode_i_64(deserializer)); } + @protected + BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_u_64(deserializer)); + } + @protected PlatformInt64 sse_decode_i_64(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -791,6 +856,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { totalSats: var_totalSats); } + @protected + BoltzSwap? sse_decode_opt_box_autoadd_boltz_swap( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_boltz_swap(deserializer)); + } else { + return null; + } + } + @protected PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -802,6 +879,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_u_64(deserializer)); + } else { + return null; + } + } + @protected Transaction sse_decode_transaction(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -894,6 +982,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.boarding, serializer); sse_encode_String(self.offchain, serializer); sse_encode_String(self.bip21, serializer); + sse_encode_opt_box_autoadd_boltz_swap(self.lightning, serializer); } @protected @@ -902,12 +991,27 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_offchain_balance(self.offchain, serializer); } + @protected + void sse_encode_boltz_swap(BoltzSwap self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.swapId, serializer); + sse_encode_u_64(self.amountSats, serializer); + sse_encode_String(self.invoice, serializer); + } + @protected void sse_encode_bool(bool self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs serializer.buffer.putUint8(self ? 1 : 0); } + @protected + void sse_encode_box_autoadd_boltz_swap( + BoltzSwap self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_boltz_swap(self, serializer); + } + @protected void sse_encode_box_autoadd_i_64( PlatformInt64 self, SseSerializer serializer) { @@ -915,6 +1019,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_i_64(self, serializer); } + @protected + void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_64(self, serializer); + } + @protected void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -967,6 +1077,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_64(self.totalSats, serializer); } + @protected + void sse_encode_opt_box_autoadd_boltz_swap( + BoltzSwap? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_boltz_swap(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_i_64( PlatformInt64? self, SseSerializer serializer) { @@ -978,6 +1099,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_u_64(self, serializer); + } + } + @protected void sse_encode_transaction(Transaction self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart index 7fc2233..fd8549d 100644 --- a/lib/src/rust/frb_generated.io.dart +++ b/lib/src/rust/frb_generated.io.dart @@ -35,12 +35,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Balance dco_decode_balance(dynamic raw); + @protected + BoltzSwap dco_decode_boltz_swap(dynamic raw); + @protected bool dco_decode_bool(dynamic raw); + @protected + BoltzSwap dco_decode_box_autoadd_boltz_swap(dynamic raw); + @protected PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw); + @protected + BigInt dco_decode_box_autoadd_u_64(dynamic raw); + @protected PlatformInt64 dco_decode_i_64(dynamic raw); @@ -59,9 +68,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected OffchainBalance dco_decode_offchain_balance(dynamic raw); + @protected + BoltzSwap? dco_decode_opt_box_autoadd_boltz_swap(dynamic raw); + @protected PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw); + @protected + BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw); + @protected Transaction dco_decode_transaction(dynamic raw); @@ -90,12 +105,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Balance sse_decode_balance(SseDeserializer deserializer); + @protected + BoltzSwap sse_decode_boltz_swap(SseDeserializer deserializer); + @protected bool sse_decode_bool(SseDeserializer deserializer); + @protected + BoltzSwap sse_decode_box_autoadd_boltz_swap(SseDeserializer deserializer); + @protected PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer); + @protected + BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer); + @protected PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); @@ -114,9 +138,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected OffchainBalance sse_decode_offchain_balance(SseDeserializer deserializer); + @protected + BoltzSwap? sse_decode_opt_box_autoadd_boltz_swap( + SseDeserializer deserializer); + @protected PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer); + @protected + BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer); + @protected Transaction sse_decode_transaction(SseDeserializer deserializer); @@ -149,13 +180,23 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_balance(Balance self, SseSerializer serializer); + @protected + void sse_encode_boltz_swap(BoltzSwap self, SseSerializer serializer); + @protected void sse_encode_bool(bool self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_boltz_swap( + BoltzSwap self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_i_64( PlatformInt64 self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer); + @protected void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); @@ -177,10 +218,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_offchain_balance( OffchainBalance self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_boltz_swap( + BoltzSwap? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_i_64( PlatformInt64? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer); + @protected void sse_encode_transaction(Transaction self, SseSerializer serializer); diff --git a/lib/src/rust/frb_generated.web.dart b/lib/src/rust/frb_generated.web.dart index 9c33752..6bc592b 100644 --- a/lib/src/rust/frb_generated.web.dart +++ b/lib/src/rust/frb_generated.web.dart @@ -37,12 +37,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Balance dco_decode_balance(dynamic raw); + @protected + BoltzSwap dco_decode_boltz_swap(dynamic raw); + @protected bool dco_decode_bool(dynamic raw); + @protected + BoltzSwap dco_decode_box_autoadd_boltz_swap(dynamic raw); + @protected PlatformInt64 dco_decode_box_autoadd_i_64(dynamic raw); + @protected + BigInt dco_decode_box_autoadd_u_64(dynamic raw); + @protected PlatformInt64 dco_decode_i_64(dynamic raw); @@ -61,9 +70,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected OffchainBalance dco_decode_offchain_balance(dynamic raw); + @protected + BoltzSwap? dco_decode_opt_box_autoadd_boltz_swap(dynamic raw); + @protected PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw); + @protected + BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw); + @protected Transaction dco_decode_transaction(dynamic raw); @@ -92,12 +107,21 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Balance sse_decode_balance(SseDeserializer deserializer); + @protected + BoltzSwap sse_decode_boltz_swap(SseDeserializer deserializer); + @protected bool sse_decode_bool(SseDeserializer deserializer); + @protected + BoltzSwap sse_decode_box_autoadd_boltz_swap(SseDeserializer deserializer); + @protected PlatformInt64 sse_decode_box_autoadd_i_64(SseDeserializer deserializer); + @protected + BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer); + @protected PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); @@ -116,9 +140,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected OffchainBalance sse_decode_offchain_balance(SseDeserializer deserializer); + @protected + BoltzSwap? sse_decode_opt_box_autoadd_boltz_swap( + SseDeserializer deserializer); + @protected PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer); + @protected + BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer); + @protected Transaction sse_decode_transaction(SseDeserializer deserializer); @@ -151,13 +182,23 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_balance(Balance self, SseSerializer serializer); + @protected + void sse_encode_boltz_swap(BoltzSwap self, SseSerializer serializer); + @protected void sse_encode_bool(bool self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_boltz_swap( + BoltzSwap self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_i_64( PlatformInt64 self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer); + @protected void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); @@ -179,10 +220,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_offchain_balance( OffchainBalance self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_boltz_swap( + BoltzSwap? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_i_64( PlatformInt64? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer); + @protected void sse_encode_transaction(Transaction self, SseSerializer serializer); diff --git a/lib/src/ui/screens/receive_screen.dart b/lib/src/ui/screens/receive_screen.dart index da11767..51e2f0d 100644 --- a/lib/src/ui/screens/receive_screen.dart +++ b/lib/src/ui/screens/receive_screen.dart @@ -28,14 +28,18 @@ class ReceiveScreenState extends State { String _bip21Address = ""; String _btcAddress = ""; String _arkAddress = ""; + String _lightningInvoice = ""; bool _showCopyMenu = false; + final TextEditingController _amountController = TextEditingController(); + // Track which addresses have been copied (for showing checkmarks) final Map _copiedAddresses = { 'BIP21': false, 'BTC': false, 'Ark': false, + 'Lightning': false, }; // Timers for resetting the checkmarks @@ -43,6 +47,7 @@ class ReceiveScreenState extends State { 'BIP21': null, 'BTC': null, 'Ark': null, + 'Lightning': null, }; @override @@ -53,11 +58,21 @@ class ReceiveScreenState extends State { Future _fetchAddresses() async { try { - final addresses = await address(); + // Parse amount from controller (in sats) + BigInt? amountSats; + if (_amountController.text.isNotEmpty) { + final parsedAmount = int.tryParse(_amountController.text); + if (parsedAmount != null && parsedAmount > 0) { + amountSats = BigInt.from(parsedAmount); + } + } + + final addresses = await address(amount: amountSats); setState(() { _bip21Address = addresses.bip21; _arkAddress = addresses.offchain; _btcAddress = addresses.boarding; + _lightningInvoice = addresses.lightning?.invoice ?? ""; }); } catch (e) { logger.e("Error fetching addresses: $e"); @@ -75,6 +90,7 @@ class ReceiveScreenState extends State { timer.cancel(); } }); + _amountController.dispose(); super.dispose(); } @@ -136,6 +152,8 @@ class ReceiveScreenState extends State { _buildShareOption('BIP21 Address', 'BIP21'), _buildShareOption('BTC Address', 'BTC'), _buildShareOption('Ark Address', 'Ark'), + if (_lightningInvoice.isNotEmpty) + _buildShareOption('Lightning Invoice', 'Lightning'), _buildShareOption('QR Code Image', 'QR'), ], ), @@ -161,6 +179,10 @@ class ReceiveScreenState extends State { addressToShare = _arkAddress; addressType = "Ark"; break; + case 'Lightning': + addressToShare = _lightningInvoice; + addressType = "Lightning"; + break; case 'QR': // Share the QR code as an image await _shareQrCodeImage(); @@ -259,6 +281,42 @@ class ReceiveScreenState extends State { padding: const EdgeInsets.all(16.0), child: Column( children: [ + // Amount input field + Container( + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(8.0), + ), + child: TextField( + controller: _amountController, + keyboardType: TextInputType.number, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: 'Amount (sats)', + labelStyle: TextStyle(color: Colors.grey[400]), + // TODO: This minimum amount requirement is not true and will be removed + helperText: 'Lightning invoice requires amount ≥ 500 sats', + helperStyle: TextStyle(color: Colors.grey[500], fontSize: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + suffixIcon: IconButton( + icon: const Icon(Icons.refresh, color: Colors.amber), + onPressed: _fetchAddresses, + tooltip: 'Generate addresses', + ), + ), + onSubmitted: (_) => _fetchAddresses(), + ), + ), + + const SizedBox(height: 24), + // QR Code RepaintBoundary( key: _qrKey, @@ -405,6 +463,18 @@ class ReceiveScreenState extends State { onTap: () => _copyAddress(_arkAddress, 'Ark'), isCopied: _copiedAddresses['Ark']!, ), + + // Lightning Invoice (only show if available) + if (_lightningInvoice.isNotEmpty) ...[ + const Divider( + height: 1, indent: 16, endIndent: 16, color: Colors.grey), + _buildAddressOption( + label: 'Lightning invoice', + address: _lightningInvoice, + onTap: () => _copyAddress(_lightningInvoice, 'Lightning'), + isCopied: _copiedAddresses['Lightning']!, + ), + ], ], ), ); diff --git a/pubspec.lock b/pubspec.lock index dd47ecb..9c447ab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.12.0" boolean_selector: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: @@ -379,10 +379,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: @@ -839,10 +839,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "14.3.1" watcher: dependency: transitive description: @@ -879,10 +879,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.0.4" win32: dependency: transitive description: diff --git a/rust/src/api/ark_api.rs b/rust/src/api/ark_api.rs index 311f2bb..009e9c8 100644 --- a/rust/src/api/ark_api.rs +++ b/rust/src/api/ark_api.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use bitcoin::{Amount, Network}; +use bitcoin::Network; use nostr::ToBech32; use std::str::FromStr; @@ -62,21 +62,50 @@ pub async fn balance() -> Result { }) } +#[derive(Debug, Clone)] pub struct Addresses { pub boarding: String, pub offchain: String, pub bip21: String, + pub lightning: Option, } -pub fn address() -> Result { - let addresses = crate::ark::client::address()?; +#[derive(Debug, Clone)] +pub struct BoltzSwap { + pub swap_id: String, + pub amount_sats: u64, + pub invoice: String, +} + +pub async fn address(amount: Option) -> Result { + let addresses = crate::ark::client::address(amount.map(bitcoin::Amount::from_sat)).await?; let boarding = addresses.boarding.to_string(); let offchain = addresses.offchain.encode(); - let bip21 = format!("bitcoin:{boarding}?ark={offchain}"); + let lightning = addresses.boltz_swap; + let amount = match amount { + None => "".to_string(), + Some(a) => { + format!("&amount={}", a.to_string()) + } + }; + + let lightning_invoice = match &lightning { + None => "".to_string(), + Some(lightning) => { + format!("&lightning={}", lightning.invoice) + } + }; + + let bip21 = format!("bitcoin:{boarding}?arkade={offchain}{lightning_invoice}{amount}",); Ok(Addresses { boarding, offchain, + lightning: lightning.map(|lightning| BoltzSwap { + swap_id: lightning.swap_id, + amount_sats: lightning.amount.to_sat(), + invoice: lightning.invoice, + }), bip21, }) } @@ -141,7 +170,7 @@ pub async fn tx_history() -> Result> { } pub async fn send(address: String, amount_sats: u64) -> Result { - let amount = Amount::from_sat(amount_sats); + let amount = bitcoin::Amount::from_sat(amount_sats); let txid = crate::ark::client::send(address, amount).await?; Ok(txid.to_string()) } diff --git a/rust/src/ark/client.rs b/rust/src/ark/client.rs index 02910a7..20c987f 100644 --- a/rust/src/ark/client.rs +++ b/rust/src/ark/client.rs @@ -44,12 +44,19 @@ pub async fn balance() -> Result { } } +pub struct BoltzSwap { + pub swap_id: String, + pub amount: Amount, + pub invoice: String, +} + pub struct Addresses { pub boarding: Address, pub offchain: ArkAddress, + pub boltz_swap: Option, } -pub fn address() -> Result { +pub async fn address(amount: Option) -> Result { let maybe_client = ARK_CLIENT.try_get(); match maybe_client { @@ -70,9 +77,19 @@ pub fn address() -> Result { .get_offchain_address() .map_err(|error| anyhow!("Could not get offchain address {error:#}"))?; + let reverse_swap_result = match amount { + None => None, + Some(amount) => Some(client.get_ln_invoice(amount).await?), + }; + Ok(Addresses { boarding: boarding_address, offchain: offchain_address, + boltz_swap: reverse_swap_result.map(|s| BoltzSwap { + swap_id: s.swap_id, + amount: s.amount, + invoice: s.invoice.to_string(), + }), }) } } diff --git a/rust/src/ark/mod.rs b/rust/src/ark/mod.rs index 62b823e..4b04581 100644 --- a/rust/src/ark/mod.rs +++ b/rust/src/ark/mod.rs @@ -9,13 +9,14 @@ use crate::ark::seed_file::{read_seed_file, reset_wallet, write_seed_file}; use crate::ark::storage::InMemoryDb; use crate::state::ARK_CLIENT; use anyhow::{anyhow, bail, Result}; -use ark_client::{InMemorySwapStorage, OfflineClient}; +use ark_client::{OfflineClient, SqliteSwapStorage}; use bitcoin::key::{Keypair, Secp256k1}; use bitcoin::secp256k1::{All, SecretKey}; use bitcoin::Network; use nostr::Keys; use parking_lot::RwLock; use rand::RngCore; +use std::path::Path; use std::sync::Arc; use std::time::Duration; @@ -35,7 +36,8 @@ pub async fn setup_new_wallet( let sk = SecretKey::from_slice(&random_bytes) .map_err(|e| anyhow::anyhow!("Failed to create secret key: {}", e))?; - write_seed_file(&sk, data_dir).map_err(|e| anyhow!("Failed to write seed file: {}", e))?; + write_seed_file(&sk, data_dir.clone()) + .map_err(|e| anyhow!("Failed to write seed file: {}", e))?; let kp = Keypair::from_secret_key(&secp, &sk); let pubkey = setup_client( @@ -45,6 +47,7 @@ pub async fn setup_new_wallet( esplora.clone(), server.clone(), boltz_url.clone(), + data_dir ) .await .map_err(|e| { @@ -73,10 +76,10 @@ pub async fn restore_wallet( let keys = Keys::parse(nsec.as_str()).map_err(|e| anyhow!("Failed to parse nsec key: {}", e))?; let kp = *keys.key_pair(&secp); - write_seed_file(&kp.secret_key(), data_dir) + write_seed_file(&kp.secret_key(), data_dir.clone()) .map_err(|e| anyhow!("Failed to write seed file: {}", e))?; - let pubkey = setup_client(kp, secp, network, esplora.clone(), server.clone(), boltz_url).await + let pubkey = setup_client(kp, secp, network, esplora.clone(), server.clone(), boltz_url,data_dir ).await .map_err(|e| anyhow!("Failed to setup client after restore - Network: {:?}, Esplora: {}, Server: {} - Error: {}", network, esplora, server, e))?; Ok(pubkey) } @@ -99,7 +102,7 @@ pub(crate) async fn load_existing_wallet( Some(key) => { let secp = Secp256k1::new(); let kp = Keypair::from_secret_key(&secp, &key); - let server_pk = setup_client(kp, secp, network, esplora.clone(), server.clone(), boltz_url).await + let server_pk = setup_client(kp, secp, network, esplora.clone(), server.clone(), boltz_url, data_dir ).await .map_err(|e| anyhow!("Failed to setup client from existing wallet - Network: {:?}, Esplora: {}, Server: {} - Error: {}", network, esplora, server, e))?; Ok(server_pk) } @@ -113,6 +116,7 @@ pub async fn setup_client( esplora_url: String, server: String, boltz_url: String, + data_dir: String, ) -> Result { let db = InMemoryDb::default(); @@ -135,13 +139,21 @@ pub async fn setup_client( .map_err(|e| anyhow!("Failed to connect to Esplora at '{}': {}", esplora_url, e))?; tracing::info!("Connecting to Ark"); + + let data_path = Path::new(data_dir.as_str()); + let swap_storage = data_path.join("boltz_swap_storage.sqlite"); + + let sqlite_storage = SqliteSwapStorage::new(swap_storage) + .await + .map_err(|e| anyhow!(e))?; + let client = OfflineClient::new( "sample-client".to_string(), kp, Arc::new(esplora), wallet, server.clone(), - Arc::new(InMemorySwapStorage::new()), + Arc::new(sqlite_storage), boltz_url, Duration::from_secs(30), ) diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 4fcd129..98570d4 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -51,7 +51,7 @@ fn wire__crate__api__ark_api__address_impl( rust_vec_len_: i32, data_len_: i32, ) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { debug_name: "address", port: Some(port_), @@ -67,13 +67,15 @@ fn wire__crate__api__ark_api__address_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_amount = >::sse_decode(&mut deserializer); deserializer.end(); - move |context| { + move |context| async move { transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( - (move || { - let output_ok = crate::api::ark_api::address()?; + (move || async move { + let output_ok = crate::api::ark_api::address(api_amount).await?; Ok(output_ok) - })(), + })() + .await, ) } }, @@ -615,10 +617,12 @@ impl SseDecode for crate::api::ark_api::Addresses { let mut var_boarding = ::sse_decode(deserializer); let mut var_offchain = ::sse_decode(deserializer); let mut var_bip21 = ::sse_decode(deserializer); + let mut var_lightning = >::sse_decode(deserializer); return crate::api::ark_api::Addresses { boarding: var_boarding, offchain: var_offchain, bip21: var_bip21, + lightning: var_lightning, }; } } @@ -633,6 +637,20 @@ impl SseDecode for crate::api::ark_api::Balance { } } +impl SseDecode for crate::api::ark_api::BoltzSwap { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_swapId = ::sse_decode(deserializer); + let mut var_amountSats = ::sse_decode(deserializer); + let mut var_invoice = ::sse_decode(deserializer); + return crate::api::ark_api::BoltzSwap { + swap_id: var_swapId, + amount_sats: var_amountSats, + invoice: var_invoice, + }; + } +} + impl SseDecode for bool { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -719,6 +737,17 @@ impl SseDecode for crate::api::ark_api::OffchainBalance { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -730,6 +759,17 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for crate::api::ark_api::Transaction { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -850,6 +890,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::ark_api::Addresses { self.boarding.into_into_dart().into_dart(), self.offchain.into_into_dart().into_dart(), self.bip21.into_into_dart().into_dart(), + self.lightning.into_into_dart().into_dart(), ] .into_dart() } @@ -880,6 +921,28 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::ark_api::BoltzSwap { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.swap_id.into_into_dart().into_dart(), + self.amount_sats.into_into_dart().into_dart(), + self.invoice.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::ark_api::BoltzSwap +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::ark_api::BoltzSwap +{ + fn into_into_dart(self) -> crate::api::ark_api::BoltzSwap { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::ark_api::Info { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -1024,6 +1087,7 @@ impl SseEncode for crate::api::ark_api::Addresses { ::sse_encode(self.boarding, serializer); ::sse_encode(self.offchain, serializer); ::sse_encode(self.bip21, serializer); + >::sse_encode(self.lightning, serializer); } } @@ -1034,6 +1098,15 @@ impl SseEncode for crate::api::ark_api::Balance { } } +impl SseEncode for crate::api::ark_api::BoltzSwap { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.swap_id, serializer); + ::sse_encode(self.amount_sats, serializer); + ::sse_encode(self.invoice, serializer); + } +} + impl SseEncode for bool { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1098,6 +1171,16 @@ impl SseEncode for crate::api::ark_api::OffchainBalance { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1108,6 +1191,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for crate::api::ark_api::Transaction { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { diff --git a/rust/src/state.rs b/rust/src/state.rs index 890c9be..8709fd5 100644 --- a/rust/src/state.rs +++ b/rust/src/state.rs @@ -3,7 +3,7 @@ use crate::ark::storage::InMemoryDb; use crate::frb_generated::StreamSink; use crate::logger::LogEntry; use ark_bdk_wallet::Wallet; -use ark_client::{Client, InMemorySwapStorage}; +use ark_client::{Client, SqliteSwapStorage}; use parking_lot::RwLock; use state::InitCell; use std::sync::Arc; @@ -11,5 +11,5 @@ use std::sync::Arc; pub static LOG_STREAM_SINK: InitCell>>> = InitCell::new(); #[allow(clippy::type_complexity)] pub static ARK_CLIENT: InitCell< - RwLock, InMemorySwapStorage>>>, + RwLock, SqliteSwapStorage>>>, > = InitCell::new(); From 4ba8fe5053cc28110f6284f07fa50faba5036ae6 Mon Sep 17 00:00:00 2001 From: bonomat Date: Sat, 25 Oct 2025 23:54:11 +0400 Subject: [PATCH 2/5] feat: monitor for payment --- lib/src/rust/api/ark_api.dart | 32 ++++ lib/src/rust/frb_generated.dart | 102 ++++++++++++- lib/src/rust/frb_generated.io.dart | 19 +++ lib/src/rust/frb_generated.web.dart | 19 +++ lib/src/ui/screens/receive_screen.dart | 194 +++++++++++++++++++++++++ pubspec.lock | 20 +-- rust/Cargo.lock | 18 ++- rust/Cargo.toml | 10 +- rust/src/api/ark_api.rs | 38 +++++ rust/src/ark/client.rs | 158 +++++++++++++++++++- rust/src/frb_generated.rs | 112 +++++++++++++- 11 files changed, 696 insertions(+), 26 deletions(-) diff --git a/lib/src/rust/api/ark_api.dart b/lib/src/rust/api/ark_api.dart index ddffa20..e8c4758 100644 --- a/lib/src/rust/api/ark_api.dart +++ b/lib/src/rust/api/ark_api.dart @@ -76,6 +76,17 @@ Future resetWallet({required String dataDir}) => Future information() => RustLib.instance.api.crateApiArkApiInformation(); +Future waitForPayment( + {String? arkAddress, + String? boardingAddress, + String? boltzSwapId, + required BigInt timeoutSeconds}) => + RustLib.instance.api.crateApiArkApiWaitForPayment( + arkAddress: arkAddress, + boardingAddress: boardingAddress, + boltzSwapId: boltzSwapId, + timeoutSeconds: timeoutSeconds); + class Addresses { final String boarding; final String offchain; @@ -195,6 +206,27 @@ class OffchainBalance { totalSats == other.totalSats; } +class PaymentReceived { + final String txid; + final BigInt amountSats; + + const PaymentReceived({ + required this.txid, + required this.amountSats, + }); + + @override + int get hashCode => txid.hashCode ^ amountSats.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PaymentReceived && + runtimeType == other.runtimeType && + txid == other.txid && + amountSats == other.amountSats; +} + @freezed sealed class Transaction with _$Transaction { const Transaction._(); diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index 083bb83..ad6ea78 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -72,7 +72,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => 2080225858; + int get rustContentHash => -95231310; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -126,6 +126,12 @@ abstract class RustLibApi extends BaseApi { Future> crateApiArkApiTxHistory(); + Future crateApiArkApiWaitForPayment( + {String? arkAddress, + String? boardingAddress, + String? boltzSwapId, + required BigInt timeoutSeconds}); + Future crateApiArkApiWalletExists({required String dataDir}); } @@ -487,6 +493,43 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: [], ); + @override + Future crateApiArkApiWaitForPayment( + {String? arkAddress, + String? boardingAddress, + String? boltzSwapId, + required BigInt timeoutSeconds}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_opt_String(arkAddress, serializer); + sse_encode_opt_String(boardingAddress, serializer); + sse_encode_opt_String(boltzSwapId, serializer); + sse_encode_u_64(timeoutSeconds, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 14, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_payment_received, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiArkApiWaitForPaymentConstMeta, + argValues: [arkAddress, boardingAddress, boltzSwapId, timeoutSeconds], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateApiArkApiWaitForPaymentConstMeta => + const TaskConstMeta( + debugName: "wait_for_payment", + argNames: [ + "arkAddress", + "boardingAddress", + "boltzSwapId", + "timeoutSeconds" + ], + ); + @override Future crateApiArkApiWalletExists({required String dataDir}) { return handler.executeNormal(NormalTask( @@ -494,7 +537,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(dataDir, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 14, port: port_); + funcId: 15, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -651,6 +694,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + String? dco_decode_opt_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_String(raw); + } + @protected BoltzSwap? dco_decode_opt_box_autoadd_boltz_swap(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -669,6 +718,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_box_autoadd_u_64(raw); } + @protected + PaymentReceived dco_decode_payment_received(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return PaymentReceived( + txid: dco_decode_String(arr[0]), + amountSats: dco_decode_u_64(arr[1]), + ); + } + @protected Transaction dco_decode_transaction(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -856,6 +917,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { totalSats: var_totalSats); } + @protected + String? sse_decode_opt_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_String(deserializer)); + } else { + return null; + } + } + @protected BoltzSwap? sse_decode_opt_box_autoadd_boltz_swap( SseDeserializer deserializer) { @@ -890,6 +962,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + PaymentReceived sse_decode_payment_received(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_txid = sse_decode_String(deserializer); + var var_amountSats = sse_decode_u_64(deserializer); + return PaymentReceived(txid: var_txid, amountSats: var_amountSats); + } + @protected Transaction sse_decode_transaction(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1077,6 +1157,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_64(self.totalSats, serializer); } + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_String(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_boltz_swap( BoltzSwap? self, SseSerializer serializer) { @@ -1109,6 +1199,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_payment_received( + PaymentReceived self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.txid, serializer); + sse_encode_u_64(self.amountSats, serializer); + } + @protected void sse_encode_transaction(Transaction self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart index fd8549d..3717457 100644 --- a/lib/src/rust/frb_generated.io.dart +++ b/lib/src/rust/frb_generated.io.dart @@ -68,6 +68,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected OffchainBalance dco_decode_offchain_balance(dynamic raw); + @protected + String? dco_decode_opt_String(dynamic raw); + @protected BoltzSwap? dco_decode_opt_box_autoadd_boltz_swap(dynamic raw); @@ -77,6 +80,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw); + @protected + PaymentReceived dco_decode_payment_received(dynamic raw); + @protected Transaction dco_decode_transaction(dynamic raw); @@ -138,6 +144,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected OffchainBalance sse_decode_offchain_balance(SseDeserializer deserializer); + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + @protected BoltzSwap? sse_decode_opt_box_autoadd_boltz_swap( SseDeserializer deserializer); @@ -148,6 +157,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer); + @protected + PaymentReceived sse_decode_payment_received(SseDeserializer deserializer); + @protected Transaction sse_decode_transaction(SseDeserializer deserializer); @@ -218,6 +230,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_offchain_balance( OffchainBalance self, SseSerializer serializer); + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_boltz_swap( BoltzSwap? self, SseSerializer serializer); @@ -229,6 +244,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer); + @protected + void sse_encode_payment_received( + PaymentReceived self, SseSerializer serializer); + @protected void sse_encode_transaction(Transaction self, SseSerializer serializer); diff --git a/lib/src/rust/frb_generated.web.dart b/lib/src/rust/frb_generated.web.dart index 6bc592b..f7c185b 100644 --- a/lib/src/rust/frb_generated.web.dart +++ b/lib/src/rust/frb_generated.web.dart @@ -70,6 +70,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected OffchainBalance dco_decode_offchain_balance(dynamic raw); + @protected + String? dco_decode_opt_String(dynamic raw); + @protected BoltzSwap? dco_decode_opt_box_autoadd_boltz_swap(dynamic raw); @@ -79,6 +82,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw); + @protected + PaymentReceived dco_decode_payment_received(dynamic raw); + @protected Transaction dco_decode_transaction(dynamic raw); @@ -140,6 +146,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected OffchainBalance sse_decode_offchain_balance(SseDeserializer deserializer); + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + @protected BoltzSwap? sse_decode_opt_box_autoadd_boltz_swap( SseDeserializer deserializer); @@ -150,6 +159,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer); + @protected + PaymentReceived sse_decode_payment_received(SseDeserializer deserializer); + @protected Transaction sse_decode_transaction(SseDeserializer deserializer); @@ -220,6 +232,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_offchain_balance( OffchainBalance self, SseSerializer serializer); + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_boltz_swap( BoltzSwap? self, SseSerializer serializer); @@ -231,6 +246,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer); + @protected + void sse_encode_payment_received( + PaymentReceived self, SseSerializer serializer); + @protected void sse_encode_transaction(Transaction self, SseSerializer serializer); diff --git a/lib/src/ui/screens/receive_screen.dart b/lib/src/ui/screens/receive_screen.dart index 51e2f0d..e85cb7a 100644 --- a/lib/src/ui/screens/receive_screen.dart +++ b/lib/src/ui/screens/receive_screen.dart @@ -29,6 +29,7 @@ class ReceiveScreenState extends State { String _btcAddress = ""; String _arkAddress = ""; String _lightningInvoice = ""; + String? _boltzSwapId; bool _showCopyMenu = false; @@ -50,6 +51,11 @@ class ReceiveScreenState extends State { 'Lightning': null, }; + // Payment monitoring state + bool _waitingForPayment = false; + PaymentReceived? _paymentReceived; + int _monitoringSessionId = 0; // Track current monitoring session + @override void initState() { super.initState(); @@ -73,7 +79,14 @@ class ReceiveScreenState extends State { _arkAddress = addresses.offchain; _btcAddress = addresses.boarding; _lightningInvoice = addresses.lightning?.invoice ?? ""; + _boltzSwapId = addresses.lightning?.swapId; + _paymentReceived = null; // Reset payment state + _monitoringSessionId++; // Increment session to invalidate old monitoring + _waitingForPayment = false; // Reset flag for new monitoring }); + + // Start monitoring for payments with new session + _startPaymentMonitoring(); } catch (e) { logger.e("Error fetching addresses: $e"); setState(() { @@ -82,6 +95,113 @@ class ReceiveScreenState extends State { } finally {} } + Future _startPaymentMonitoring() async { + if (_waitingForPayment) { + logger.i("Already waiting for payment, skipping duplicate call"); + return; + } + + // Capture the current session ID to check later if this monitoring is still valid + final sessionId = _monitoringSessionId; + + setState(() { + _waitingForPayment = true; + }); + + try { + logger.i("Started waiting for payment (session $sessionId)..."); + logger.i("Ark address: $_arkAddress"); + logger.i("Boarding address: $_btcAddress"); + logger.i("Boltz swap ID: $_boltzSwapId"); + + // Wait for payment with 5 minute timeout + final payment = await waitForPayment( + arkAddress: _arkAddress.isNotEmpty ? _arkAddress : null, + boardingAddress: _btcAddress.isNotEmpty ? _btcAddress : null, + boltzSwapId: _boltzSwapId, + timeoutSeconds: BigInt.from(300), // 5 minutes + ); + + // Check if this monitoring session is still current + if (!mounted || _monitoringSessionId != sessionId) { + logger.i("Monitoring session $sessionId is outdated, ignoring result"); + return; + } + + setState(() { + _paymentReceived = payment; + _waitingForPayment = false; + }); + + logger.i("Payment received! TXID: ${payment.txid}, Amount: ${payment.amountSats} sats"); + + // Show success dialog + _showPaymentReceivedDialog(payment); + } catch (e) { + logger.e("Error waiting for payment (session $sessionId): $e"); + + // Check if this monitoring session is still current + if (!mounted || _monitoringSessionId != sessionId) { + logger.i("Monitoring session $sessionId is outdated, ignoring error"); + return; + } + + setState(() { + _waitingForPayment = false; + }); + + // Don't show error if it's just a timeout - that's expected + if (!e.toString().contains('timeout') && !e.toString().contains('Timeout')) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Payment monitoring error: ${e.toString()}')), + ); + } + } + } + + void _showPaymentReceivedDialog(PaymentReceived payment) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.grey[850], + title: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.amber, size: 32), + const SizedBox(width: 12), + const Text('Payment Received!', style: TextStyle(color: Colors.white)), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Amount: ${payment.amountSats} sats', + style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'TXID: ${payment.txid}', + style: TextStyle(color: Colors.grey[400], fontSize: 12), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); // Close dialog + Navigator.of(context).pop(); // Return to previous screen + }, + child: const Text('OK', style: TextStyle(color: Colors.amber)), + ), + ], + ); + }, + ); + } + @override void dispose() { // Cancel any active timers @@ -317,6 +437,80 @@ class ReceiveScreenState extends State { const SizedBox(height: 24), + // Payment monitoring status + if (_waitingForPayment) + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.amber.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.amber.withOpacity(0.3)), + ), + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.amber), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Monitoring for incoming payment...', + style: TextStyle( + color: Colors.amber[300], + fontSize: 14, + ), + ), + ), + ], + ), + ), + + // Payment received status + if (_paymentReceived != null) + Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.withOpacity(0.3)), + ), + child: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.green, size: 24), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Payment Received!', + style: TextStyle( + color: Colors.green, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + Text( + '${_paymentReceived!.amountSats} sats', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + // QR Code RepaintBoundary( key: _qrKey, diff --git a/pubspec.lock b/pubspec.lock index 9c447ab..dd47ecb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -379,10 +379,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -839,10 +839,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: @@ -879,10 +879,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" win32: dependency: transitive description: diff --git a/rust/Cargo.lock b/rust/Cargo.lock index f3ffca1..9a509e8 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -100,7 +100,6 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "ark-bdk-wallet" version = "0.7.0" -source = "git+https://github.com/ArkLabsHQ/ark-rs.git?rev=f64b45#f64b455123e310e1c3d40ac91c5e193104331827" dependencies = [ "anyhow", "ark-client", @@ -119,7 +118,6 @@ dependencies = [ [[package]] name = "ark-client" version = "0.7.0" -source = "git+https://github.com/ArkLabsHQ/ark-rs.git?rev=f64b45#f64b455123e310e1c3d40ac91c5e193104331827" dependencies = [ "ark-core", "ark-grpc", @@ -156,7 +154,6 @@ dependencies = [ [[package]] name = "ark-core" version = "0.7.0" -source = "git+https://github.com/ArkLabsHQ/ark-rs.git?rev=f64b45#f64b455123e310e1c3d40ac91c5e193104331827" dependencies = [ "ark-secp256k1", "bech32", @@ -173,7 +170,6 @@ dependencies = [ [[package]] name = "ark-grpc" version = "0.7.0" -source = "git+https://github.com/ArkLabsHQ/ark-rs.git?rev=f64b45#f64b455123e310e1c3d40ac91c5e193104331827" dependencies = [ "ark-core", "async-stream", @@ -191,7 +187,6 @@ dependencies = [ [[package]] name = "ark-secp256k1" version = "0.31.0" -source = "git+https://github.com/ArkLabsHQ/ark-rs.git?rev=f64b45#f64b455123e310e1c3d40ac91c5e193104331827" dependencies = [ "bitcoin_hashes 0.14.0", "rand 0.9.1", @@ -2809,6 +2804,7 @@ dependencies = [ "rustls", "serde_json", "state", + "tokio", "tracing", "tracing-log", "tracing-subscriber", @@ -2986,7 +2982,6 @@ dependencies = [ [[package]] name = "secp256k1-sys" version = "0.11.0" -source = "git+https://github.com/ArkLabsHQ/ark-rs.git?rev=f64b45#f64b455123e310e1c3d40ac91c5e193104331827" dependencies = [ "cc", ] @@ -3137,6 +3132,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -3658,7 +3662,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2 0.5.9", "tokio-macros", "windows-sys 0.52.0", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index b29aa4f..27f8568 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -8,9 +8,12 @@ crate-type = ["cdylib", "staticlib"] [dependencies] anyhow = "1" -ark-bdk-wallet = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "f64b45" } -ark-core = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "f64b45" } -ark-client = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "f64b45", default-features = false, features = ["tls-webpki-roots"] } +#ark-bdk-wallet = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a" } +#ark-core = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a" } +#ark-client = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a", default-features = false, features = ["tls-webpki-roots"] }ark-bdk-wallet = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a" } +ark-bdk-wallet = { path = "/Users/bonomat/src/github/arkade/rust-sdk/ark-bdk-wallet" } +ark-core = { path = "/Users/bonomat/src/github/arkade/rust-sdk/ark-core" } +ark-client = { path = "/Users/bonomat/src/github/arkade/rust-sdk/ark-client", default-features = false, features = ["tls-webpki-roots"] } async-trait = "0.1" bitcoin = { version = "0.32.4", features = ["rand"] } esplora-client = { version = "0.11.0", features = ["async-https-native"] } @@ -23,6 +26,7 @@ parking_lot = { version = "0.12.1" } rand = "0.8.5" serde_json = "1.0.145" state = "0.6.0" +tokio = { version = "1", features = ["full"] } tracing = "0.1.37" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "time", "json"] } diff --git a/rust/src/api/ark_api.rs b/rust/src/api/ark_api.rs index 009e9c8..b5f0fa9 100644 --- a/rust/src/api/ark_api.rs +++ b/rust/src/api/ark_api.rs @@ -201,3 +201,41 @@ pub async fn information() -> Result { network: info.network.to_string(), }) } + +pub struct PaymentReceived { + pub txid: String, + pub amount_sats: u64, +} + +pub async fn wait_for_payment( + ark_address: Option, + boarding_address: Option, + boltz_swap_id: Option, + timeout_seconds: u64, +) -> Result { + use ark_core::ArkAddress; + use bitcoin::Address; + use std::str::FromStr; + + let ark_addr = ark_address + .map(|s| ArkAddress::decode(&s)) + .transpose()?; + + let boarding_addr = boarding_address + .map(|s| Address::from_str(&s)) + .transpose()? + .map(|a| a.assume_checked()); + + let payment = crate::ark::client::wait_for_payment( + ark_addr, + boarding_addr, + boltz_swap_id, + timeout_seconds, + ) + .await?; + + Ok(PaymentReceived { + txid: payment.txid.to_string(), + amount_sats: payment.amount.to_sat(), + }) +} diff --git a/rust/src/ark/client.rs b/rust/src/ark/client.rs index 20c987f..1e84573 100644 --- a/rust/src/ark/client.rs +++ b/rust/src/ark/client.rs @@ -1,16 +1,21 @@ use crate::ark::address_helper::{decode_bip21, is_ark_address, is_bip21, is_btc_address}; +use crate::ark::esplora::EsploraClient; +use crate::ark::storage::InMemoryDb; use crate::state::ARK_CLIENT; use anyhow::Result; use anyhow::{anyhow, bail}; -use ark_client::OffChainBalance; +use ark_bdk_wallet::Wallet; +use ark_client::{Client, OffChainBalance, SqliteSwapStorage}; use ark_core::history::Transaction; -use ark_core::server::Info; +use ark_core::server::{Info, SubscriptionResponse}; use ark_core::ArkAddress; use bitcoin::{Address, Amount, Txid}; +use futures::StreamExt; use rand::rngs::StdRng; use rand::SeedableRng; use std::str::FromStr; use std::sync::Arc; +use std::time::Duration; pub struct Balance { pub offchain: OffChainBalance, @@ -56,6 +61,11 @@ pub struct Addresses { pub boltz_swap: Option, } +pub struct PaymentReceived { + pub txid: Txid, + pub amount: Amount, +} + pub async fn address(amount: Option) -> Result { let maybe_client = ARK_CLIENT.try_get(); @@ -79,7 +89,7 @@ pub async fn address(amount: Option) -> Result { let reverse_swap_result = match amount { None => None, - Some(amount) => Some(client.get_ln_invoice(amount).await?), + Some(amount) => Some(client.get_ln_invoice(amount, Some(300)).await?), }; Ok(Addresses { @@ -199,6 +209,148 @@ pub async fn settle() -> Result<()> { Ok(()) } +pub(crate) async fn wait_for_payment( + ark_address: Option, + _boarding_address: Option
, + boltz_swap_id: Option, + timeout_seconds: u64, +) -> Result { + let maybe_client = ARK_CLIENT.try_get(); + match maybe_client { + None => { + bail!("Ark client not initialized") + } + Some(client) => { + let client = { + let guard = client.read(); + Arc::clone(&*guard) + }; + + let timeout_duration = Duration::from_secs(timeout_seconds); + + // Race between ark address subscription, lightning invoice, and timeout + tokio::select! { + // Monitor ark_address subscription if provided + result = async { + if let Some(address) = ark_address { + monitor_ark_address(&client, address).await + } else { + // If no ark address, wait forever (will be cancelled by other branches) + futures::future::pending().await + } + } => result, + + // Monitor lightning invoice payment if provided + result = async { + if let Some(swap_id) = boltz_swap_id { + monitor_lightning_payment(&client, swap_id).await + } else { + // If no swap id, wait forever (will be cancelled by other branches) + futures::future::pending().await + } + } => result, + + // Timeout + _ = tokio::time::sleep(timeout_duration) => { + bail!("Payment waiting timed out after {} seconds", timeout_seconds) + } + } + } + } +} + +async fn monitor_ark_address( + client: &Arc, SqliteSwapStorage>>, + address: ArkAddress, +) -> Result { + tracing::info!("Subscribing to ark address: {}", address.encode()); + + // Subscribe to the address to get notifications + let subscription_id = client + .subscribe_to_scripts(vec![address], None) + .await + .map_err(|e| anyhow!("Failed to subscribe to address: {e}"))?; + + tracing::info!("Subscription ID: {subscription_id}"); + + // Get the subscription stream + let mut subscription_stream = client + .get_subscription(subscription_id) + .await + .map_err(|e| anyhow!("Failed to get subscription stream: {e}"))?; + + tracing::info!("Listening for ark address notifications..."); + + // Process subscription responses as they come in + while let Some(result) = subscription_stream.next().await { + match result { + Ok(SubscriptionResponse::Event(e)) => { + if let Some(psbt) = e.tx { + let tx = &psbt.unsigned_tx; + let txid = tx.compute_txid(); + + // Find the output that matches our address + let output = tx.output.iter().find_map(|out| { + if out.script_pubkey == address.to_p2tr_script_pubkey() { + Some(out.clone()) + } else { + None + } + }); + + if let Some(output) = output { + tracing::info!("Payment received on ark address!"); + tracing::info!(" TXID: {}", txid); + tracing::info!(" Amount: {:?}", output.value); + + return Ok(PaymentReceived { + txid, + amount: output.value, + }); + } else { + tracing::warn!( + "Received subscription response did not include our address" + ); + } + } else { + tracing::warn!("No tx found in subscription event"); + } + } + Ok(SubscriptionResponse::Heartbeat) => { + // Ignore heartbeats + } + Err(e) => { + bail!("Error receiving subscription response: {e}"); + } + } + } + + bail!("Subscription stream ended unexpectedly") +} + +async fn monitor_lightning_payment( + client: &Arc, SqliteSwapStorage>>, + swap_id: String, +) -> Result { + tracing::info!("Waiting for lightning invoice payment: {}", swap_id); + + client + .wait_for_vhtlc(swap_id.as_str()) + .await + .map_err(|e| anyhow!("Failed waiting for invoice payment: {e}"))?; + + tracing::info!("Lightning invoice paid!"); + + // TODO: Get actual txid and amount from the payment + // For now, return placeholder values - this needs to be updated when the API provides this info + Ok(PaymentReceived { + // TODO: this is of course not a valid txid + txid: Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000") + .unwrap(), + amount: Amount::ZERO, + }) +} + pub(crate) fn info() -> Result { let maybe_client = ARK_CLIENT.try_get(); diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 98570d4..9f2bc79 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 2080225858; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -95231310; // Section: executor @@ -546,6 +546,51 @@ fn wire__crate__api__ark_api__tx_history_impl( }, ) } +fn wire__crate__api__ark_api__wait_for_payment_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "wait_for_payment", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_ark_address = >::sse_decode(&mut deserializer); + let api_boarding_address = >::sse_decode(&mut deserializer); + let api_boltz_swap_id = >::sse_decode(&mut deserializer); + let api_timeout_seconds = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::ark_api::wait_for_payment( + api_ark_address, + api_boarding_address, + api_boltz_swap_id, + api_timeout_seconds, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__ark_api__wallet_exists_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -737,6 +782,17 @@ impl SseDecode for crate::api::ark_api::OffchainBalance { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -770,6 +826,18 @@ impl SseDecode for Option { } } +impl SseDecode for crate::api::ark_api::PaymentReceived { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_txid = ::sse_decode(deserializer); + let mut var_amountSats = ::sse_decode(deserializer); + return crate::api::ark_api::PaymentReceived { + txid: var_txid, + amount_sats: var_amountSats, + }; + } +} + impl SseDecode for crate::api::ark_api::Transaction { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -864,7 +932,8 @@ fn pde_ffi_dispatcher_primary_impl( 11 => wire__crate__api__ark_api__settle_impl(port, ptr, rust_vec_len, data_len), 12 => wire__crate__api__ark_api__setup_new_wallet_impl(port, ptr, rust_vec_len, data_len), 13 => wire__crate__api__ark_api__tx_history_impl(port, ptr, rust_vec_len, data_len), - 14 => wire__crate__api__ark_api__wallet_exists_impl(port, ptr, rust_vec_len, data_len), + 14 => wire__crate__api__ark_api__wait_for_payment_impl(port, ptr, rust_vec_len, data_len), + 15 => wire__crate__api__ark_api__wallet_exists_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -1002,6 +1071,27 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::ark_api::PaymentReceived { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.txid.into_into_dart().into_dart(), + self.amount_sats.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::ark_api::PaymentReceived +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::ark_api::PaymentReceived +{ + fn into_into_dart(self) -> crate::api::ark_api::PaymentReceived { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::ark_api::Transaction { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self { @@ -1171,6 +1261,16 @@ impl SseEncode for crate::api::ark_api::OffchainBalance { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1201,6 +1301,14 @@ impl SseEncode for Option { } } +impl SseEncode for crate::api::ark_api::PaymentReceived { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.txid, serializer); + ::sse_encode(self.amount_sats, serializer); + } +} + impl SseEncode for crate::api::ark_api::Transaction { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { From 8b65f8e4ec1d25459840a6677be7cc2e0e2743a3 Mon Sep 17 00:00:00 2001 From: bonomat Date: Sun, 26 Oct 2025 00:07:57 +0400 Subject: [PATCH 3/5] chore: split payment and amount screen --- lib/src/ui/screens/amount_input_screen.dart | 200 ++++++++++++++++++++ lib/src/ui/screens/dashboard_screen.dart | 15 +- lib/src/ui/screens/receive_screen.dart | 111 +++++------ 3 files changed, 253 insertions(+), 73 deletions(-) create mode 100644 lib/src/ui/screens/amount_input_screen.dart diff --git a/lib/src/ui/screens/amount_input_screen.dart b/lib/src/ui/screens/amount_input_screen.dart new file mode 100644 index 0000000..c7eaa19 --- /dev/null +++ b/lib/src/ui/screens/amount_input_screen.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:ark_flutter/src/ui/screens/receive_screen.dart'; + +class AmountInputScreen extends StatefulWidget { + final String aspId; + + const AmountInputScreen({ + super.key, + required this.aspId, + }); + + @override + AmountInputScreenState createState() => AmountInputScreenState(); +} + +class AmountInputScreenState extends State { + String _amount = ''; + + void _onNumberPressed(String number) { + setState(() { + _amount += number; + }); + } + + void _onDeletePressed() { + if (_amount.isNotEmpty) { + setState(() { + _amount = _amount.substring(0, _amount.length - 1); + }); + } + } + + void _onClearPressed() { + setState(() { + _amount = ''; + }); + } + + void _onContinue() { + final amount = _amount.isEmpty ? 0 : int.tryParse(_amount) ?? 0; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ReceiveScreen( + aspId: widget.aspId, + amount: amount, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF121212), + appBar: AppBar( + title: const Text( + 'Enter Amount', + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: Colors.grey[800], + height: 1.0, + ), + ), + ), + body: Column( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Amount display + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: Column( + children: [ + Text( + 'Amount (sats)', + style: TextStyle( + color: Colors.grey[400], + fontSize: 16, + ), + ), + const SizedBox(height: 16), + Text( + _amount.isEmpty ? '0' : _amount, + style: const TextStyle( + color: Colors.white, + fontSize: 48, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + + const SizedBox(height: 32), + + // Number pad + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + children: [ + _buildNumberRow(['1', '2', '3']), + const SizedBox(height: 12), + _buildNumberRow(['4', '5', '6']), + const SizedBox(height: 12), + _buildNumberRow(['7', '8', '9']), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildNumberButton('C', onPressed: _onClearPressed), + _buildNumberButton('0'), + _buildNumberButton('⌫', onPressed: _onDeletePressed), + ], + ), + ], + ), + ), + ], + ), + ), + + // Bottom buttons + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _onContinue, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.amber[500], + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: Text( + _amount.isEmpty ? 'SKIP (ANY AMOUNT)' : 'CONTINUE', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildNumberRow(List numbers) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: numbers.map((number) => _buildNumberButton(number)).toList(), + ); + } + + Widget _buildNumberButton(String number, {VoidCallback? onPressed}) { + return SizedBox( + width: 80, + height: 80, + child: ElevatedButton( + onPressed: onPressed ?? () => _onNumberPressed(number), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[850], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + elevation: 0, + ), + child: Text( + number, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } +} diff --git a/lib/src/ui/screens/dashboard_screen.dart b/lib/src/ui/screens/dashboard_screen.dart index a8cc1d8..b8c3c80 100644 --- a/lib/src/ui/screens/dashboard_screen.dart +++ b/lib/src/ui/screens/dashboard_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:ark_flutter/src/logger/logger.dart'; import 'package:ark_flutter/src/ui/screens/settings_screen.dart'; import 'package:ark_flutter/src/ui/screens/send_screen.dart'; -import 'package:ark_flutter/src/ui/screens/receive_screen.dart'; +import 'package:ark_flutter/src/ui/screens/amount_input_screen.dart'; import 'package:ark_flutter/src/rust/api/ark_api.dart'; enum BalanceType { pending, confirmed, total } @@ -168,17 +168,22 @@ class DashboardScreenState extends State { ); } - void _handleReceive() { - // Navigate to receive screen + Future _handleReceive() async { + // Navigate to amount input screen logger.i("Receive button pressed"); - Navigator.push( + await Navigator.push( context, MaterialPageRoute( - builder: (context) => ReceiveScreen( + builder: (context) => AmountInputScreen( aspId: widget.aspId, ), ), ); + + // Refresh wallet data when returning from receive flow + // This will update the transaction history if a payment was received + logger.i("Returned from receive flow, refreshing wallet data"); + _fetchWalletData(); } // Helper methods for the balance display diff --git a/lib/src/ui/screens/receive_screen.dart b/lib/src/ui/screens/receive_screen.dart index e85cb7a..3cc849d 100644 --- a/lib/src/ui/screens/receive_screen.dart +++ b/lib/src/ui/screens/receive_screen.dart @@ -12,10 +12,12 @@ import 'package:path_provider/path_provider.dart'; class ReceiveScreen extends StatefulWidget { final String aspId; + final int amount; // Amount in sats, 0 means any amount const ReceiveScreen({ super.key, required this.aspId, + required this.amount, }); @override @@ -33,8 +35,6 @@ class ReceiveScreenState extends State { bool _showCopyMenu = false; - final TextEditingController _amountController = TextEditingController(); - // Track which addresses have been copied (for showing checkmarks) final Map _copiedAddresses = { 'BIP21': false, @@ -54,7 +54,6 @@ class ReceiveScreenState extends State { // Payment monitoring state bool _waitingForPayment = false; PaymentReceived? _paymentReceived; - int _monitoringSessionId = 0; // Track current monitoring session @override void initState() { @@ -64,14 +63,8 @@ class ReceiveScreenState extends State { Future _fetchAddresses() async { try { - // Parse amount from controller (in sats) - BigInt? amountSats; - if (_amountController.text.isNotEmpty) { - final parsedAmount = int.tryParse(_amountController.text); - if (parsedAmount != null && parsedAmount > 0) { - amountSats = BigInt.from(parsedAmount); - } - } + // Use amount from widget (0 means any amount) + final BigInt? amountSats = widget.amount > 0 ? BigInt.from(widget.amount) : null; final addresses = await address(amount: amountSats); setState(() { @@ -80,19 +73,16 @@ class ReceiveScreenState extends State { _btcAddress = addresses.boarding; _lightningInvoice = addresses.lightning?.invoice ?? ""; _boltzSwapId = addresses.lightning?.swapId; - _paymentReceived = null; // Reset payment state - _monitoringSessionId++; // Increment session to invalidate old monitoring - _waitingForPayment = false; // Reset flag for new monitoring }); - // Start monitoring for payments with new session + // Start monitoring for payments _startPaymentMonitoring(); } catch (e) { logger.e("Error fetching addresses: $e"); setState(() { _error = e.toString(); }); - } finally {} + } } Future _startPaymentMonitoring() async { @@ -101,15 +91,12 @@ class ReceiveScreenState extends State { return; } - // Capture the current session ID to check later if this monitoring is still valid - final sessionId = _monitoringSessionId; - setState(() { _waitingForPayment = true; }); try { - logger.i("Started waiting for payment (session $sessionId)..."); + logger.i("Started waiting for payment..."); logger.i("Ark address: $_arkAddress"); logger.i("Boarding address: $_btcAddress"); logger.i("Boltz swap ID: $_boltzSwapId"); @@ -122,11 +109,7 @@ class ReceiveScreenState extends State { timeoutSeconds: BigInt.from(300), // 5 minutes ); - // Check if this monitoring session is still current - if (!mounted || _monitoringSessionId != sessionId) { - logger.i("Monitoring session $sessionId is outdated, ignoring result"); - return; - } + if (!mounted) return; setState(() { _paymentReceived = payment; @@ -138,13 +121,8 @@ class ReceiveScreenState extends State { // Show success dialog _showPaymentReceivedDialog(payment); } catch (e) { - logger.e("Error waiting for payment (session $sessionId): $e"); - - // Check if this monitoring session is still current - if (!mounted || _monitoringSessionId != sessionId) { - logger.i("Monitoring session $sessionId is outdated, ignoring error"); - return; - } + logger.e("Error waiting for payment: $e"); + if (!mounted) return; setState(() { _waitingForPayment = false; @@ -191,8 +169,11 @@ class ReceiveScreenState extends State { actions: [ TextButton( onPressed: () { - Navigator.of(context).pop(); // Close dialog - Navigator.of(context).pop(); // Return to previous screen + // Close dialog + Navigator.of(context).pop(); + + // Navigate back to dashboard (pop until we reach it) + Navigator.of(context).popUntil((route) => route.isFirst); }, child: const Text('OK', style: TextStyle(color: Colors.amber)), ), @@ -210,7 +191,6 @@ class ReceiveScreenState extends State { timer.cancel(); } }); - _amountController.dispose(); super.dispose(); } @@ -401,41 +381,36 @@ class ReceiveScreenState extends State { padding: const EdgeInsets.all(16.0), child: Column( children: [ - // Amount input field - Container( - decoration: BoxDecoration( - color: Colors.grey[850], - borderRadius: BorderRadius.circular(8.0), - ), - child: TextField( - controller: _amountController, - keyboardType: TextInputType.number, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - labelText: 'Amount (sats)', - labelStyle: TextStyle(color: Colors.grey[400]), - // TODO: This minimum amount requirement is not true and will be removed - helperText: 'Lightning invoice requires amount ≥ 500 sats', - helperStyle: TextStyle(color: Colors.grey[500], fontSize: 12), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - suffixIcon: IconButton( - icon: const Icon(Icons.refresh, color: Colors.amber), - onPressed: _fetchAddresses, - tooltip: 'Generate addresses', - ), + // Amount display (if specified) + if (widget.amount > 0) + Container( + margin: const EdgeInsets.only(bottom: 24), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Requesting: ', + style: TextStyle( + color: Colors.grey[400], + fontSize: 16, + ), + ), + Text( + '${widget.amount} sats', + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], ), - onSubmitted: (_) => _fetchAddresses(), ), - ), - - const SizedBox(height: 24), // Payment monitoring status if (_waitingForPayment) From a692eb2dd6d7bc4514d77db8b214aca098078c90 Mon Sep 17 00:00:00 2001 From: bonomat Date: Sun, 26 Oct 2025 00:10:44 +0400 Subject: [PATCH 4/5] chore: format code --- rust/Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 27f8568..8de646d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -8,12 +8,12 @@ crate-type = ["cdylib", "staticlib"] [dependencies] anyhow = "1" -#ark-bdk-wallet = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a" } -#ark-core = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a" } -#ark-client = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a", default-features = false, features = ["tls-webpki-roots"] }ark-bdk-wallet = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a" } +# ark-bdk-wallet = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a" } +# ark-core = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a" } +# ark-client = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a", default-features = false, features = ["tls-webpki-roots"] }ark-bdk-wallet = { git = "https://github.com/ArkLabsHQ/ark-rs.git", rev = "8028a" } ark-bdk-wallet = { path = "/Users/bonomat/src/github/arkade/rust-sdk/ark-bdk-wallet" } -ark-core = { path = "/Users/bonomat/src/github/arkade/rust-sdk/ark-core" } ark-client = { path = "/Users/bonomat/src/github/arkade/rust-sdk/ark-client", default-features = false, features = ["tls-webpki-roots"] } +ark-core = { path = "/Users/bonomat/src/github/arkade/rust-sdk/ark-core" } async-trait = "0.1" bitcoin = { version = "0.32.4", features = ["rand"] } esplora-client = { version = "0.11.0", features = ["async-https-native"] } @@ -24,6 +24,7 @@ nostr = { version = "0.40.0", default-features = false, features = ["std"] } openssl = { version = "0.10", features = ["vendored"] } parking_lot = { version = "0.12.1" } rand = "0.8.5" +rustls = { version = "0.23", default-features = false, features = ["ring"] } serde_json = "1.0.145" state = "0.6.0" tokio = { version = "1", features = ["full"] } @@ -31,7 +32,6 @@ tracing = "0.1.37" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "time", "json"] } url = "2.5.4" -rustls = { version = "0.23", default-features = false, features = ["ring"] } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } From bae2c0f48463f3c36cdfd179b92e96a1800513a2 Mon Sep 17 00:00:00 2001 From: bonomat Date: Sun, 26 Oct 2025 00:12:22 +0400 Subject: [PATCH 5/5] chore: format rust --- .github/workflows/ci.yml | 16 ++++++++-------- README.md | 6 +++--- justfile | 7 +++++++ rust/src/api/ark_api.rs | 4 +--- rust/src/ark/address_helper.rs | 2 +- rust/src/ark/client.rs | 4 ++-- rust/src/ark/mod.rs | 4 ++-- rust/src/ark/seed_file.rs | 2 +- rust/src/ark/storage.rs | 4 ++-- rust/src/frb_generated.rs | 6 +++--- rust/src/logger.rs | 4 ++-- rustfmt.toml | 1 + 12 files changed, 33 insertions(+), 27 deletions(-) create mode 100644 rustfmt.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c547b8b..4e61567 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,8 +43,8 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.29.2' - channel: 'stable' + flutter-version: "3.29.2" + channel: "stable" - name: Install dependencies run: flutter pub get @@ -54,7 +54,7 @@ jobs: build-android: name: Build Android APK - needs: [ rust-checks, flutter-checks ] + needs: [rust-checks, flutter-checks] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -73,8 +73,8 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.29.2' - channel: 'stable' + flutter-version: "3.29.2" + channel: "stable" - uses: kuhnroyal/flutter-fvm-config-action/setup@v3 @@ -96,7 +96,7 @@ jobs: build-ios: name: Build iOS - needs: [ rust-checks, flutter-checks ] + needs: [rust-checks, flutter-checks] runs-on: macos-latest steps: - uses: actions/checkout@v3 @@ -115,8 +115,8 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.29.2' - channel: 'stable' + flutter-version: "3.29.2" + channel: "stable" - uses: kuhnroyal/flutter-fvm-config-action/setup@v3 diff --git a/README.md b/README.md index 02fb6cf..47d753e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ as a reference implementation for building an Ark Wallet using Flutter and Rust. ``` 3. Generate Flutter-Rust bindings : -NOTE: make sure you have the correct flutter version installed as defined in .fmrc! + NOTE: make sure you have the correct flutter version installed as defined in .fmrc! ```bash just ffi-build ``` @@ -41,8 +41,8 @@ NOTE: make sure you have the correct flutter version installed as defined in .fm 5. Build for your target platform ```bash - just ios-build - just android-build +just ios-build +just android-build ``` 5. Run the app: diff --git a/justfile b/justfile index f82ed35..a2cb76e 100644 --- a/justfile +++ b/justfile @@ -31,3 +31,10 @@ run: flutter-fmt: dart format --output=write . + +## ------------------------ +## formatting +## ------------------------ + +fmt: + dprint fmt diff --git a/rust/src/api/ark_api.rs b/rust/src/api/ark_api.rs index b5f0fa9..17ad2a3 100644 --- a/rust/src/api/ark_api.rs +++ b/rust/src/api/ark_api.rs @@ -217,9 +217,7 @@ pub async fn wait_for_payment( use bitcoin::Address; use std::str::FromStr; - let ark_addr = ark_address - .map(|s| ArkAddress::decode(&s)) - .transpose()?; + let ark_addr = ark_address.map(|s| ArkAddress::decode(&s)).transpose()?; let boarding_addr = boarding_address .map(|s| Address::from_str(&s)) diff --git a/rust/src/ark/address_helper.rs b/rust/src/ark/address_helper.rs index 4e5f689..c79f245 100644 --- a/rust/src/ark/address_helper.rs +++ b/rust/src/ark/address_helper.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use ark_core::ArkAddress; use bitcoin::address::NetworkUnchecked; use bitcoin::{Address, Amount}; diff --git a/rust/src/ark/client.rs b/rust/src/ark/client.rs index 1e84573..789a8b8 100644 --- a/rust/src/ark/client.rs +++ b/rust/src/ark/client.rs @@ -6,13 +6,13 @@ use anyhow::Result; use anyhow::{anyhow, bail}; use ark_bdk_wallet::Wallet; use ark_client::{Client, OffChainBalance, SqliteSwapStorage}; +use ark_core::ArkAddress; use ark_core::history::Transaction; use ark_core::server::{Info, SubscriptionResponse}; -use ark_core::ArkAddress; use bitcoin::{Address, Amount, Txid}; use futures::StreamExt; -use rand::rngs::StdRng; use rand::SeedableRng; +use rand::rngs::StdRng; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; diff --git a/rust/src/ark/mod.rs b/rust/src/ark/mod.rs index 4b04581..3134923 100644 --- a/rust/src/ark/mod.rs +++ b/rust/src/ark/mod.rs @@ -8,11 +8,11 @@ use crate::ark::esplora::EsploraClient; use crate::ark::seed_file::{read_seed_file, reset_wallet, write_seed_file}; use crate::ark::storage::InMemoryDb; use crate::state::ARK_CLIENT; -use anyhow::{anyhow, bail, Result}; +use anyhow::{Result, anyhow, bail}; use ark_client::{OfflineClient, SqliteSwapStorage}; +use bitcoin::Network; use bitcoin::key::{Keypair, Secp256k1}; use bitcoin::secp256k1::{All, SecretKey}; -use bitcoin::Network; use nostr::Keys; use parking_lot::RwLock; use rand::RngCore; diff --git a/rust/src/ark/seed_file.rs b/rust/src/ark/seed_file.rs index 6f6a5d9..f068e82 100644 --- a/rust/src/ark/seed_file.rs +++ b/rust/src/ark/seed_file.rs @@ -1,5 +1,5 @@ -use anyhow::anyhow; use anyhow::Result; +use anyhow::anyhow; use bitcoin::secp256k1::SecretKey; use std::fs; use std::fs::File; diff --git a/rust/src/ark/storage.rs b/rust/src/ark/storage.rs index 245bd22..e330c2d 100644 --- a/rust/src/ark/storage.rs +++ b/rust/src/ark/storage.rs @@ -1,8 +1,8 @@ -use ark_client::wallet::Persistence; use ark_client::Error; +use ark_client::wallet::Persistence; use ark_core::BoardingOutput; -use bitcoin::secp256k1::SecretKey; use bitcoin::XOnlyPublicKey; +use bitcoin::secp256k1::SecretKey; use std::collections::HashMap; use std::sync::RwLock; diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 9f2bc79..1ccb3d9 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -26,7 +26,7 @@ // Section: imports use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; -use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; +use flutter_rust_bridge::for_generated::{Lifetimeable, Lockable, transform_result_dco}; use flutter_rust_bridge::{Handler, IntoIntoDart}; // Section: boilerplate @@ -1389,7 +1389,7 @@ mod io { use flutter_rust_bridge::for_generated::byteorder::{ NativeEndian, ReadBytesExt, WriteBytesExt, }; - use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::for_generated::{Lifetimeable, Lockable, transform_result_dco}; use flutter_rust_bridge::{Handler, IntoIntoDart}; // Section: boilerplate @@ -1413,7 +1413,7 @@ mod web { }; use flutter_rust_bridge::for_generated::wasm_bindgen; use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; - use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::for_generated::{Lifetimeable, Lockable, transform_result_dco}; use flutter_rust_bridge::{Handler, IntoIntoDart}; // Section: boilerplate diff --git a/rust/src/logger.rs b/rust/src/logger.rs index 375e8ad..2570c46 100644 --- a/rust/src/logger.rs +++ b/rust/src/logger.rs @@ -7,14 +7,14 @@ use std::collections::BTreeMap; use std::sync::Arc; use std::sync::Once; use tracing_log::LogTracer; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::Layer; use tracing_subscriber::filter::Directive; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::fmt::time; use tracing_subscriber::fmt::time::UtcTime; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::EnvFilter; -use tracing_subscriber::Layer; const RUST_LOG_ENV: &str = "RUST_LOG"; static INIT_LOGGER_ONCE: Once = Once::new(); diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f216078 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +edition = "2024"