diff --git a/CHANGELOG.md b/CHANGELOG.md index 850351327..8f79b766f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 0.7.0-rc.22 (Synonym Fork) +# 0.7.0-rc.24 (Synonym Fork) ## Bug Fixes diff --git a/Cargo.toml b/Cargo.toml index b399aa503..19747886f 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ exclude = ["bindings/uniffi-bindgen"] [package] name = "ldk-node" -version = "0.7.0-rc.22" +version = "0.7.0-rc.24" authors = ["Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" diff --git a/Package.swift b/Package.swift index b42c3e299..935c130ca 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.7.0-rc.22" -let checksum = "3ec3b86365ecfb925cf00c5268bf92176188f06db04a4775c76f249e2cffabfd" +let tag = "v0.7.0-rc.24" +let checksum = "2d8518370aaaea4c39be8c7128f3bc311fbeb6411680ac66d4551e51db2cf7e4" let url = "https://github.com/synonymdev/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" let package = Package( diff --git a/bindings/kotlin/ldk-node-android/gradle.properties b/bindings/kotlin/ldk-node-android/gradle.properties index d6fcc821e..6268901e5 100644 --- a/bindings/kotlin/ldk-node-android/gradle.properties +++ b/bindings/kotlin/ldk-node-android/gradle.properties @@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official -libraryVersion=0.7.0-rc.22 +libraryVersion=0.7.0-rc.24 diff --git a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/arm64-v8a/libldk_node.so b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/arm64-v8a/libldk_node.so index 8b0c8982f..9c7e5297f 100755 Binary files a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/arm64-v8a/libldk_node.so and b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/arm64-v8a/libldk_node.so differ diff --git a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/armeabi-v7a/libldk_node.so b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/armeabi-v7a/libldk_node.so index b5067839e..655789e2b 100755 Binary files a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/armeabi-v7a/libldk_node.so and b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/armeabi-v7a/libldk_node.so differ diff --git a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/x86_64/libldk_node.so b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/x86_64/libldk_node.so index 611fb7f87..de17762c8 100755 Binary files a/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/x86_64/libldk_node.so and b/bindings/kotlin/ldk-node-android/lib/src/main/jniLibs/x86_64/libldk_node.so differ diff --git a/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.android.kt b/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.android.kt index d97998ebf..a30ccbb0a 100644 --- a/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.android.kt +++ b/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.android.kt @@ -1497,6 +1497,16 @@ internal typealias UniffiVTableCallbackInterfaceVssHeaderProviderUniffiByValue = + + + + + + + + + + @@ -2192,6 +2202,19 @@ internal interface UniffiLib : Library { `ptr`: Pointer?, uniffiCallStatus: UniffiRustCallStatus, ): Unit + fun uniffi_ldk_node_fn_method_node_add_address_type_to_monitor( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `seedBytes`: RustBufferByValue, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit + fun uniffi_ldk_node_fn_method_node_add_address_type_to_monitor_with_mnemonic( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `mnemonic`: RustBufferByValue, + `passphrase`: RustBufferByValue, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit fun uniffi_ldk_node_fn_method_node_announcement_addresses( `ptr`: Pointer?, uniffiCallStatus: UniffiRustCallStatus, @@ -2334,11 +2357,29 @@ internal interface UniffiLib : Library { `paymentId`: RustBufferByValue, uniffiCallStatus: UniffiRustCallStatus, ): RustBufferByValue + fun uniffi_ldk_node_fn_method_node_remove_address_type_from_monitor( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit fun uniffi_ldk_node_fn_method_node_remove_payment( `ptr`: Pointer?, `paymentId`: RustBufferByValue, uniffiCallStatus: UniffiRustCallStatus, ): Unit + fun uniffi_ldk_node_fn_method_node_set_primary_address_type( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `seedBytes`: RustBufferByValue, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit + fun uniffi_ldk_node_fn_method_node_set_primary_address_type_with_mnemonic( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `mnemonic`: RustBufferByValue, + `passphrase`: RustBufferByValue, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit fun uniffi_ldk_node_fn_method_node_sign_message( `ptr`: Pointer?, `msg`: RustBufferByValue, @@ -3141,6 +3182,10 @@ internal interface UniffiLib : Library { ): Short fun uniffi_ldk_node_checksum_method_networkgraph_node( ): Short + fun uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor( + ): Short + fun uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor_with_mnemonic( + ): Short fun uniffi_ldk_node_checksum_method_node_announcement_addresses( ): Short fun uniffi_ldk_node_checksum_method_node_bolt11_payment( @@ -3201,8 +3246,14 @@ internal interface UniffiLib : Library { ): Short fun uniffi_ldk_node_checksum_method_node_payment( ): Short + fun uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor( + ): Short fun uniffi_ldk_node_checksum_method_node_remove_payment( ): Short + fun uniffi_ldk_node_checksum_method_node_set_primary_address_type( + ): Short + fun uniffi_ldk_node_checksum_method_node_set_primary_address_type_with_mnemonic( + ): Short fun uniffi_ldk_node_checksum_method_node_sign_message( ): Short fun uniffi_ldk_node_checksum_method_node_splice_in( @@ -3654,6 +3705,12 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_ldk_node_checksum_method_networkgraph_node() != 48925.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor() != 14706.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor_with_mnemonic() != 4517.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_ldk_node_checksum_method_node_announcement_addresses() != 61426.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -3744,9 +3801,18 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_ldk_node_checksum_method_node_payment() != 60296.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor() != 37081.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_ldk_node_checksum_method_node_remove_payment() != 47952.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_ldk_node_checksum_method_node_set_primary_address_type() != 11005.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ldk_node_checksum_method_node_set_primary_address_type_with_mnemonic() != 50783.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_ldk_node_checksum_method_node_sign_message() != 49319.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -6887,6 +6953,35 @@ open class Node: Disposable, NodeInterface { } + @Throws(NodeException::class) + override fun `addAddressTypeToMonitor`(`addressType`: AddressType, `seedBytes`: List) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_add_address_type_to_monitor( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterSequenceUByte.lower(`seedBytes`), + uniffiRustCallStatus, + ) + } + } + } + + @Throws(NodeException::class) + override fun `addAddressTypeToMonitorWithMnemonic`(`addressType`: AddressType, `mnemonic`: Mnemonic, `passphrase`: kotlin.String?) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_add_address_type_to_monitor_with_mnemonic( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterTypeMnemonic.lower(`mnemonic`), + FfiConverterOptionalString.lower(`passphrase`), + uniffiRustCallStatus, + ) + } + } + } + override fun `announcementAddresses`(): List? { return FfiConverterOptionalSequenceTypeSocketAddress.lift(callWithPointer { uniffiRustCall { uniffiRustCallStatus -> @@ -7257,6 +7352,19 @@ open class Node: Disposable, NodeInterface { }) } + @Throws(NodeException::class) + override fun `removeAddressTypeFromMonitor`(`addressType`: AddressType) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_remove_address_type_from_monitor( + it, + FfiConverterTypeAddressType.lower(`addressType`), + uniffiRustCallStatus, + ) + } + } + } + @Throws(NodeException::class) override fun `removePayment`(`paymentId`: PaymentId) { callWithPointer { @@ -7270,6 +7378,35 @@ open class Node: Disposable, NodeInterface { } } + @Throws(NodeException::class) + override fun `setPrimaryAddressType`(`addressType`: AddressType, `seedBytes`: List) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_set_primary_address_type( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterSequenceUByte.lower(`seedBytes`), + uniffiRustCallStatus, + ) + } + } + } + + @Throws(NodeException::class) + override fun `setPrimaryAddressTypeWithMnemonic`(`addressType`: AddressType, `mnemonic`: Mnemonic, `passphrase`: kotlin.String?) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_set_primary_address_type_with_mnemonic( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterTypeMnemonic.lower(`mnemonic`), + FfiConverterOptionalString.lower(`passphrase`), + uniffiRustCallStatus, + ) + } + } + } + override fun `signMessage`(`msg`: List): kotlin.String { return FfiConverterString.lift(callWithPointer { uniffiRustCall { uniffiRustCallStatus -> @@ -11319,6 +11456,10 @@ object FfiConverterTypeNodeError : FfiConverterRustBuffer { 61 -> NodeException.CoinSelectionFailed(FfiConverterString.read(buf)) 62 -> NodeException.InvalidMnemonic(FfiConverterString.read(buf)) 63 -> NodeException.BackgroundSyncNotEnabled(FfiConverterString.read(buf)) + 64 -> NodeException.AddressTypeAlreadyMonitored(FfiConverterString.read(buf)) + 65 -> NodeException.AddressTypeIsPrimary(FfiConverterString.read(buf)) + 66 -> NodeException.AddressTypeNotMonitored(FfiConverterString.read(buf)) + 67 -> NodeException.InvalidSeedBytes(FfiConverterString.read(buf)) else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } } @@ -11581,6 +11722,22 @@ object FfiConverterTypeNodeError : FfiConverterRustBuffer { buf.putInt(63) Unit } + is NodeException.AddressTypeAlreadyMonitored -> { + buf.putInt(64) + Unit + } + is NodeException.AddressTypeIsPrimary -> { + buf.putInt(65) + Unit + } + is NodeException.AddressTypeNotMonitored -> { + buf.putInt(66) + Unit + } + is NodeException.InvalidSeedBytes -> { + buf.putInt(67) + Unit + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } } diff --git a/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.common.kt b/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.common.kt index 66177cbd0..7a97af512 100644 --- a/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.common.kt +++ b/bindings/kotlin/ldk-node-android/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.common.kt @@ -414,6 +414,12 @@ interface NetworkGraphInterface { interface NodeInterface { + @Throws(NodeException::class) + fun `addAddressTypeToMonitor`(`addressType`: AddressType, `seedBytes`: List) + + @Throws(NodeException::class) + fun `addAddressTypeToMonitorWithMnemonic`(`addressType`: AddressType, `mnemonic`: Mnemonic, `passphrase`: kotlin.String?) + fun `announcementAddresses`(): List? fun `bolt11Payment`(): Bolt11Payment @@ -484,9 +490,18 @@ interface NodeInterface { fun `payment`(`paymentId`: PaymentId): PaymentDetails? + @Throws(NodeException::class) + fun `removeAddressTypeFromMonitor`(`addressType`: AddressType) + @Throws(NodeException::class) fun `removePayment`(`paymentId`: PaymentId) + @Throws(NodeException::class) + fun `setPrimaryAddressType`(`addressType`: AddressType, `seedBytes`: List) + + @Throws(NodeException::class) + fun `setPrimaryAddressTypeWithMnemonic`(`addressType`: AddressType, `mnemonic`: Mnemonic, `passphrase`: kotlin.String?) + fun `signMessage`(`msg`: List): kotlin.String @Throws(NodeException::class) @@ -1842,6 +1857,14 @@ sealed class NodeException(message: String): kotlin.Exception(message) { class BackgroundSyncNotEnabled(message: String) : NodeException(message) + class AddressTypeAlreadyMonitored(message: String) : NodeException(message) + + class AddressTypeIsPrimary(message: String) : NodeException(message) + + class AddressTypeNotMonitored(message: String) : NodeException(message) + + class InvalidSeedBytes(message: String) : NodeException(message) + } diff --git a/bindings/kotlin/ldk-node-jvm/gradle.properties b/bindings/kotlin/ldk-node-jvm/gradle.properties index 54e754965..06c0eed7b 100644 --- a/bindings/kotlin/ldk-node-jvm/gradle.properties +++ b/bindings/kotlin/ldk-node-jvm/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs=-Xmx1536m kotlin.code.style=official -libraryVersion=0.7.0-rc.22 +libraryVersion=0.7.0-rc.24 diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.common.kt b/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.common.kt index 66177cbd0..7a97af512 100644 --- a/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.common.kt +++ b/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.common.kt @@ -414,6 +414,12 @@ interface NetworkGraphInterface { interface NodeInterface { + @Throws(NodeException::class) + fun `addAddressTypeToMonitor`(`addressType`: AddressType, `seedBytes`: List) + + @Throws(NodeException::class) + fun `addAddressTypeToMonitorWithMnemonic`(`addressType`: AddressType, `mnemonic`: Mnemonic, `passphrase`: kotlin.String?) + fun `announcementAddresses`(): List? fun `bolt11Payment`(): Bolt11Payment @@ -484,9 +490,18 @@ interface NodeInterface { fun `payment`(`paymentId`: PaymentId): PaymentDetails? + @Throws(NodeException::class) + fun `removeAddressTypeFromMonitor`(`addressType`: AddressType) + @Throws(NodeException::class) fun `removePayment`(`paymentId`: PaymentId) + @Throws(NodeException::class) + fun `setPrimaryAddressType`(`addressType`: AddressType, `seedBytes`: List) + + @Throws(NodeException::class) + fun `setPrimaryAddressTypeWithMnemonic`(`addressType`: AddressType, `mnemonic`: Mnemonic, `passphrase`: kotlin.String?) + fun `signMessage`(`msg`: List): kotlin.String @Throws(NodeException::class) @@ -1842,6 +1857,14 @@ sealed class NodeException(message: String): kotlin.Exception(message) { class BackgroundSyncNotEnabled(message: String) : NodeException(message) + class AddressTypeAlreadyMonitored(message: String) : NodeException(message) + + class AddressTypeIsPrimary(message: String) : NodeException(message) + + class AddressTypeNotMonitored(message: String) : NodeException(message) + + class InvalidSeedBytes(message: String) : NodeException(message) + } diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.jvm.kt b/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.jvm.kt index 3a2430505..492fe11c0 100644 --- a/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.jvm.kt +++ b/bindings/kotlin/ldk-node-jvm/lib/src/main/kotlin/org/lightningdevkit/ldknode/ldk_node.jvm.kt @@ -1495,6 +1495,16 @@ internal typealias UniffiVTableCallbackInterfaceVssHeaderProviderUniffiByValue = + + + + + + + + + + @@ -2190,6 +2200,19 @@ internal interface UniffiLib : Library { `ptr`: Pointer?, uniffiCallStatus: UniffiRustCallStatus, ): Unit + fun uniffi_ldk_node_fn_method_node_add_address_type_to_monitor( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `seedBytes`: RustBufferByValue, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit + fun uniffi_ldk_node_fn_method_node_add_address_type_to_monitor_with_mnemonic( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `mnemonic`: RustBufferByValue, + `passphrase`: RustBufferByValue, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit fun uniffi_ldk_node_fn_method_node_announcement_addresses( `ptr`: Pointer?, uniffiCallStatus: UniffiRustCallStatus, @@ -2332,11 +2355,29 @@ internal interface UniffiLib : Library { `paymentId`: RustBufferByValue, uniffiCallStatus: UniffiRustCallStatus, ): RustBufferByValue + fun uniffi_ldk_node_fn_method_node_remove_address_type_from_monitor( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit fun uniffi_ldk_node_fn_method_node_remove_payment( `ptr`: Pointer?, `paymentId`: RustBufferByValue, uniffiCallStatus: UniffiRustCallStatus, ): Unit + fun uniffi_ldk_node_fn_method_node_set_primary_address_type( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `seedBytes`: RustBufferByValue, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit + fun uniffi_ldk_node_fn_method_node_set_primary_address_type_with_mnemonic( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `mnemonic`: RustBufferByValue, + `passphrase`: RustBufferByValue, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit fun uniffi_ldk_node_fn_method_node_sign_message( `ptr`: Pointer?, `msg`: RustBufferByValue, @@ -3139,6 +3180,10 @@ internal interface UniffiLib : Library { ): Short fun uniffi_ldk_node_checksum_method_networkgraph_node( ): Short + fun uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor( + ): Short + fun uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor_with_mnemonic( + ): Short fun uniffi_ldk_node_checksum_method_node_announcement_addresses( ): Short fun uniffi_ldk_node_checksum_method_node_bolt11_payment( @@ -3199,8 +3244,14 @@ internal interface UniffiLib : Library { ): Short fun uniffi_ldk_node_checksum_method_node_payment( ): Short + fun uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor( + ): Short fun uniffi_ldk_node_checksum_method_node_remove_payment( ): Short + fun uniffi_ldk_node_checksum_method_node_set_primary_address_type( + ): Short + fun uniffi_ldk_node_checksum_method_node_set_primary_address_type_with_mnemonic( + ): Short fun uniffi_ldk_node_checksum_method_node_sign_message( ): Short fun uniffi_ldk_node_checksum_method_node_splice_in( @@ -3652,6 +3703,12 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_ldk_node_checksum_method_networkgraph_node() != 48925.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor() != 14706.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor_with_mnemonic() != 4517.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_ldk_node_checksum_method_node_announcement_addresses() != 61426.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -3742,9 +3799,18 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_ldk_node_checksum_method_node_payment() != 60296.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor() != 37081.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_ldk_node_checksum_method_node_remove_payment() != 47952.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_ldk_node_checksum_method_node_set_primary_address_type() != 11005.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_ldk_node_checksum_method_node_set_primary_address_type_with_mnemonic() != 50783.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_ldk_node_checksum_method_node_sign_message() != 49319.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -6876,6 +6942,35 @@ open class Node: Disposable, NodeInterface { } + @Throws(NodeException::class) + override fun `addAddressTypeToMonitor`(`addressType`: AddressType, `seedBytes`: List) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_add_address_type_to_monitor( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterSequenceUByte.lower(`seedBytes`), + uniffiRustCallStatus, + ) + } + } + } + + @Throws(NodeException::class) + override fun `addAddressTypeToMonitorWithMnemonic`(`addressType`: AddressType, `mnemonic`: Mnemonic, `passphrase`: kotlin.String?) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_add_address_type_to_monitor_with_mnemonic( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterTypeMnemonic.lower(`mnemonic`), + FfiConverterOptionalString.lower(`passphrase`), + uniffiRustCallStatus, + ) + } + } + } + override fun `announcementAddresses`(): List? { return FfiConverterOptionalSequenceTypeSocketAddress.lift(callWithPointer { uniffiRustCall { uniffiRustCallStatus -> @@ -7246,6 +7341,19 @@ open class Node: Disposable, NodeInterface { }) } + @Throws(NodeException::class) + override fun `removeAddressTypeFromMonitor`(`addressType`: AddressType) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_remove_address_type_from_monitor( + it, + FfiConverterTypeAddressType.lower(`addressType`), + uniffiRustCallStatus, + ) + } + } + } + @Throws(NodeException::class) override fun `removePayment`(`paymentId`: PaymentId) { callWithPointer { @@ -7259,6 +7367,35 @@ open class Node: Disposable, NodeInterface { } } + @Throws(NodeException::class) + override fun `setPrimaryAddressType`(`addressType`: AddressType, `seedBytes`: List) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_set_primary_address_type( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterSequenceUByte.lower(`seedBytes`), + uniffiRustCallStatus, + ) + } + } + } + + @Throws(NodeException::class) + override fun `setPrimaryAddressTypeWithMnemonic`(`addressType`: AddressType, `mnemonic`: Mnemonic, `passphrase`: kotlin.String?) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_set_primary_address_type_with_mnemonic( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterTypeMnemonic.lower(`mnemonic`), + FfiConverterOptionalString.lower(`passphrase`), + uniffiRustCallStatus, + ) + } + } + } + override fun `signMessage`(`msg`: List): kotlin.String { return FfiConverterString.lift(callWithPointer { uniffiRustCall { uniffiRustCallStatus -> @@ -11308,6 +11445,10 @@ object FfiConverterTypeNodeError : FfiConverterRustBuffer { 61 -> NodeException.CoinSelectionFailed(FfiConverterString.read(buf)) 62 -> NodeException.InvalidMnemonic(FfiConverterString.read(buf)) 63 -> NodeException.BackgroundSyncNotEnabled(FfiConverterString.read(buf)) + 64 -> NodeException.AddressTypeAlreadyMonitored(FfiConverterString.read(buf)) + 65 -> NodeException.AddressTypeIsPrimary(FfiConverterString.read(buf)) + 66 -> NodeException.AddressTypeNotMonitored(FfiConverterString.read(buf)) + 67 -> NodeException.InvalidSeedBytes(FfiConverterString.read(buf)) else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } } @@ -11570,6 +11711,22 @@ object FfiConverterTypeNodeError : FfiConverterRustBuffer { buf.putInt(63) Unit } + is NodeException.AddressTypeAlreadyMonitored -> { + buf.putInt(64) + Unit + } + is NodeException.AddressTypeIsPrimary -> { + buf.putInt(65) + Unit + } + is NodeException.AddressTypeNotMonitored -> { + buf.putInt(66) + Unit + } + is NodeException.InvalidSeedBytes -> { + buf.putInt(67) + Unit + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } } diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/darwin-aarch64/libldk_node.dylib b/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/darwin-aarch64/libldk_node.dylib index bcd2e1a4d..da21d38f6 100644 Binary files a/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/darwin-aarch64/libldk_node.dylib and b/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/darwin-aarch64/libldk_node.dylib differ diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/darwin-x86-64/libldk_node.dylib b/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/darwin-x86-64/libldk_node.dylib index 99fe380d3..f82f5cfe8 100644 Binary files a/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/darwin-x86-64/libldk_node.dylib and b/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/darwin-x86-64/libldk_node.dylib differ diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d5cd8e144..04d56013e 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -198,6 +198,16 @@ interface Node { [Throws=NodeError] AddressTypeBalance get_balance_for_address_type(AddressType address_type); sequence list_monitored_address_types(); + [Throws=NodeError] + void add_address_type_to_monitor(AddressType address_type, sequence seed_bytes); + [Throws=NodeError] + void add_address_type_to_monitor_with_mnemonic(AddressType address_type, Mnemonic mnemonic, string? passphrase); + [Throws=NodeError] + void remove_address_type_from_monitor(AddressType address_type); + [Throws=NodeError] + void set_primary_address_type(AddressType address_type, sequence seed_bytes); + [Throws=NodeError] + void set_primary_address_type_with_mnemonic(AddressType address_type, Mnemonic mnemonic, string? passphrase); sequence list_payments(); sequence list_peers(); sequence list_channels(); @@ -410,6 +420,10 @@ enum NodeError { "CoinSelectionFailed", "InvalidMnemonic", "BackgroundSyncNotEnabled", + "AddressTypeAlreadyMonitored", + "AddressTypeIsPrimary", + "AddressTypeNotMonitored", + "InvalidSeedBytes", }; dictionary NodeStatus { diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index ea72f1e0c..a377e2b25 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ldk_node" -version = "0.7.0-rc.22" +version = "0.7.0-rc.24" authors = [ { name="Elias Rohrer", email="dev@tnull.de" }, ] diff --git a/bindings/python/src/ldk_node/ldk_node.py b/bindings/python/src/ldk_node/ldk_node.py index dd32add08..d90d1e574 100644 --- a/bindings/python/src/ldk_node/ldk_node.py +++ b/bindings/python/src/ldk_node/ldk_node.py @@ -669,6 +669,10 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_ldk_node_checksum_method_networkgraph_node() != 48925: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor() != 14706: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor_with_mnemonic() != 4517: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_ldk_node_checksum_method_node_announcement_addresses() != 61426: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_ldk_node_checksum_method_node_bolt11_payment() != 41402: @@ -729,8 +733,14 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_ldk_node_checksum_method_node_payment() != 60296: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor() != 37081: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_ldk_node_checksum_method_node_remove_payment() != 47952: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_ldk_node_checksum_method_node_set_primary_address_type() != 11005: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_ldk_node_checksum_method_node_set_primary_address_type_with_mnemonic() != 50783: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_ldk_node_checksum_method_node_sign_message() != 49319: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_ldk_node_checksum_method_node_splice_in() != 46431: @@ -1754,6 +1764,21 @@ class _UniffiVTableCallbackInterfaceVssHeaderProvider(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_ldk_node_fn_free_node.restype = None +_UniffiLib.uniffi_ldk_node_fn_method_node_add_address_type_to_monitor.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_ldk_node_fn_method_node_add_address_type_to_monitor.restype = None +_UniffiLib.uniffi_ldk_node_fn_method_node_add_address_type_to_monitor_with_mnemonic.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_ldk_node_fn_method_node_add_address_type_to_monitor_with_mnemonic.restype = None _UniffiLib.uniffi_ldk_node_fn_method_node_announcement_addresses.argtypes = ( ctypes.c_void_p, ctypes.POINTER(_UniffiRustCallStatus), @@ -1926,12 +1951,33 @@ class _UniffiVTableCallbackInterfaceVssHeaderProvider(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_ldk_node_fn_method_node_payment.restype = _UniffiRustBuffer +_UniffiLib.uniffi_ldk_node_fn_method_node_remove_address_type_from_monitor.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_ldk_node_fn_method_node_remove_address_type_from_monitor.restype = None _UniffiLib.uniffi_ldk_node_fn_method_node_remove_payment.argtypes = ( ctypes.c_void_p, _UniffiRustBuffer, ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_ldk_node_fn_method_node_remove_payment.restype = None +_UniffiLib.uniffi_ldk_node_fn_method_node_set_primary_address_type.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_ldk_node_fn_method_node_set_primary_address_type.restype = None +_UniffiLib.uniffi_ldk_node_fn_method_node_set_primary_address_type_with_mnemonic.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_ldk_node_fn_method_node_set_primary_address_type_with_mnemonic.restype = None _UniffiLib.uniffi_ldk_node_fn_method_node_sign_message.argtypes = ( ctypes.c_void_p, _UniffiRustBuffer, @@ -2973,6 +3019,12 @@ class _UniffiVTableCallbackInterfaceVssHeaderProvider(ctypes.Structure): _UniffiLib.uniffi_ldk_node_checksum_method_networkgraph_node.argtypes = ( ) _UniffiLib.uniffi_ldk_node_checksum_method_networkgraph_node.restype = ctypes.c_uint16 +_UniffiLib.uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor.argtypes = ( +) +_UniffiLib.uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor.restype = ctypes.c_uint16 +_UniffiLib.uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor_with_mnemonic.argtypes = ( +) +_UniffiLib.uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor_with_mnemonic.restype = ctypes.c_uint16 _UniffiLib.uniffi_ldk_node_checksum_method_node_announcement_addresses.argtypes = ( ) _UniffiLib.uniffi_ldk_node_checksum_method_node_announcement_addresses.restype = ctypes.c_uint16 @@ -3063,9 +3115,18 @@ class _UniffiVTableCallbackInterfaceVssHeaderProvider(ctypes.Structure): _UniffiLib.uniffi_ldk_node_checksum_method_node_payment.argtypes = ( ) _UniffiLib.uniffi_ldk_node_checksum_method_node_payment.restype = ctypes.c_uint16 +_UniffiLib.uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor.argtypes = ( +) +_UniffiLib.uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor.restype = ctypes.c_uint16 _UniffiLib.uniffi_ldk_node_checksum_method_node_remove_payment.argtypes = ( ) _UniffiLib.uniffi_ldk_node_checksum_method_node_remove_payment.restype = ctypes.c_uint16 +_UniffiLib.uniffi_ldk_node_checksum_method_node_set_primary_address_type.argtypes = ( +) +_UniffiLib.uniffi_ldk_node_checksum_method_node_set_primary_address_type.restype = ctypes.c_uint16 +_UniffiLib.uniffi_ldk_node_checksum_method_node_set_primary_address_type_with_mnemonic.argtypes = ( +) +_UniffiLib.uniffi_ldk_node_checksum_method_node_set_primary_address_type_with_mnemonic.restype = ctypes.c_uint16 _UniffiLib.uniffi_ldk_node_checksum_method_node_sign_message.argtypes = ( ) _UniffiLib.uniffi_ldk_node_checksum_method_node_sign_message.restype = ctypes.c_uint16 @@ -5519,6 +5580,10 @@ def write(cls, value: NetworkGraphProtocol, buf: _UniffiRustBuffer): class NodeProtocol(typing.Protocol): + def add_address_type_to_monitor(self, address_type: "AddressType",seed_bytes: "typing.List[int]"): + raise NotImplementedError + def add_address_type_to_monitor_with_mnemonic(self, address_type: "AddressType",mnemonic: "Mnemonic",passphrase: "typing.Optional[str]"): + raise NotImplementedError def announcement_addresses(self, ): raise NotImplementedError def bolt11_payment(self, ): @@ -5579,8 +5644,14 @@ def open_channel(self, node_id: "PublicKey",address: "SocketAddress",channel_amo raise NotImplementedError def payment(self, payment_id: "PaymentId"): raise NotImplementedError + def remove_address_type_from_monitor(self, address_type: "AddressType"): + raise NotImplementedError def remove_payment(self, payment_id: "PaymentId"): raise NotImplementedError + def set_primary_address_type(self, address_type: "AddressType",seed_bytes: "typing.List[int]"): + raise NotImplementedError + def set_primary_address_type_with_mnemonic(self, address_type: "AddressType",mnemonic: "Mnemonic",passphrase: "typing.Optional[str]"): + raise NotImplementedError def sign_message(self, msg: "typing.List[int]"): raise NotImplementedError def splice_in(self, user_channel_id: "UserChannelId",counterparty_node_id: "PublicKey",splice_amount_sats: "int"): @@ -5634,6 +5705,37 @@ def _make_instance_(cls, pointer): return inst + def add_address_type_to_monitor(self, address_type: "AddressType",seed_bytes: "typing.List[int]") -> None: + _UniffiConverterTypeAddressType.check_lower(address_type) + + _UniffiConverterSequenceUInt8.check_lower(seed_bytes) + + _uniffi_rust_call_with_error(_UniffiConverterTypeNodeError,_UniffiLib.uniffi_ldk_node_fn_method_node_add_address_type_to_monitor,self._uniffi_clone_pointer(), + _UniffiConverterTypeAddressType.lower(address_type), + _UniffiConverterSequenceUInt8.lower(seed_bytes)) + + + + + + + def add_address_type_to_monitor_with_mnemonic(self, address_type: "AddressType",mnemonic: "Mnemonic",passphrase: "typing.Optional[str]") -> None: + _UniffiConverterTypeAddressType.check_lower(address_type) + + _UniffiConverterTypeMnemonic.check_lower(mnemonic) + + _UniffiConverterOptionalString.check_lower(passphrase) + + _uniffi_rust_call_with_error(_UniffiConverterTypeNodeError,_UniffiLib.uniffi_ldk_node_fn_method_node_add_address_type_to_monitor_with_mnemonic,self._uniffi_clone_pointer(), + _UniffiConverterTypeAddressType.lower(address_type), + _UniffiConverterTypeMnemonic.lower(mnemonic), + _UniffiConverterOptionalString.lower(passphrase)) + + + + + + def announcement_addresses(self, ) -> "typing.Optional[typing.List[SocketAddress]]": return _UniffiConverterOptionalSequenceTypeSocketAddress.lift( _uniffi_rust_call(_UniffiLib.uniffi_ldk_node_fn_method_node_announcement_addresses,self._uniffi_clone_pointer(),) @@ -5978,6 +6080,17 @@ def payment(self, payment_id: "PaymentId") -> "typing.Optional[PaymentDetails]": + def remove_address_type_from_monitor(self, address_type: "AddressType") -> None: + _UniffiConverterTypeAddressType.check_lower(address_type) + + _uniffi_rust_call_with_error(_UniffiConverterTypeNodeError,_UniffiLib.uniffi_ldk_node_fn_method_node_remove_address_type_from_monitor,self._uniffi_clone_pointer(), + _UniffiConverterTypeAddressType.lower(address_type)) + + + + + + def remove_payment(self, payment_id: "PaymentId") -> None: _UniffiConverterTypePaymentId.check_lower(payment_id) @@ -5989,6 +6102,37 @@ def remove_payment(self, payment_id: "PaymentId") -> None: + def set_primary_address_type(self, address_type: "AddressType",seed_bytes: "typing.List[int]") -> None: + _UniffiConverterTypeAddressType.check_lower(address_type) + + _UniffiConverterSequenceUInt8.check_lower(seed_bytes) + + _uniffi_rust_call_with_error(_UniffiConverterTypeNodeError,_UniffiLib.uniffi_ldk_node_fn_method_node_set_primary_address_type,self._uniffi_clone_pointer(), + _UniffiConverterTypeAddressType.lower(address_type), + _UniffiConverterSequenceUInt8.lower(seed_bytes)) + + + + + + + def set_primary_address_type_with_mnemonic(self, address_type: "AddressType",mnemonic: "Mnemonic",passphrase: "typing.Optional[str]") -> None: + _UniffiConverterTypeAddressType.check_lower(address_type) + + _UniffiConverterTypeMnemonic.check_lower(mnemonic) + + _UniffiConverterOptionalString.check_lower(passphrase) + + _uniffi_rust_call_with_error(_UniffiConverterTypeNodeError,_UniffiLib.uniffi_ldk_node_fn_method_node_set_primary_address_type_with_mnemonic,self._uniffi_clone_pointer(), + _UniffiConverterTypeAddressType.lower(address_type), + _UniffiConverterTypeMnemonic.lower(mnemonic), + _UniffiConverterOptionalString.lower(passphrase)) + + + + + + def sign_message(self, msg: "typing.List[int]") -> "str": _UniffiConverterSequenceUInt8.check_lower(msg) @@ -12344,6 +12488,26 @@ class BackgroundSyncNotEnabled(_UniffiTempNodeError): def __repr__(self): return "NodeError.BackgroundSyncNotEnabled({})".format(repr(str(self))) _UniffiTempNodeError.BackgroundSyncNotEnabled = BackgroundSyncNotEnabled # type: ignore + class AddressTypeAlreadyMonitored(_UniffiTempNodeError): + + def __repr__(self): + return "NodeError.AddressTypeAlreadyMonitored({})".format(repr(str(self))) + _UniffiTempNodeError.AddressTypeAlreadyMonitored = AddressTypeAlreadyMonitored # type: ignore + class AddressTypeIsPrimary(_UniffiTempNodeError): + + def __repr__(self): + return "NodeError.AddressTypeIsPrimary({})".format(repr(str(self))) + _UniffiTempNodeError.AddressTypeIsPrimary = AddressTypeIsPrimary # type: ignore + class AddressTypeNotMonitored(_UniffiTempNodeError): + + def __repr__(self): + return "NodeError.AddressTypeNotMonitored({})".format(repr(str(self))) + _UniffiTempNodeError.AddressTypeNotMonitored = AddressTypeNotMonitored # type: ignore + class InvalidSeedBytes(_UniffiTempNodeError): + + def __repr__(self): + return "NodeError.InvalidSeedBytes({})".format(repr(str(self))) + _UniffiTempNodeError.InvalidSeedBytes = InvalidSeedBytes # type: ignore NodeError = _UniffiTempNodeError # type: ignore del _UniffiTempNodeError @@ -12605,6 +12769,22 @@ def read(buf): return NodeError.BackgroundSyncNotEnabled( _UniffiConverterString.read(buf), ) + if variant == 64: + return NodeError.AddressTypeAlreadyMonitored( + _UniffiConverterString.read(buf), + ) + if variant == 65: + return NodeError.AddressTypeIsPrimary( + _UniffiConverterString.read(buf), + ) + if variant == 66: + return NodeError.AddressTypeNotMonitored( + _UniffiConverterString.read(buf), + ) + if variant == 67: + return NodeError.InvalidSeedBytes( + _UniffiConverterString.read(buf), + ) raise InternalError("Raw enum value doesn't match any cases") @staticmethod @@ -12735,6 +12915,14 @@ def check_lower(value): return if isinstance(value, NodeError.BackgroundSyncNotEnabled): return + if isinstance(value, NodeError.AddressTypeAlreadyMonitored): + return + if isinstance(value, NodeError.AddressTypeIsPrimary): + return + if isinstance(value, NodeError.AddressTypeNotMonitored): + return + if isinstance(value, NodeError.InvalidSeedBytes): + return @staticmethod def write(value, buf): @@ -12864,6 +13052,14 @@ def write(value, buf): buf.write_i32(62) if isinstance(value, NodeError.BackgroundSyncNotEnabled): buf.write_i32(63) + if isinstance(value, NodeError.AddressTypeAlreadyMonitored): + buf.write_i32(64) + if isinstance(value, NodeError.AddressTypeIsPrimary): + buf.write_i32(65) + if isinstance(value, NodeError.AddressTypeNotMonitored): + buf.write_i32(66) + if isinstance(value, NodeError.InvalidSeedBytes): + buf.write_i32(67) diff --git a/bindings/swift/Sources/LDKNode/LDKNode.swift b/bindings/swift/Sources/LDKNode/LDKNode.swift index 1b2a1d06c..cf832e0e8 100644 --- a/bindings/swift/Sources/LDKNode/LDKNode.swift +++ b/bindings/swift/Sources/LDKNode/LDKNode.swift @@ -2475,6 +2475,10 @@ public func FfiConverterTypeNetworkGraph_lower(_ value: NetworkGraph) -> UnsafeM } public protocol NodeProtocol: AnyObject { + func addAddressTypeToMonitor(addressType: AddressType, seedBytes: [UInt8]) throws + + func addAddressTypeToMonitorWithMnemonic(addressType: AddressType, mnemonic: Mnemonic, passphrase: String?) throws + func announcementAddresses() -> [SocketAddress]? func bolt11Payment() -> Bolt11Payment @@ -2535,8 +2539,14 @@ public protocol NodeProtocol: AnyObject { func payment(paymentId: PaymentId) -> PaymentDetails? + func removeAddressTypeFromMonitor(addressType: AddressType) throws + func removePayment(paymentId: PaymentId) throws + func setPrimaryAddressType(addressType: AddressType, seedBytes: [UInt8]) throws + + func setPrimaryAddressTypeWithMnemonic(addressType: AddressType, mnemonic: Mnemonic, passphrase: String?) throws + func signMessage(msg: [UInt8]) -> String func spliceIn(userChannelId: UserChannelId, counterpartyNodeId: PublicKey, spliceAmountSats: UInt64) throws @@ -2613,6 +2623,21 @@ open class Node: try! rustCall { uniffi_ldk_node_fn_free_node(pointer, $0) } } + open func addAddressTypeToMonitor(addressType: AddressType, seedBytes: [UInt8]) throws { try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_add_address_type_to_monitor(self.uniffiClonePointer(), + FfiConverterTypeAddressType.lower(addressType), + FfiConverterSequenceUInt8.lower(seedBytes), $0) + } + } + + open func addAddressTypeToMonitorWithMnemonic(addressType: AddressType, mnemonic: Mnemonic, passphrase: String?) throws { try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_add_address_type_to_monitor_with_mnemonic(self.uniffiClonePointer(), + FfiConverterTypeAddressType.lower(addressType), + FfiConverterTypeMnemonic.lower(mnemonic), + FfiConverterOptionString.lower(passphrase), $0) + } + } + open func announcementAddresses() -> [SocketAddress]? { return try! FfiConverterOptionSequenceTypeSocketAddress.lift(try! rustCall { uniffi_ldk_node_fn_method_node_announcement_addresses(self.uniffiClonePointer(), $0) @@ -2821,12 +2846,33 @@ open class Node: }) } + open func removeAddressTypeFromMonitor(addressType: AddressType) throws { try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_remove_address_type_from_monitor(self.uniffiClonePointer(), + FfiConverterTypeAddressType.lower(addressType), $0) + } + } + open func removePayment(paymentId: PaymentId) throws { try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_node_remove_payment(self.uniffiClonePointer(), FfiConverterTypePaymentId.lower(paymentId), $0) } } + open func setPrimaryAddressType(addressType: AddressType, seedBytes: [UInt8]) throws { try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_set_primary_address_type(self.uniffiClonePointer(), + FfiConverterTypeAddressType.lower(addressType), + FfiConverterSequenceUInt8.lower(seedBytes), $0) + } + } + + open func setPrimaryAddressTypeWithMnemonic(addressType: AddressType, mnemonic: Mnemonic, passphrase: String?) throws { try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_set_primary_address_type_with_mnemonic(self.uniffiClonePointer(), + FfiConverterTypeAddressType.lower(addressType), + FfiConverterTypeMnemonic.lower(mnemonic), + FfiConverterOptionString.lower(passphrase), $0) + } + } + open func signMessage(msg: [UInt8]) -> String { return try! FfiConverterString.lift(try! rustCall { uniffi_ldk_node_fn_method_node_sign_message(self.uniffiClonePointer(), @@ -8470,6 +8516,14 @@ public enum NodeError { case InvalidMnemonic(message: String) case BackgroundSyncNotEnabled(message: String) + + case AddressTypeAlreadyMonitored(message: String) + + case AddressTypeIsPrimary(message: String) + + case AddressTypeNotMonitored(message: String) + + case InvalidSeedBytes(message: String) } #if swift(>=5.8) @@ -8733,6 +8787,22 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer { message: FfiConverterString.read(from: &buf) ) + case 64: return try .AddressTypeAlreadyMonitored( + message: FfiConverterString.read(from: &buf) + ) + + case 65: return try .AddressTypeIsPrimary( + message: FfiConverterString.read(from: &buf) + ) + + case 66: return try .AddressTypeNotMonitored( + message: FfiConverterString.read(from: &buf) + ) + + case 67: return try .InvalidSeedBytes( + message: FfiConverterString.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -8865,6 +8935,14 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer { writeInt(&buf, Int32(62)) case .BackgroundSyncNotEnabled(_ /* message is ignored*/ ): writeInt(&buf, Int32(63)) + case .AddressTypeAlreadyMonitored(_ /* message is ignored*/ ): + writeInt(&buf, Int32(64)) + case .AddressTypeIsPrimary(_ /* message is ignored*/ ): + writeInt(&buf, Int32(65)) + case .AddressTypeNotMonitored(_ /* message is ignored*/ ): + writeInt(&buf, Int32(66)) + case .InvalidSeedBytes(_ /* message is ignored*/ ): + writeInt(&buf, Int32(67)) } } } @@ -12348,6 +12426,12 @@ private var initializationResult: InitializationResult = { if uniffi_ldk_node_checksum_method_networkgraph_node() != 48925 { return InitializationResult.apiChecksumMismatch } + if uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor() != 14706 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_ldk_node_checksum_method_node_add_address_type_to_monitor_with_mnemonic() != 4517 { + return InitializationResult.apiChecksumMismatch + } if uniffi_ldk_node_checksum_method_node_announcement_addresses() != 61426 { return InitializationResult.apiChecksumMismatch } @@ -12438,9 +12522,18 @@ private var initializationResult: InitializationResult = { if uniffi_ldk_node_checksum_method_node_payment() != 60296 { return InitializationResult.apiChecksumMismatch } + if uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor() != 37081 { + return InitializationResult.apiChecksumMismatch + } if uniffi_ldk_node_checksum_method_node_remove_payment() != 47952 { return InitializationResult.apiChecksumMismatch } + if uniffi_ldk_node_checksum_method_node_set_primary_address_type() != 11005 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_ldk_node_checksum_method_node_set_primary_address_type_with_mnemonic() != 50783 { + return InitializationResult.apiChecksumMismatch + } if uniffi_ldk_node_checksum_method_node_sign_message() != 49319 { return InitializationResult.apiChecksumMismatch } diff --git a/crates/bdk-wallet-aggregate/src/lib.rs b/crates/bdk-wallet-aggregate/src/lib.rs index 4c291906c..c45d89178 100644 --- a/crates/bdk-wallet-aggregate/src/lib.rs +++ b/crates/bdk-wallet-aggregate/src/lib.rs @@ -103,6 +103,10 @@ where persisters.insert(primary_key, primary_persister); for (key, wallet, persister) in additional_wallets { + debug_assert!( + !wallets.contains_key(&key), + "duplicate wallet key in AggregateWallet::new" + ); wallets.insert(key, wallet); persisters.insert(key, persister); } @@ -167,6 +171,39 @@ where self.persisters.get_mut(&self.primary).expect("Primary persister must always exist") } + /// Add a secondary wallet. Returns `Err(WalletAlreadyExists)` if key exists. + pub fn add_wallet( + &mut self, key: K, wallet: PersistedWallet

, persister: P, + ) -> Result<(), Error> { + if self.wallets.contains_key(&key) { + return Err(Error::WalletAlreadyExists); + } + self.wallets.insert(key, wallet); + self.persisters.insert(key, persister); + Ok(()) + } + + /// Change the primary wallet. New primary must already be in the aggregate. + pub fn set_primary(&mut self, new_primary: K) -> Result<(), Error> { + if !self.wallets.contains_key(&new_primary) { + return Err(Error::WalletNotFound); + } + self.primary = new_primary; + Ok(()) + } + + /// Remove a secondary wallet. Returns `Err` if key is primary or not found. + pub fn remove_wallet(&mut self, key: K) -> Result<(), Error> { + if key == self.primary { + return Err(Error::CannotRemovePrimary); + } + if self.wallets.remove(&key).is_none() { + return Err(Error::WalletNotFound); + } + self.persisters.remove(&key).expect("persister must exist if wallet existed"); + Ok(()) + } + // ─── Balance ──────────────────────────────────────────────────────── /// Aggregate balance across all wallets. diff --git a/crates/bdk-wallet-aggregate/src/types.rs b/crates/bdk-wallet-aggregate/src/types.rs index 5565d7d41..e5453a4ab 100644 --- a/crates/bdk-wallet-aggregate/src/types.rs +++ b/crates/bdk-wallet-aggregate/src/types.rs @@ -40,6 +40,10 @@ pub enum Error { TransactionNotFound, /// Cannot replace a transaction that is already confirmed. TransactionAlreadyConfirmed, + /// A wallet for the given key already exists. + WalletAlreadyExists, + /// Cannot remove the primary wallet. + CannotRemovePrimary, } impl fmt::Display for Error { @@ -62,6 +66,8 @@ impl fmt::Display for Error { Error::TransactionAlreadyConfirmed => { write!(f, "Cannot replace an already-confirmed transaction") }, + Error::WalletAlreadyExists => write!(f, "Wallet for this key already exists"), + Error::CannotRemovePrimary => write!(f, "Cannot remove the primary wallet"), } } } diff --git a/src/builder.rs b/src/builder.rs index f9bdb3803..93ee47957 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -49,9 +49,10 @@ use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvide use crate::chain::ChainSource; use crate::config::{ - default_user_config, may_announce_channel, AddressType, AnnounceError, AsyncPaymentsRole, - BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, RuntimeSyncIntervals, - DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, + default_user_config, may_announce_channel, AddressType, AddressTypeRuntimeConfig, + AnnounceError, AsyncPaymentsRole, BitcoindRestClientConfig, Config, ElectrumSyncConfig, + EsploraSyncConfig, RuntimeSyncIntervals, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, + DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, }; use crate::connection::ConnectionManager; use crate::event::EventQueue; @@ -1220,7 +1221,7 @@ impl ArcedNodeBuilder { } } -fn get_or_create_wallet_for_address_type( +pub(crate) fn get_or_create_wallet_for_address_type( address_type: AddressType, xprv: Xpriv, network: Network, persister: &mut KVStoreWalletPersister, ) -> Result, BuildError> { @@ -1449,6 +1450,9 @@ fn build_with_store_internal( }; let bdk_wallet_loaded = wallet_load_result?; + let address_type_runtime_config = + Arc::new(RwLock::new(AddressTypeRuntimeConfig::from_config(&config))); + // Chain source setup let (chain_source, chain_tip_opt) = match chain_data_source_config { Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => { @@ -1461,6 +1465,7 @@ fn build_with_store_internal( Arc::clone(&tx_broadcaster), Arc::clone(&kv_store), Arc::clone(&config), + Arc::clone(&address_type_runtime_config), Arc::clone(&logger), Arc::clone(&node_metrics), ) @@ -1474,6 +1479,7 @@ fn build_with_store_internal( Arc::clone(&tx_broadcaster), Arc::clone(&kv_store), Arc::clone(&config), + Arc::clone(&address_type_runtime_config), Arc::clone(&logger), Arc::clone(&node_metrics), ) @@ -1530,6 +1536,7 @@ fn build_with_store_internal( Arc::clone(&tx_broadcaster), Arc::clone(&kv_store), Arc::clone(&config), + Arc::clone(&address_type_runtime_config), Arc::clone(&logger), Arc::clone(&node_metrics), ) @@ -1566,6 +1573,9 @@ fn build_with_store_internal( Arc::clone(&fee_estimator), Arc::clone(&payment_store), Arc::clone(&config), + Arc::clone(&kv_store), + Arc::clone(&address_type_runtime_config), + Arc::clone(&node_metrics), Arc::clone(&logger), )); diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 475c32243..eb26f47b0 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -27,8 +27,9 @@ use lightning_transaction_sync::ElectrumSyncClient; use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; use crate::config::{ - Config, ElectrumSyncConfig, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, - FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, + AddressTypeRuntimeConfig, Config, ElectrumSyncConfig, BDK_CLIENT_STOP_GAP, + BDK_WALLET_SYNC_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, + TX_BROADCAST_TIMEOUT_SECS, }; use crate::error::Error; use crate::fee_estimator::{ @@ -54,6 +55,7 @@ pub(super) struct ElectrumChainSource { fee_estimator: Arc, pub(super) kv_store: Arc, pub(super) config: Arc, + address_type_runtime_config: Arc>, logger: Arc, pub(super) node_metrics: Arc>, } @@ -62,7 +64,8 @@ impl ElectrumChainSource { pub(super) fn new( server_url: String, sync_config: ElectrumSyncConfig, fee_estimator: Arc, kv_store: Arc, config: Arc, - logger: Arc, node_metrics: Arc>, + address_type_runtime_config: Arc>, logger: Arc, + node_metrics: Arc>, ) -> Self { let electrum_runtime_status = RwLock::new(ElectrumRuntimeStatus::new()); let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); @@ -76,6 +79,7 @@ impl ElectrumChainSource { fee_estimator, kv_store, config, + address_type_runtime_config, logger: Arc::clone(&logger), node_metrics, } @@ -195,8 +199,10 @@ impl ElectrumChainSource { Err(e) => (Vec::new(), Some(e)), }; + let additional_types = + self.address_type_runtime_config.read().unwrap().additional_address_types(); let sync_requests = super::collect_additional_sync_requests( - &self.config, + &additional_types, &onchain_wallet, &self.node_metrics, &self.logger, diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 67691ec92..af4a2e883 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -20,8 +20,8 @@ use lightning_transaction_sync::EsploraSyncClient; use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; use crate::config::{ - Config, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, - BDK_WALLET_SYNC_TIMEOUT_SECS, DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS, + AddressTypeRuntimeConfig, Config, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, + BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, }; use crate::fee_estimator::{ @@ -42,6 +42,7 @@ pub(super) struct EsploraChainSource { fee_estimator: Arc, pub(super) kv_store: Arc, pub(super) config: Arc, + address_type_runtime_config: Arc>, logger: Arc, pub(super) node_metrics: Arc>, } @@ -50,7 +51,8 @@ impl EsploraChainSource { pub(crate) fn new( server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, fee_estimator: Arc, kv_store: Arc, config: Arc, - logger: Arc, node_metrics: Arc>, + address_type_runtime_config: Arc>, logger: Arc, + node_metrics: Arc>, ) -> Self { let mut client_builder = esplora_client::Builder::new(&server_url); client_builder = client_builder.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); @@ -74,6 +76,7 @@ impl EsploraChainSource { fee_estimator, kv_store, config, + address_type_runtime_config, logger, node_metrics, } @@ -216,8 +219,10 @@ impl EsploraChainSource { Err(e) => (Vec::new(), Some(e)), }; + let additional_types = + self.address_type_runtime_config.read().unwrap().additional_address_types(); let sync_requests = super::collect_additional_sync_requests( - &self.config, + &additional_types, &onchain_wallet, &self.node_metrics, &self.logger, diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 284ee925f..0c2c8ab8e 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -26,8 +26,8 @@ use crate::chain::bitcoind::BitcoindChainSource; use crate::chain::electrum::ElectrumChainSource; use crate::chain::esplora::EsploraChainSource; use crate::config::{ - AddressType, BackgroundSyncConfig, BitcoindRestClientConfig, Config, ElectrumSyncConfig, - EsploraSyncConfig, RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, + AddressType, AddressTypeRuntimeConfig, BackgroundSyncConfig, BitcoindRestClientConfig, Config, + ElectrumSyncConfig, EsploraSyncConfig, RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, WALLET_SYNC_INTERVAL_MINIMUM_SECS, }; use crate::event::{Event, EventQueue, SyncType, TransactionDetails}; @@ -167,12 +167,12 @@ fn get_transaction_details( } pub(super) fn collect_additional_sync_requests( - config: &Config, onchain_wallet: &Wallet, node_metrics: &Arc>, - logger: &Arc, + additional_types: &[AddressType], onchain_wallet: &Wallet, + node_metrics: &Arc>, logger: &Arc, ) -> Vec<(AddressType, FullScanRequest, SyncRequest<(KeychainKind, u32)>, bool)> { - config - .additional_address_types() - .into_iter() + additional_types + .iter() + .copied() .filter_map(|address_type| { let do_incremental = node_metrics.read().unwrap().get_wallet_sync_timestamp(address_type).is_some(); @@ -347,7 +347,8 @@ impl ChainSource { pub(crate) fn new_esplora( server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, fee_estimator: Arc, tx_broadcaster: Arc, - kv_store: Arc, config: Arc, logger: Arc, + kv_store: Arc, config: Arc, + address_type_runtime_config: Arc>, logger: Arc, node_metrics: Arc>, ) -> (Self, Option) { // Create watch channel for runtime sync config updates if background sync is enabled @@ -363,6 +364,7 @@ impl ChainSource { fee_estimator, kv_store, config, + address_type_runtime_config, Arc::clone(&logger), node_metrics, ); @@ -383,7 +385,8 @@ impl ChainSource { pub(crate) fn new_electrum( server_url: String, sync_config: ElectrumSyncConfig, fee_estimator: Arc, tx_broadcaster: Arc, - kv_store: Arc, config: Arc, logger: Arc, + kv_store: Arc, config: Arc, + address_type_runtime_config: Arc>, logger: Arc, node_metrics: Arc>, ) -> (Self, Option) { // Create watch channel for runtime sync config updates if background sync is enabled @@ -398,6 +401,7 @@ impl ChainSource { fee_estimator, kv_store, config, + address_type_runtime_config, Arc::clone(&logger), node_metrics, ); diff --git a/src/config.rs b/src/config.rs index 49020979f..df0bb5fad 100644 --- a/src/config.rs +++ b/src/config.rs @@ -176,6 +176,33 @@ impl Config { } } +/// Runtime address-type configuration, initialized from [`Config`] and updated at runtime. +#[derive(Debug, Clone)] +pub(crate) struct AddressTypeRuntimeConfig { + /// The primary address type for new addresses and change outputs. + pub(crate) primary: AddressType, + /// Additional address types to monitor for existing funds. + pub(crate) monitored: Vec, +} + +impl AddressTypeRuntimeConfig { + pub(crate) fn from_config(config: &Config) -> Self { + let monitored = config + .address_types_to_monitor + .iter() + .copied() + .filter(|&at| at != config.address_type) + .collect(); + Self { primary: config.address_type, monitored } + } + + /// Returns the additional address types to monitor, deduplicating. + pub(crate) fn additional_address_types(&self) -> Vec { + let mut seen = std::collections::HashSet::new(); + self.monitored.iter().copied().filter(|&at| seen.insert(at)).collect() + } +} + #[derive(Debug, Clone)] /// Represents the configuration of an [`Node`] instance. /// diff --git a/src/error.rs b/src/error.rs index c122cda04..7b516559b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -146,6 +146,14 @@ pub enum Error { /// This error is returned when attempting to update sync intervals but background /// syncing was disabled at build time by setting `background_sync_config` to `None`. BackgroundSyncNotEnabled, + /// The address type is already being monitored. + AddressTypeAlreadyMonitored, + /// The address type is the current primary and cannot be added as a monitored type or removed. + AddressTypeIsPrimary, + /// The address type is not currently being monitored. + AddressTypeNotMonitored, + /// The given seed bytes have an invalid length. + InvalidSeedBytes, } impl fmt::Display for Error { @@ -236,6 +244,21 @@ impl fmt::Display for Error { Self::CoinSelectionFailed => write!(f, "Coin selection failed to find suitable UTXOs."), Self::InvalidMnemonic => write!(f, "The given mnemonic is invalid."), Self::BackgroundSyncNotEnabled => write!(f, "Background syncing is not enabled."), + Self::AddressTypeAlreadyMonitored => { + write!(f, "The address type is already being monitored.") + }, + Self::AddressTypeIsPrimary => { + write!( + f, + "The address type is the current primary and cannot be added as monitored or removed." + ) + }, + Self::AddressTypeNotMonitored => { + write!(f, "The address type is not currently being monitored.") + }, + Self::InvalidSeedBytes => { + write!(f, "The given seed bytes have an invalid length.") + }, } } } diff --git a/src/lib.rs b/src/lib.rs index cd1aed572..751d02160 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1765,6 +1765,110 @@ impl Node { self.wallet.get_loaded_address_types() } + /// Adds an address type to the monitored set. + /// + /// The wallet is created from `seed_bytes` (or loaded from the kv-store if previously + /// monitored) and included in subsequent sync cycles. The seed must be the same 64-byte + /// entropy used to build this node. + /// + /// Not persisted across restarts; re-apply on each start or update [`Config`]. + #[cfg(not(feature = "uniffi"))] + pub fn add_address_type_to_monitor( + &self, address_type: config::AddressType, seed_bytes: [u8; config::WALLET_KEYS_SEED_LEN], + ) -> Result<(), Error> { + self.wallet.add_monitored_address_type(address_type, &seed_bytes) + } + + /// Adds an address type to the monitored set. + /// + /// The wallet is created from `seed_bytes` (or loaded from the kv-store if previously + /// monitored) and included in subsequent sync cycles. The seed must be the same 64-byte + /// entropy used to build this node. + /// + /// Returns [`Error::InvalidSeedBytes`] if `seed_bytes` length differs from + /// [`config::WALLET_KEYS_SEED_LEN`]. + /// + /// Not persisted across restarts; re-apply on each start or update [`Config`]. + #[cfg(feature = "uniffi")] + pub fn add_address_type_to_monitor( + &self, address_type: config::AddressType, seed_bytes: Vec, + ) -> Result<(), Error> { + let seed = validate_seed_bytes(seed_bytes)?; + self.wallet.add_monitored_address_type(address_type, &seed) + } + + /// Removes an address type from the monitored set and unloads its wallet. + /// + /// Persisted state is retained in the kv-store; re-adding the type via + /// [`Node::add_address_type_to_monitor`] recovers all funds on the next sync. + /// + /// Not persisted across restarts; re-apply on each start or update [`Config`]. + pub fn remove_address_type_from_monitor( + &self, address_type: config::AddressType, + ) -> Result<(), Error> { + self.wallet.remove_monitored_address_type(address_type) + } + + /// Changes the primary address type used for new addresses and change outputs. + /// + /// Creates the wallet from `seed_bytes` if not already loaded. The previous primary is + /// demoted to the monitored set. If the new primary has never been synced, a full scan + /// is triggered on the next sync cycle. + /// + /// Not persisted across restarts; re-apply on each start or update [`Config`]. + #[cfg(not(feature = "uniffi"))] + pub fn set_primary_address_type( + &self, address_type: config::AddressType, seed_bytes: [u8; config::WALLET_KEYS_SEED_LEN], + ) -> Result<(), Error> { + self.wallet.set_primary_address_type(address_type, &seed_bytes) + } + + /// Changes the primary address type used for new addresses and change outputs. + /// + /// Creates the wallet from `seed_bytes` if not already loaded. The previous primary is + /// demoted to the monitored set. If the new primary has never been synced, a full scan + /// is triggered on the next sync cycle. + /// + /// Returns [`Error::InvalidSeedBytes`] if `seed_bytes` length differs from + /// [`config::WALLET_KEYS_SEED_LEN`]. + /// + /// Not persisted across restarts; re-apply on each start or update [`Config`]. + #[cfg(feature = "uniffi")] + pub fn set_primary_address_type( + &self, address_type: config::AddressType, seed_bytes: Vec, + ) -> Result<(), Error> { + let seed = validate_seed_bytes(seed_bytes)?; + self.wallet.set_primary_address_type(address_type, &seed) + } + + /// Adds an address type to the monitored set, deriving the wallet from a BIP39 mnemonic. + /// + /// Convenience wrapper around [`Node::add_address_type_to_monitor`]. The `mnemonic` and + /// `passphrase` must match the values used at build time. + /// + /// Not persisted across restarts. + pub fn add_address_type_to_monitor_with_mnemonic( + &self, address_type: config::AddressType, mnemonic: bip39::Mnemonic, + passphrase: Option, + ) -> Result<(), Error> { + let seed = mnemonic.to_seed(passphrase.as_deref().unwrap_or("")); + self.wallet.add_monitored_address_type(address_type, &seed) + } + + /// Changes the primary address type, deriving the wallet from a BIP39 mnemonic. + /// + /// Convenience wrapper around [`Node::set_primary_address_type`]. The `mnemonic` and + /// `passphrase` must match the values used at build time. + /// + /// Not persisted across restarts. + pub fn set_primary_address_type_with_mnemonic( + &self, address_type: config::AddressType, mnemonic: bip39::Mnemonic, + passphrase: Option, + ) -> Result<(), Error> { + let seed = mnemonic.to_seed(passphrase.as_deref().unwrap_or("")); + self.wallet.set_primary_address_type(address_type, &seed) + } + /// Retrieves all payments that match the given predicate. /// /// For example, you could retrieve all stored outbound payments as follows: @@ -1887,6 +1991,16 @@ impl Drop for Node { } } +#[cfg(feature = "uniffi")] +fn validate_seed_bytes(seed_bytes: Vec) -> Result<[u8; config::WALLET_KEYS_SEED_LEN], Error> { + if seed_bytes.len() != config::WALLET_KEYS_SEED_LEN { + return Err(Error::InvalidSeedBytes); + } + let mut bytes = [0u8; config::WALLET_KEYS_SEED_LEN]; + bytes.copy_from_slice(&seed_bytes); + Ok(bytes) +} + /// Represents the status of the [`Node`]. #[derive(Clone, Debug, PartialEq, Eq)] pub struct NodeStatus { diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 949421e5c..396ebd438 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -9,14 +9,14 @@ use std::future::Future; use std::ops::Deref; use std::pin::Pin; use std::str::FromStr; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::{Arc, Mutex, MutexGuard, RwLock}; use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; -pub use bdk_wallet::coin_selection::CoinSelectionAlgorithm as BdkCoinSelectionAlgorithm; use bdk_wallet::event::WalletEvent; use bdk_wallet::{Balance, KeychainKind, LocalOutput, PersistedWallet, Update}; use bdk_wallet_aggregate::AggregateWallet; use bitcoin::address::NetworkUnchecked; +use bitcoin::bip32::Xpriv; use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::hashes::Hash; @@ -26,8 +26,8 @@ use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; use bitcoin::{ - Address, Amount, FeeRate, OutPoint, PubkeyHash, Script, ScriptBuf, Transaction, TxOut, Txid, - WPubkeyHash, Weight, WitnessProgram, WitnessVersion, + Address, Amount, FeeRate, Network, OutPoint, PubkeyHash, Script, ScriptBuf, Transaction, TxOut, + Txid, WPubkeyHash, Weight, WitnessProgram, WitnessVersion, }; use lightning::chain::chaininterface::BroadcasterInterface; use lightning::chain::channelmonitor::ANTI_REORG_DELAY; @@ -46,14 +46,14 @@ use lightning::util::message_signing; use lightning_invoice::RawBolt11Invoice; use persist::KVStoreWalletPersister; -use crate::config::{AddressType, Config}; +use crate::config::{AddressType, AddressTypeRuntimeConfig, Config, WALLET_KEYS_SEED_LEN}; use crate::event::{TxInput, TxOutput}; use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator}; use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use crate::payment::store::ConfirmationStatus; use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; -use crate::types::{Broadcaster, ChannelManager, PaymentStore}; -use crate::Error; +use crate::types::{Broadcaster, ChannelManager, DynStore, PaymentStore}; +use crate::{Error, NodeMetrics}; // Minimum economical output value (dust limit) const DUST_LIMIT_SATS: u64 = 546; @@ -83,10 +83,15 @@ pub(crate) mod ser; pub(crate) struct Wallet { inner: Mutex>, + // Serializes add/remove/set_primary to keep aggregate and runtime config in sync. + operation_lock: Mutex<()>, broadcaster: Arc, fee_estimator: Arc, payment_store: Arc, config: Arc, + kv_store: Arc, + address_type_runtime_config: Arc>, + node_metrics: Arc>, logger: Arc, } @@ -100,12 +105,26 @@ impl Wallet { KVStoreWalletPersister, )>, broadcaster: Arc, fee_estimator: Arc, - payment_store: Arc, config: Arc, logger: Arc, + payment_store: Arc, config: Arc, kv_store: Arc, + address_type_runtime_config: Arc>, + node_metrics: Arc>, logger: Arc, ) -> Self { let aggregate = AggregateWallet::new(wallet, wallet_persister, config.address_type, additional_wallets); let inner = Mutex::new(aggregate); - Self { inner, broadcaster, fee_estimator, payment_store, config, logger } + let operation_lock = Mutex::new(()); + Self { + inner, + operation_lock, + broadcaster, + fee_estimator, + payment_store, + config, + kv_store, + address_type_runtime_config, + node_metrics, + logger, + } } pub(crate) fn is_funding_transaction( @@ -178,6 +197,157 @@ impl Wallet { self.inner.lock().unwrap().loaded_keys() } + /// Adds an address type to the monitored set, creating its wallet if not already loaded. + pub(crate) fn add_monitored_address_type( + &self, address_type: AddressType, seed_bytes: &[u8; WALLET_KEYS_SEED_LEN], + ) -> Result<(), Error> { + let _op = self.operation_lock.lock().unwrap(); + + { + let runtime_config = self.address_type_runtime_config.read().unwrap(); + if runtime_config.primary == address_type { + return Err(Error::AddressTypeIsPrimary); + } + if runtime_config.monitored.contains(&address_type) { + return Err(Error::AddressTypeAlreadyMonitored); + } + } + + let (wallet, persister) = create_wallet_for_address_type( + seed_bytes, + self.config.network, + address_type, + self.current_best_block(), + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + + { + let mut aggregate = self.inner.lock().unwrap(); + aggregate.add_wallet(address_type, wallet, persister).map_err(|e| { + log_error!(self.logger, "Failed to add wallet for {:?}: {}", address_type, e); + Error::WalletOperationFailed + })?; + self.address_type_runtime_config.write().unwrap().monitored.push(address_type); + } + + log_info!(self.logger, "Added address type {:?} to monitor", address_type); + Ok(()) + } + + /// Removes an address type from monitoring and unloads its wallet. + /// Persisted state is retained so re-adding recovers funds on the next sync. + pub(crate) fn remove_monitored_address_type( + &self, address_type: AddressType, + ) -> Result<(), Error> { + let _op = self.operation_lock.lock().unwrap(); + + { + let runtime_config = self.address_type_runtime_config.read().unwrap(); + if runtime_config.primary == address_type { + return Err(Error::AddressTypeIsPrimary); + } + if !runtime_config.monitored.contains(&address_type) { + return Err(Error::AddressTypeNotMonitored); + } + } + + { + let mut aggregate = self.inner.lock().unwrap(); + match aggregate.remove_wallet(address_type) { + Ok(()) => {}, + Err(bdk_wallet_aggregate::Error::CannotRemovePrimary) => { + return Err(Error::AddressTypeIsPrimary); + }, + Err(bdk_wallet_aggregate::Error::WalletNotFound) => { + log_debug!( + self.logger, + "Wallet for {:?} was not in aggregate (already unloaded)", + address_type + ); + }, + Err(e) => { + log_error!(self.logger, "Failed to remove wallet {:?}: {}", address_type, e); + return Err(Error::WalletOperationFailed); + }, + } + self.address_type_runtime_config + .write() + .unwrap() + .monitored + .retain(|&at| at != address_type); + } + + log_info!(self.logger, "Removed address type {:?} from monitor", address_type); + Ok(()) + } + + /// Sets the primary address type, creating its wallet if not already loaded. + /// The previous primary is demoted to the monitored set. + pub(crate) fn set_primary_address_type( + &self, address_type: AddressType, seed_bytes: &[u8; WALLET_KEYS_SEED_LEN], + ) -> Result<(), Error> { + let _op = self.operation_lock.lock().unwrap(); + + let old_primary = self.address_type_runtime_config.read().unwrap().primary; + if address_type == old_primary { + return Ok(()); + } + + let already_loaded = self.inner.lock().unwrap().loaded_keys().contains(&address_type); + + let new_wallet = if !already_loaded { + Some(create_wallet_for_address_type( + seed_bytes, + self.config.network, + address_type, + self.current_best_block(), + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?) + } else { + None + }; + + { + let mut aggregate = self.inner.lock().unwrap(); + if let Some((wallet, persister)) = new_wallet { + aggregate.add_wallet(address_type, wallet, persister).map_err(|e| { + log_error!(self.logger, "Failed to add wallet for {:?}: {}", address_type, e); + Error::WalletOperationFailed + })?; + } + + aggregate.set_primary(address_type).map_err(|e| { + log_error!( + self.logger, + "Failed to set primary address type to {:?}: {}", + address_type, + e + ); + Error::WalletOperationFailed + })?; + + let mut runtime_config = self.address_type_runtime_config.write().unwrap(); + runtime_config.primary = address_type; + runtime_config.monitored.retain(|&at| at != address_type); + if !runtime_config.monitored.contains(&old_primary) { + runtime_config.monitored.push(old_primary); + } + } + + // Clear primary sync timestamp for never-synced types so the next cycle does a full + // scan. Additional wallets have independent per-type timestamps in node_metrics. + let needs_full_scan = + self.node_metrics.read().unwrap().get_wallet_sync_timestamp(address_type).is_none(); + if needs_full_scan { + self.node_metrics.write().unwrap().latest_onchain_wallet_sync_timestamp = None; + } + + log_info!(self.logger, "Set primary address type to {:?}", address_type); + Ok(()) + } + pub(crate) fn apply_update( &self, update: impl Into, ) -> Result, Error> { @@ -1420,6 +1590,41 @@ impl Listen for Wallet { } } +fn create_wallet_for_address_type( + seed_bytes: &[u8; WALLET_KEYS_SEED_LEN], network: Network, address_type: AddressType, + chain_tip: BestBlock, kv_store: Arc, logger: Arc, +) -> Result<(PersistedWallet, KVStoreWalletPersister), Error> { + let xprv = Xpriv::new_master(network, seed_bytes).map_err(|e| { + log_error!(logger, "Failed to derive master secret: {}", e); + Error::WalletOperationFailed + })?; + + let mut persister = + KVStoreWalletPersister::new(Arc::clone(&kv_store), Arc::clone(&logger), address_type); + + let mut wallet = crate::builder::get_or_create_wallet_for_address_type( + address_type, + xprv, + network, + &mut persister, + ) + .map_err(|e| { + log_error!(logger, "Failed to setup wallet for {:?}: {}", address_type, e); + Error::WalletOperationFailed + })?; + + let block_id = bdk_chain::BlockId { height: chain_tip.height, hash: chain_tip.block_hash }; + let mut cp = wallet.latest_checkpoint(); + cp = cp.insert(block_id); + let update = bdk_wallet::Update { chain: Some(cp), ..Default::default() }; + wallet.apply_update(update).map_err(|e| { + log_error!(logger, "Failed to apply checkpoint for {:?}: {}", address_type, e); + Error::WalletOperationFailed + })?; + + Ok((wallet, persister)) +} + impl WalletSource for Wallet { fn list_confirmed_utxos<'a>( &'a self, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 72efa9a8a..a21af838a 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -299,8 +299,11 @@ pub(crate) fn random_listening_addresses() -> Vec { for _ in 0..num_addresses { let mut rand_port = random_port(); + let mut attempts = 0u32; while used_ports.contains(&rand_port) { rand_port = random_port(); + attempts += 1; + assert!(attempts < 1000, "Failed to find a unique random port after 1000 attempts"); } used_ports.insert(rand_port); let address: SocketAddress = format!("127.0.0.1:{}", rand_port).parse().unwrap(); diff --git a/tests/multi_address_types_tests.rs b/tests/multi_address_types_tests.rs index 3e8241141..edee7c1a6 100644 --- a/tests/multi_address_types_tests.rs +++ b/tests/multi_address_types_tests.rs @@ -110,6 +110,25 @@ mod helpers { config.node_config.address_types_to_monitor = monitored; config } + + /// Read the seed bytes from the node's storage directory (created by the default entropy + /// source during build). Used to pass seed bytes to the dynamic address type APIs in tests. + pub fn read_node_seed(node: &Node) -> [u8; 64] { + let config = node.config(); + let seed_path = format!("{}/keys_seed", config.storage_dir_path); + let seed = std::fs::read(&seed_path) + .unwrap_or_else(|e| panic!("Failed to read seed file at {}: {}", seed_path, e)); + assert_eq!( + seed.len(), + 64, + "keys_seed at {} must be 64 bytes (got {})", + seed_path, + seed.len() + ); + let mut bytes = [0u8; 64]; + bytes.copy_from_slice(&seed); + bytes + } } // --------------------------------------------------------------------------- @@ -583,11 +602,9 @@ mod balance { let utxo_sum: u64 = utxos.iter().map(|u| u.value_sats).sum(); assert_eq!( - balances.spendable_onchain_balance_sats, - utxo_sum, + balances.spendable_onchain_balance_sats, utxo_sum, "listBalances spendable should equal UTXO sum (spendable={} utxo_sum={})", - balances.spendable_onchain_balance_sats, - utxo_sum + balances.spendable_onchain_balance_sats, utxo_sum ); node.stop().unwrap(); @@ -599,10 +616,8 @@ mod balance { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let config = node_config( - AddressType::NativeSegwit, - vec![AddressType::Legacy, AddressType::Taproot], - ); + let config = + node_config(AddressType::NativeSegwit, vec![AddressType::Legacy, AddressType::Taproot]); let node = setup_node(&chain_source, config, None); let native_addr = node.onchain_payment().new_address().unwrap(); @@ -614,11 +629,7 @@ mod balance { &bitcoind, &electrsd, &node, - vec![ - (native_addr, 80_000), - (legacy_addr, 70_000), - (taproot_addr, 90_000), - ], + vec![(native_addr, 80_000), (legacy_addr, 70_000), (taproot_addr, 90_000)], ) .await; @@ -631,11 +642,9 @@ mod balance { .sum(); assert_eq!( - per_type_sum, - aggregate_spendable, + per_type_sum, aggregate_spendable, "Sum of per-type spendable should equal aggregate (per_type_sum={} aggregate={})", - per_type_sum, - aggregate_spendable + per_type_sum, aggregate_spendable ); node.stop().unwrap(); @@ -675,18 +684,13 @@ mod balance { let utxos = node.onchain_payment().list_spendable_outputs().unwrap(); let balances = node.list_balances(); - assert!( - utxos.is_empty(), - "listSpendableOutputs should be empty after spend-all" - ); + assert!(utxos.is_empty(), "listSpendableOutputs should be empty after spend-all"); assert_eq!( - balances.total_onchain_balance_sats, - 0, + balances.total_onchain_balance_sats, 0, "total_onchain_balance_sats should be 0" ); assert_eq!( - balances.spendable_onchain_balance_sats, - 0, + balances.spendable_onchain_balance_sats, 0, "spendable_onchain_balance_sats should be 0" ); @@ -713,8 +717,7 @@ mod balance { let utxo_sum: u64 = utxos.iter().map(|u| u.value_sats).sum(); assert_eq!( - balances.spendable_onchain_balance_sats, - utxo_sum, + balances.spendable_onchain_balance_sats, utxo_sum, "Single-type: spendable should equal UTXO sum" ); @@ -2823,3 +2826,334 @@ mod coin_selection { node.stop().unwrap(); } } + +// --------------------------------------------------------------------------- +// Dynamic Address Types Changes +// --------------------------------------------------------------------------- +mod dynamic_address_type_changes { + use ldk_node::config::AddressType; + + use crate::common::{setup_bitcoind_and_electrsd, setup_node, wait_for_tx, TestChainSource}; + use crate::helpers::{ + confirm_and_sync, fund_and_sync, node_config, read_node_seed, test_recipient, + }; + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_add_address_type_to_monitor() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![]); + let node = setup_node(&chain_source, config, None); + + let monitored = node.list_monitored_address_types(); + assert_eq!(monitored.len(), 1); + assert!(monitored.contains(&AddressType::NativeSegwit)); + + node.add_address_type_to_monitor(AddressType::Legacy, read_node_seed(&node)).unwrap(); + + let monitored = node.list_monitored_address_types(); + assert!(monitored.contains(&AddressType::Legacy)); + assert!(monitored.contains(&AddressType::NativeSegwit)); + + let legacy_addr = node.onchain_payment().new_address_for_type(AddressType::Legacy).unwrap(); + assert!(legacy_addr.script_pubkey().is_p2pkh()); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_add_address_type_taproot() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![]); + let node = setup_node(&chain_source, config, None); + + node.add_address_type_to_monitor(AddressType::Taproot, read_node_seed(&node)).unwrap(); + let taproot_addr = + node.onchain_payment().new_address_for_type(AddressType::Taproot).unwrap(); + assert!(taproot_addr.script_pubkey().is_p2tr()); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_add_address_type_then_fund_and_sync() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![]); + let node = setup_node(&chain_source, config, None); + + node.add_address_type_to_monitor(AddressType::Legacy, read_node_seed(&node)).unwrap(); + let legacy_addr = node.onchain_payment().new_address_for_type(AddressType::Legacy).unwrap(); + + fund_and_sync(&bitcoind, &electrsd, &node, legacy_addr, 100_000).await; + + let legacy_balance = node.get_balance_for_address_type(AddressType::Legacy).unwrap(); + assert!(legacy_balance.total_sats >= 99_000); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_add_address_type_already_primary_returns_error() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![]); + let node = setup_node(&chain_source, config, None); + + let result = + node.add_address_type_to_monitor(AddressType::NativeSegwit, read_node_seed(&node)); + assert_eq!(result, Err(ldk_node::NodeError::AddressTypeIsPrimary)); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_add_address_type_nested_segwit() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![]); + let node = setup_node(&chain_source, config, None); + + node.add_address_type_to_monitor(AddressType::NestedSegwit, read_node_seed(&node)).unwrap(); + let nested_addr = + node.onchain_payment().new_address_for_type(AddressType::NestedSegwit).unwrap(); + assert!(nested_addr.script_pubkey().is_p2sh()); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_add_address_type_already_monitored_returns_error() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![AddressType::Legacy]); + let node = setup_node(&chain_source, config, None); + + let result = node.add_address_type_to_monitor(AddressType::Legacy, read_node_seed(&node)); + assert_eq!(result, Err(ldk_node::NodeError::AddressTypeAlreadyMonitored)); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_remove_address_type_from_monitor() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![AddressType::Legacy]); + let node = setup_node(&chain_source, config, None); + + assert!(node.list_monitored_address_types().contains(&AddressType::Legacy)); + + node.remove_address_type_from_monitor(AddressType::Legacy).unwrap(); + + assert!(!node.list_monitored_address_types().contains(&AddressType::Legacy)); + assert!(node.onchain_payment().new_address_for_type(AddressType::Legacy).is_err()); + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_remove_then_add_preserves_funds() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![AddressType::Legacy]); + let node = setup_node(&chain_source, config, None); + + let legacy_addr = node.onchain_payment().new_address_for_type(AddressType::Legacy).unwrap(); + fund_and_sync(&bitcoind, &electrsd, &node, legacy_addr, 50_000).await; + + let balance_before = node.get_balance_for_address_type(AddressType::Legacy).unwrap(); + assert!(balance_before.total_sats >= 49_000); + + node.remove_address_type_from_monitor(AddressType::Legacy).unwrap(); + assert!(node.get_balance_for_address_type(AddressType::Legacy).is_err()); + + node.add_address_type_to_monitor(AddressType::Legacy, read_node_seed(&node)).unwrap(); + node.sync_wallets().unwrap(); + let balance_after = node.get_balance_for_address_type(AddressType::Legacy).unwrap(); + assert!(balance_after.total_sats >= 49_000); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_remove_primary_returns_error() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![AddressType::Legacy]); + let node = setup_node(&chain_source, config, None); + + let result = node.remove_address_type_from_monitor(AddressType::NativeSegwit); + assert_eq!(result, Err(ldk_node::NodeError::AddressTypeIsPrimary)); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_remove_unmonitored_type_returns_error() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![AddressType::Legacy]); + let node = setup_node(&chain_source, config, None); + + let result = node.remove_address_type_from_monitor(AddressType::Taproot); + assert_eq!(result, Err(ldk_node::NodeError::AddressTypeNotMonitored)); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_set_primary_address_type() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![AddressType::Legacy]); + let node = setup_node(&chain_source, config, None); + + let addr_before = node.onchain_payment().new_address().unwrap(); + assert!(addr_before.script_pubkey().is_p2wpkh()); + + node.set_primary_address_type(AddressType::Legacy, read_node_seed(&node)).unwrap(); + + let addr_after = node.onchain_payment().new_address().unwrap(); + assert!(addr_after.script_pubkey().is_p2pkh()); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_set_primary_auto_loads_unloaded_type() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![]); + let node = setup_node(&chain_source, config, None); + + // Taproot not loaded; set_primary should auto-load it + node.set_primary_address_type(AddressType::Taproot, read_node_seed(&node)).unwrap(); + + let addr = node.onchain_payment().new_address().unwrap(); + assert!(addr.script_pubkey().is_p2tr()); + + // Old primary should be demoted to the monitored set + let monitored = node.list_monitored_address_types(); + assert!(monitored.contains(&AddressType::NativeSegwit)); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_set_primary_same_primary_no_op() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![AddressType::Legacy]); + let node = setup_node(&chain_source, config, None); + + // Setting to current primary should succeed (no-op) + node.set_primary_address_type(AddressType::NativeSegwit, read_node_seed(&node)).unwrap(); + + let addr = node.onchain_payment().new_address().unwrap(); + assert!(addr.script_pubkey().is_p2wpkh()); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_set_primary_round_trip_preserves_monitored() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![AddressType::Legacy]); + let node = setup_node(&chain_source, config, None); + + // NativeSegwit primary, Legacy monitored + assert!(node.list_monitored_address_types().contains(&AddressType::Legacy)); + assert!(node.list_monitored_address_types().contains(&AddressType::NativeSegwit)); + + // Swap to Legacy + node.set_primary_address_type(AddressType::Legacy, read_node_seed(&node)).unwrap(); + assert!(node.onchain_payment().new_address().unwrap().script_pubkey().is_p2pkh()); + + // Swap back to NativeSegwit + node.set_primary_address_type(AddressType::NativeSegwit, read_node_seed(&node)).unwrap(); + assert!(node.onchain_payment().new_address().unwrap().script_pubkey().is_p2wpkh()); + + // Both types should still be loaded; Legacy should be in monitored again + let monitored = node.list_monitored_address_types(); + assert!(monitored.contains(&AddressType::Legacy)); + assert!(monitored.contains(&AddressType::NativeSegwit)); + assert!(node.onchain_payment().new_address_for_type(AddressType::Legacy).is_ok()); + + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_add_then_set_primary_then_send() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![]); + let node = setup_node(&chain_source, config, None); + + // set_primary auto-loads Taproot + node.set_primary_address_type(AddressType::Taproot, read_node_seed(&node)).unwrap(); + let taproot_addr = node.onchain_payment().new_address().unwrap(); + fund_and_sync(&bitcoind, &electrsd, &node, taproot_addr, 100_000).await; + + let txid = + node.onchain_payment().send_to_address(&test_recipient(), 10_000, None, None).unwrap(); + wait_for_tx(&electrsd.client, txid).await; + confirm_and_sync(&bitcoind, &electrsd, 1, &[&node]).await; + + let balance = node.list_balances().total_onchain_balance_sats; + assert!(balance < 100_000); + + node.stop().unwrap(); + } + + /// set_primary on a never-synced type triggers a full scan, discovering pre-existing UTXOs. + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_set_primary_triggers_full_scan_for_never_synced_type() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config = node_config(AddressType::NativeSegwit, vec![]); + let node = setup_node(&chain_source, config, None); + + // Fund NativeSegwit so the node has a non-null primary sync timestamp. + let native_addr = node.onchain_payment().new_address().unwrap(); + fund_and_sync(&bitcoind, &electrsd, &node, native_addr, 50_000).await; + + // Briefly load Taproot to obtain an address, then remove it so no sync timestamp is + // recorded for it. + node.add_address_type_to_monitor(AddressType::Taproot, read_node_seed(&node)).unwrap(); + let taproot_addr = + node.onchain_payment().new_address_for_type(AddressType::Taproot).unwrap(); + node.remove_address_type_from_monitor(AddressType::Taproot).unwrap(); + + // Fund Taproot while unmonitored; balance is not visible yet. + fund_and_sync(&bitcoind, &electrsd, &node, taproot_addr, 80_000).await; + assert!(node.get_balance_for_address_type(AddressType::Taproot).is_err()); + + // Switching primary to an unsynced type clears the primary sync timestamp, forcing a + // full scan on the next cycle. + node.set_primary_address_type(AddressType::Taproot, read_node_seed(&node)).unwrap(); + node.sync_wallets().unwrap(); + + let taproot_balance = node.get_balance_for_address_type(AddressType::Taproot).unwrap(); + assert!(taproot_balance.total_sats >= 79_000, "got {} sats", taproot_balance.total_sats); + + node.stop().unwrap(); + } +}