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/lib/src/rust/api/ark_api.dart b/lib/src/rust/api/ark_api.dart index 2bc0736..e8c4758 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(); @@ -73,19 +76,36 @@ 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; 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 +114,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 +136,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; @@ -161,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/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..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( @@ -83,7 +83,7 @@ class RustLib extends BaseEntrypoint { } abstract class RustLibApi extends BaseApi { - Future crateApiArkApiAddress(); + Future crateApiArkApiAddress({BigInt? amount}); Future crateApiArkApiBalance(); @@ -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}); } @@ -138,10 +144,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 +157,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 @@ -486,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( @@ -493,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, @@ -532,12 +576,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 +597,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 +694,42 @@ 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 + 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 + 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 @@ -703,8 +803,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 +818,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 +917,29 @@ 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) { + // 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 +951,25 @@ 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 + 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 @@ -894,6 +1062,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 +1071,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 +1099,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 +1157,27 @@ 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) { + // 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 +1189,24 @@ 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_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 7fc2233..3717457 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,21 @@ 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); + @protected PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw); + @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); @@ -90,12 +111,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 +144,22 @@ 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); + @protected PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer); + @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); @@ -149,13 +192,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 +230,24 @@ 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); + @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_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 9c33752..f7c185b 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,21 @@ 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); + @protected PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw); + @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); @@ -92,12 +113,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 +146,22 @@ 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); + @protected PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer); + @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); @@ -151,13 +194,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 +232,24 @@ 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); + @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_payment_received( + PaymentReceived self, SseSerializer serializer); + @protected void sse_encode_transaction(Transaction self, SseSerializer serializer); 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 da11767..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 @@ -28,6 +30,8 @@ class ReceiveScreenState extends State { String _bip21Address = ""; String _btcAddress = ""; String _arkAddress = ""; + String _lightningInvoice = ""; + String? _boltzSwapId; bool _showCopyMenu = false; @@ -36,6 +40,7 @@ class ReceiveScreenState extends State { 'BIP21': false, 'BTC': false, 'Ark': false, + 'Lightning': false, }; // Timers for resetting the checkmarks @@ -43,8 +48,13 @@ class ReceiveScreenState extends State { 'BIP21': null, 'BTC': null, 'Ark': null, + 'Lightning': null, }; + // Payment monitoring state + bool _waitingForPayment = false; + PaymentReceived? _paymentReceived; + @override void initState() { super.initState(); @@ -53,18 +63,124 @@ class ReceiveScreenState extends State { Future _fetchAddresses() async { try { - final addresses = await address(); + // 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(() { _bip21Address = addresses.bip21; _arkAddress = addresses.offchain; _btcAddress = addresses.boarding; + _lightningInvoice = addresses.lightning?.invoice ?? ""; + _boltzSwapId = addresses.lightning?.swapId; }); + + // Start monitoring for payments + _startPaymentMonitoring(); } catch (e) { logger.e("Error fetching addresses: $e"); setState(() { _error = e.toString(); }); - } finally {} + } + } + + Future _startPaymentMonitoring() async { + if (_waitingForPayment) { + logger.i("Already waiting for payment, skipping duplicate call"); + return; + } + + setState(() { + _waitingForPayment = true; + }); + + try { + logger.i("Started waiting for payment..."); + 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 + ); + + if (!mounted) 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: $e"); + if (!mounted) 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: () { + // 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)), + ), + ], + ); + }, + ); } @override @@ -136,6 +252,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 +279,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 +381,111 @@ class ReceiveScreenState extends State { padding: const EdgeInsets.all(16.0), child: Column( children: [ + // 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, + ), + ), + ], + ), + ), + + // 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, @@ -405,6 +632,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/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..8de646d 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-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"] } @@ -21,13 +24,14 @@ 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"] } 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)'] } diff --git a/rust/src/api/ark_api.rs b/rust/src/api/ark_api.rs index 311f2bb..17ad2a3 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()) } @@ -172,3 +201,39 @@ 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/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 02910a7..789a8b8 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_core::history::Transaction; -use ark_core::server::Info; +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 bitcoin::{Address, Amount, Txid}; -use rand::rngs::StdRng; +use futures::StreamExt; use rand::SeedableRng; +use rand::rngs::StdRng; use std::str::FromStr; use std::sync::Arc; +use std::time::Duration; pub struct Balance { pub offchain: OffChainBalance, @@ -44,12 +49,24 @@ 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 struct PaymentReceived { + pub txid: Txid, + pub amount: Amount, } -pub fn address() -> Result { +pub async fn address(amount: Option) -> Result { let maybe_client = ARK_CLIENT.try_get(); match maybe_client { @@ -70,9 +87,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, Some(300)).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(), + }), }) } } @@ -182,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/ark/mod.rs b/rust/src/ark/mod.rs index 62b823e..3134923 100644 --- a/rust/src/ark/mod.rs +++ b/rust/src/ark/mod.rs @@ -8,14 +8,15 @@ 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 ark_client::{InMemorySwapStorage, OfflineClient}; +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; +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/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 4fcd129..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 @@ -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 @@ -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, ) } }, @@ -544,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, @@ -615,10 +662,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 +682,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 +782,28 @@ 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 { + 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 +815,29 @@ 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::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 { @@ -824,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!(), } } @@ -850,6 +959,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 +990,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 { [ @@ -939,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 { @@ -1024,6 +1177,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 +1188,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 +1261,26 @@ 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) { + ::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 +1291,24 @@ 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::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) { @@ -1188,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 @@ -1212,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/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(); 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"