diff --git a/CHANGELOG.md b/CHANGELOG.md index 3846f71c3..af462accf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 0.7.0-rc.54 (Synonym Fork) +# 0.7.0-rc.55 (Synonym Fork) ## Bug Fixes @@ -65,16 +65,23 @@ ## Synonym Fork Additions +- Added configurable Electrum batch size and stop gap for full scans of non-primary on-chain + wallets, while preserving the existing primary-wallet behavior and defaults. +- Added derived-account lifecycle parity with account-`0` wallets: configured accounts load on + build, registered accounts can be unloaded at runtime without deleting persisted BDK state, and + account-specific address metadata can be derived by index or range. - Added derived on-chain wallet account support (`account_index >= 1`) from the node's master seed: - - `export_onchain_wallet_account_xpub` / `add_onchain_wallet_account` (idempotent; validates - xpub; rejects account `0` and indexes above `MAX_ONCHAIN_WALLET_ACCOUNT_INDEX`); + - `export_onchain_wallet_account_xpub` / `add_onchain_wallet_account` / + `remove_onchain_wallet_account` (idempotent add; validates xpub; reserves account `0`); `get_balance_for_onchain_wallet_account` / `list_onchain_wallet_accounts`; - `OnchainPayment::new_address_for_account` / `new_address_info_for_account` / - `reveal_receive_addresses_to_account`; - `OnchainWalletAccount` - - Registration is not persisted and not auto-loaded; re-add after each build. BDK data - remains persisted per account + `OnchainPayment::new_address_for_account` / + `new_address_info_for_account` / `address_info_for_account_at_index` / + `address_infos_for_account` / `reveal_receive_addresses_to_account`; + `OnchainWalletAccount` / `OnchainWalletAccountConfig` + - Runtime registration changes are not persisted. Configure registrations in + `Config::onchain_wallet_accounts` to load them on each build. BDK data remains persisted per + account after unload - Derived accounts full-scan after registration, then use incremental sync. Apps issuing addresses from an exported xpub reveal their highest issued index before syncing; descriptor origins use the real account path; channel preflight requires an account-`0` SegWit builder before counting diff --git a/Cargo.toml b/Cargo.toml index c5cad275e..6cc499858 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ exclude = ["bindings/uniffi-bindgen"] [package] name = "ldk-node" -version = "0.7.0-rc.54" +version = "0.7.0-rc.55" authors = ["Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" diff --git a/Package.swift b/Package.swift index d7bee989b..a9037e0e4 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.7.0-rc.54" -let checksum = "29e608bc67576cfe6e658939179d71105d6943c74f72e4e358e1f5ccc8a7b877" +let tag = "v0.7.0-rc.55" +let checksum = "098d48d602fe000f1a9dbc7e758a0fe7bd8720c12b0036c38edf38283edd55f3" 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 69a3e236c..c6f65042c 100644 --- a/bindings/kotlin/ldk-node-android/gradle.properties +++ b/bindings/kotlin/ldk-node-android/gradle.properties @@ -3,4 +3,4 @@ android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official group=com.synonym -version=0.7.0-rc.54 +version=0.7.0-rc.55 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 6b4fec0e2..54a09051e 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 37d113655..93b8cbbf7 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 a83701365..f9db6b1bb 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 f3d72913d..79759d5cb 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 @@ -1543,6 +1543,12 @@ internal typealias UniffiVTableCallbackInterfaceVssHeaderProviderUniffiByValue = + + + + + + @@ -2432,6 +2438,12 @@ internal interface UniffiLib : Library { `addressType`: RustBufferByValue, uniffiCallStatus: UniffiRustCallStatus, ): Unit + fun uniffi_ldk_node_fn_method_node_remove_onchain_wallet_account( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `accountIndex`: Int, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit fun uniffi_ldk_node_fn_method_node_remove_payment( `ptr`: Pointer?, `paymentId`: RustBufferByValue, @@ -2612,6 +2624,14 @@ internal interface UniffiLib : Library { `destinationAddress`: RustBufferByValue, uniffiCallStatus: UniffiRustCallStatus, ): RustBufferByValue + fun uniffi_ldk_node_fn_method_onchainpayment_address_info_for_account_at_index( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `accountIndex`: Int, + `keychain`: RustBufferByValue, + `index`: Int, + uniffiCallStatus: UniffiRustCallStatus, + ): RustBufferByValue fun uniffi_ldk_node_fn_method_onchainpayment_address_info_for_type_at_index( `ptr`: Pointer?, `addressType`: RustBufferByValue, @@ -2619,6 +2639,15 @@ internal interface UniffiLib : Library { `index`: Int, uniffiCallStatus: UniffiRustCallStatus, ): RustBufferByValue + fun uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_account( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `accountIndex`: Int, + `keychain`: RustBufferByValue, + `startIndex`: Int, + `count`: Int, + uniffiCallStatus: UniffiRustCallStatus, + ): RustBufferByValue fun uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_type( `ptr`: Pointer?, `addressType`: RustBufferByValue, @@ -3388,6 +3417,8 @@ internal interface UniffiLib : Library { ): Short fun uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor( ): Short + fun uniffi_ldk_node_checksum_method_node_remove_onchain_wallet_account( + ): Short fun uniffi_ldk_node_checksum_method_node_remove_payment( ): Short fun uniffi_ldk_node_checksum_method_node_set_primary_address_type( @@ -3446,8 +3477,12 @@ internal interface UniffiLib : Library { ): Short fun uniffi_ldk_node_checksum_method_onchainpayment_accelerate_by_cpfp( ): Short + fun uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_account_at_index( + ): Short fun uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_type_at_index( ): Short + fun uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_account( + ): Short fun uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_type( ): Short fun uniffi_ldk_node_checksum_method_onchainpayment_bump_fee_by_rbf( @@ -3983,6 +4018,9 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { 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_onchain_wallet_account() != 21186.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") } @@ -4070,9 +4108,15 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_ldk_node_checksum_method_onchainpayment_accelerate_by_cpfp() != 31954.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_account_at_index() != 63246.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_type_at_index() != 42692.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_account() != 39321.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_type() != 3701.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -7661,6 +7705,20 @@ open class Node: Disposable, NodeInterface { } } + @Throws(NodeException::class) + override fun `removeOnchainWalletAccount`(`addressType`: AddressType, `accountIndex`: kotlin.UInt) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_remove_onchain_wallet_account( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterUInt.lower(`accountIndex`), + uniffiRustCallStatus, + ) + } + } + } + @Throws(NodeException::class) override fun `removePayment`(`paymentId`: PaymentId) { callWithPointer { @@ -8318,6 +8376,22 @@ open class OnchainPayment: Disposable, OnchainPaymentInterface { }) } + @Throws(NodeException::class) + override fun `addressInfoForAccountAtIndex`(`addressType`: AddressType, `accountIndex`: kotlin.UInt, `keychain`: KeychainKind, `index`: kotlin.UInt): AddressInfo { + return FfiConverterTypeAddressInfo.lift(callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_onchainpayment_address_info_for_account_at_index( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterUInt.lower(`accountIndex`), + FfiConverterTypeKeychainKind.lower(`keychain`), + FfiConverterUInt.lower(`index`), + uniffiRustCallStatus, + ) + } + }) + } + @Throws(NodeException::class) override fun `addressInfoForTypeAtIndex`(`addressType`: AddressType, `keychain`: KeychainKind, `index`: kotlin.UInt): AddressInfo { return FfiConverterTypeAddressInfo.lift(callWithPointer { @@ -8333,6 +8407,23 @@ open class OnchainPayment: Disposable, OnchainPaymentInterface { }) } + @Throws(NodeException::class) + override fun `addressInfosForAccount`(`addressType`: AddressType, `accountIndex`: kotlin.UInt, `keychain`: KeychainKind, `startIndex`: kotlin.UInt, `count`: kotlin.UInt): List { + return FfiConverterSequenceTypeAddressInfo.lift(callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_account( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterUInt.lower(`accountIndex`), + FfiConverterTypeKeychainKind.lower(`keychain`), + FfiConverterUInt.lower(`startIndex`), + FfiConverterUInt.lower(`count`), + uniffiRustCallStatus, + ) + } + }) + } + @Throws(NodeException::class) override fun `addressInfosForType`(`addressType`: AddressType, `keychain`: KeychainKind, `startIndex`: kotlin.UInt, `count`: kotlin.UInt): List { return FfiConverterSequenceTypeAddressInfo.lift(callWithPointer { @@ -9818,6 +9909,7 @@ object FfiConverterTypeConfig: FfiConverterRustBuffer { FfiConverterBoolean.read(buf), FfiConverterTypeAddressType.read(buf), FfiConverterSequenceTypeAddressType.read(buf), + FfiConverterSequenceTypeOnchainWalletAccountConfig.read(buf), ) } @@ -9835,7 +9927,8 @@ object FfiConverterTypeConfig: FfiConverterRustBuffer { FfiConverterOptionalTypeScoringDecayParameters.allocationSize(value.`scoringDecayParams`) + FfiConverterBoolean.allocationSize(value.`includeUntrustedPendingInSpendable`) + FfiConverterTypeAddressType.allocationSize(value.`addressType`) + - FfiConverterSequenceTypeAddressType.allocationSize(value.`addressTypesToMonitor`) + FfiConverterSequenceTypeAddressType.allocationSize(value.`addressTypesToMonitor`) + + FfiConverterSequenceTypeOnchainWalletAccountConfig.allocationSize(value.`onchainWalletAccounts`) ) override fun write(value: Config, buf: ByteBuffer) { @@ -9853,6 +9946,7 @@ object FfiConverterTypeConfig: FfiConverterRustBuffer { FfiConverterBoolean.write(value.`includeUntrustedPendingInSpendable`, buf) FfiConverterTypeAddressType.write(value.`addressType`, buf) FfiConverterSequenceTypeAddressType.write(value.`addressTypesToMonitor`, buf) + FfiConverterSequenceTypeOnchainWalletAccountConfig.write(value.`onchainWalletAccounts`, buf) } } @@ -9886,17 +9980,23 @@ object FfiConverterTypeElectrumSyncConfig: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): OnchainWalletAccountConfig { + return OnchainWalletAccountConfig( + FfiConverterTypeAddressType.read(buf), + FfiConverterUInt.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: OnchainWalletAccountConfig) = ( + FfiConverterTypeAddressType.allocationSize(value.`addressType`) + + FfiConverterUInt.allocationSize(value.`accountIndex`) + + FfiConverterString.allocationSize(value.`xpub`) + ) + + override fun write(value: OnchainWalletAccountConfig, buf: ByteBuffer) { + FfiConverterTypeAddressType.write(value.`addressType`, buf) + FfiConverterUInt.write(value.`accountIndex`, buf) + FfiConverterString.write(value.`xpub`, buf) + } +} + + + + object FfiConverterTypeOutPoint: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): OutPoint { return OutPoint( @@ -12097,7 +12222,8 @@ object FfiConverterTypeNodeError : FfiConverterRustBuffer { 64 -> NodeException.AddressTypeAlreadyMonitored(FfiConverterString.read(buf)) 65 -> NodeException.AddressTypeIsPrimary(FfiConverterString.read(buf)) 66 -> NodeException.AddressTypeNotMonitored(FfiConverterString.read(buf)) - 67 -> NodeException.InvalidSeedBytes(FfiConverterString.read(buf)) + 67 -> NodeException.OnchainWalletAccountNotRegistered(FfiConverterString.read(buf)) + 68 -> NodeException.InvalidSeedBytes(FfiConverterString.read(buf)) else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } } @@ -12372,10 +12498,14 @@ object FfiConverterTypeNodeError : FfiConverterRustBuffer { buf.putInt(66) Unit } - is NodeException.InvalidSeedBytes -> { + is NodeException.OnchainWalletAccountNotRegistered -> { buf.putInt(67) Unit } + is NodeException.InvalidSeedBytes -> { + buf.putInt(68) + Unit + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } } @@ -14406,6 +14536,31 @@ object FfiConverterSequenceTypeOnchainWalletAccount: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeOnchainWalletAccountConfig.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.sumOf { FfiConverterTypeOnchainWalletAccountConfig.allocationSize(it) } + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeOnchainWalletAccountConfig.write(it, buf) + } + } +} + + + + object FfiConverterSequenceTypePaymentDetails: FfiConverterRustBuffer> { override fun read(buf: ByteBuffer): List { val len = buf.getInt() 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 dbb627599..2716af4bc 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 @@ -510,6 +510,9 @@ interface NodeInterface { @Throws(NodeException::class) fun `removeAddressTypeFromMonitor`(`addressType`: AddressType) + @Throws(NodeException::class) + fun `removeOnchainWalletAccount`(`addressType`: AddressType, `accountIndex`: kotlin.UInt) + @Throws(NodeException::class) fun `removePayment`(`paymentId`: PaymentId) @@ -595,9 +598,15 @@ interface OnchainPaymentInterface { @Throws(NodeException::class) fun `accelerateByCpfp`(`txid`: Txid, `feeRate`: FeeRate?, `destinationAddress`: Address?): Txid + @Throws(NodeException::class) + fun `addressInfoForAccountAtIndex`(`addressType`: AddressType, `accountIndex`: kotlin.UInt, `keychain`: KeychainKind, `index`: kotlin.UInt): AddressInfo + @Throws(NodeException::class) fun `addressInfoForTypeAtIndex`(`addressType`: AddressType, `keychain`: KeychainKind, `index`: kotlin.UInt): AddressInfo + @Throws(NodeException::class) + fun `addressInfosForAccount`(`addressType`: AddressType, `accountIndex`: kotlin.UInt, `keychain`: KeychainKind, `startIndex`: kotlin.UInt, `count`: kotlin.UInt): List + @Throws(NodeException::class) fun `addressInfosForType`(`addressType`: AddressType, `keychain`: KeychainKind, `startIndex`: kotlin.UInt, `count`: kotlin.UInt): List @@ -903,7 +912,8 @@ data class Config ( val `scoringDecayParams`: ScoringDecayParameters?, val `includeUntrustedPendingInSpendable`: kotlin.Boolean, val `addressType`: AddressType, - val `addressTypesToMonitor`: List + val `addressTypesToMonitor`: List, + val `onchainWalletAccounts`: List ) { companion object } @@ -923,7 +933,9 @@ data class CustomTlvRecord ( @kotlinx.serialization.Serializable data class ElectrumSyncConfig ( val `backgroundSyncConfig`: BackgroundSyncConfig?, - val `connectionTimeoutSecs`: kotlin.ULong + val `connectionTimeoutSecs`: kotlin.ULong, + val `additionalWalletFullScanBatchSize`: kotlin.UInt, + val `additionalWalletFullScanStopGap`: kotlin.UInt ) { companion object } @@ -1139,6 +1151,17 @@ data class OnchainWalletAccount ( +@kotlinx.serialization.Serializable +data class OnchainWalletAccountConfig ( + val `addressType`: AddressType, + val `accountIndex`: kotlin.UInt, + val `xpub`: kotlin.String +) { + companion object +} + + + @kotlinx.serialization.Serializable data class OutPoint ( val `txid`: Txid, @@ -2000,6 +2023,8 @@ sealed class NodeException(message: String): kotlin.Exception(message) { class AddressTypeNotMonitored(message: String) : NodeException(message) + class OnchainWalletAccountNotRegistered(message: String) : NodeException(message) + class InvalidSeedBytes(message: String) : NodeException(message) } @@ -2386,6 +2411,8 @@ enum class WordCount { + + diff --git a/bindings/kotlin/ldk-node-jvm/gradle.properties b/bindings/kotlin/ldk-node-jvm/gradle.properties index bf021f74b..70ca71025 100644 --- a/bindings/kotlin/ldk-node-jvm/gradle.properties +++ b/bindings/kotlin/ldk-node-jvm/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536m kotlin.code.style=official group=com.synonym -version=0.7.0-rc.54 +version=0.7.0-rc.55 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 dbb627599..2716af4bc 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 @@ -510,6 +510,9 @@ interface NodeInterface { @Throws(NodeException::class) fun `removeAddressTypeFromMonitor`(`addressType`: AddressType) + @Throws(NodeException::class) + fun `removeOnchainWalletAccount`(`addressType`: AddressType, `accountIndex`: kotlin.UInt) + @Throws(NodeException::class) fun `removePayment`(`paymentId`: PaymentId) @@ -595,9 +598,15 @@ interface OnchainPaymentInterface { @Throws(NodeException::class) fun `accelerateByCpfp`(`txid`: Txid, `feeRate`: FeeRate?, `destinationAddress`: Address?): Txid + @Throws(NodeException::class) + fun `addressInfoForAccountAtIndex`(`addressType`: AddressType, `accountIndex`: kotlin.UInt, `keychain`: KeychainKind, `index`: kotlin.UInt): AddressInfo + @Throws(NodeException::class) fun `addressInfoForTypeAtIndex`(`addressType`: AddressType, `keychain`: KeychainKind, `index`: kotlin.UInt): AddressInfo + @Throws(NodeException::class) + fun `addressInfosForAccount`(`addressType`: AddressType, `accountIndex`: kotlin.UInt, `keychain`: KeychainKind, `startIndex`: kotlin.UInt, `count`: kotlin.UInt): List + @Throws(NodeException::class) fun `addressInfosForType`(`addressType`: AddressType, `keychain`: KeychainKind, `startIndex`: kotlin.UInt, `count`: kotlin.UInt): List @@ -903,7 +912,8 @@ data class Config ( val `scoringDecayParams`: ScoringDecayParameters?, val `includeUntrustedPendingInSpendable`: kotlin.Boolean, val `addressType`: AddressType, - val `addressTypesToMonitor`: List + val `addressTypesToMonitor`: List, + val `onchainWalletAccounts`: List ) { companion object } @@ -923,7 +933,9 @@ data class CustomTlvRecord ( @kotlinx.serialization.Serializable data class ElectrumSyncConfig ( val `backgroundSyncConfig`: BackgroundSyncConfig?, - val `connectionTimeoutSecs`: kotlin.ULong + val `connectionTimeoutSecs`: kotlin.ULong, + val `additionalWalletFullScanBatchSize`: kotlin.UInt, + val `additionalWalletFullScanStopGap`: kotlin.UInt ) { companion object } @@ -1139,6 +1151,17 @@ data class OnchainWalletAccount ( +@kotlinx.serialization.Serializable +data class OnchainWalletAccountConfig ( + val `addressType`: AddressType, + val `accountIndex`: kotlin.UInt, + val `xpub`: kotlin.String +) { + companion object +} + + + @kotlinx.serialization.Serializable data class OutPoint ( val `txid`: Txid, @@ -2000,6 +2023,8 @@ sealed class NodeException(message: String): kotlin.Exception(message) { class AddressTypeNotMonitored(message: String) : NodeException(message) + class OnchainWalletAccountNotRegistered(message: String) : NodeException(message) + class InvalidSeedBytes(message: String) : NodeException(message) } @@ -2386,6 +2411,8 @@ enum class WordCount { + + 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 c232ccc5b..5bb3df135 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 @@ -1541,6 +1541,12 @@ internal typealias UniffiVTableCallbackInterfaceVssHeaderProviderUniffiByValue = + + + + + + @@ -2430,6 +2436,12 @@ internal interface UniffiLib : Library { `addressType`: RustBufferByValue, uniffiCallStatus: UniffiRustCallStatus, ): Unit + fun uniffi_ldk_node_fn_method_node_remove_onchain_wallet_account( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `accountIndex`: Int, + uniffiCallStatus: UniffiRustCallStatus, + ): Unit fun uniffi_ldk_node_fn_method_node_remove_payment( `ptr`: Pointer?, `paymentId`: RustBufferByValue, @@ -2610,6 +2622,14 @@ internal interface UniffiLib : Library { `destinationAddress`: RustBufferByValue, uniffiCallStatus: UniffiRustCallStatus, ): RustBufferByValue + fun uniffi_ldk_node_fn_method_onchainpayment_address_info_for_account_at_index( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `accountIndex`: Int, + `keychain`: RustBufferByValue, + `index`: Int, + uniffiCallStatus: UniffiRustCallStatus, + ): RustBufferByValue fun uniffi_ldk_node_fn_method_onchainpayment_address_info_for_type_at_index( `ptr`: Pointer?, `addressType`: RustBufferByValue, @@ -2617,6 +2637,15 @@ internal interface UniffiLib : Library { `index`: Int, uniffiCallStatus: UniffiRustCallStatus, ): RustBufferByValue + fun uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_account( + `ptr`: Pointer?, + `addressType`: RustBufferByValue, + `accountIndex`: Int, + `keychain`: RustBufferByValue, + `startIndex`: Int, + `count`: Int, + uniffiCallStatus: UniffiRustCallStatus, + ): RustBufferByValue fun uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_type( `ptr`: Pointer?, `addressType`: RustBufferByValue, @@ -3386,6 +3415,8 @@ internal interface UniffiLib : Library { ): Short fun uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor( ): Short + fun uniffi_ldk_node_checksum_method_node_remove_onchain_wallet_account( + ): Short fun uniffi_ldk_node_checksum_method_node_remove_payment( ): Short fun uniffi_ldk_node_checksum_method_node_set_primary_address_type( @@ -3444,8 +3475,12 @@ internal interface UniffiLib : Library { ): Short fun uniffi_ldk_node_checksum_method_onchainpayment_accelerate_by_cpfp( ): Short + fun uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_account_at_index( + ): Short fun uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_type_at_index( ): Short + fun uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_account( + ): Short fun uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_type( ): Short fun uniffi_ldk_node_checksum_method_onchainpayment_bump_fee_by_rbf( @@ -3981,6 +4016,9 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { 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_onchain_wallet_account() != 21186.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") } @@ -4068,9 +4106,15 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_ldk_node_checksum_method_onchainpayment_accelerate_by_cpfp() != 31954.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_account_at_index() != 63246.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_type_at_index() != 42692.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_account() != 39321.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_type() != 3701.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -7650,6 +7694,20 @@ open class Node: Disposable, NodeInterface { } } + @Throws(NodeException::class) + override fun `removeOnchainWalletAccount`(`addressType`: AddressType, `accountIndex`: kotlin.UInt) { + callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_node_remove_onchain_wallet_account( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterUInt.lower(`accountIndex`), + uniffiRustCallStatus, + ) + } + } + } + @Throws(NodeException::class) override fun `removePayment`(`paymentId`: PaymentId) { callWithPointer { @@ -8307,6 +8365,22 @@ open class OnchainPayment: Disposable, OnchainPaymentInterface { }) } + @Throws(NodeException::class) + override fun `addressInfoForAccountAtIndex`(`addressType`: AddressType, `accountIndex`: kotlin.UInt, `keychain`: KeychainKind, `index`: kotlin.UInt): AddressInfo { + return FfiConverterTypeAddressInfo.lift(callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_onchainpayment_address_info_for_account_at_index( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterUInt.lower(`accountIndex`), + FfiConverterTypeKeychainKind.lower(`keychain`), + FfiConverterUInt.lower(`index`), + uniffiRustCallStatus, + ) + } + }) + } + @Throws(NodeException::class) override fun `addressInfoForTypeAtIndex`(`addressType`: AddressType, `keychain`: KeychainKind, `index`: kotlin.UInt): AddressInfo { return FfiConverterTypeAddressInfo.lift(callWithPointer { @@ -8322,6 +8396,23 @@ open class OnchainPayment: Disposable, OnchainPaymentInterface { }) } + @Throws(NodeException::class) + override fun `addressInfosForAccount`(`addressType`: AddressType, `accountIndex`: kotlin.UInt, `keychain`: KeychainKind, `startIndex`: kotlin.UInt, `count`: kotlin.UInt): List { + return FfiConverterSequenceTypeAddressInfo.lift(callWithPointer { + uniffiRustCallWithError(NodeExceptionErrorHandler) { uniffiRustCallStatus -> + UniffiLib.INSTANCE.uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_account( + it, + FfiConverterTypeAddressType.lower(`addressType`), + FfiConverterUInt.lower(`accountIndex`), + FfiConverterTypeKeychainKind.lower(`keychain`), + FfiConverterUInt.lower(`startIndex`), + FfiConverterUInt.lower(`count`), + uniffiRustCallStatus, + ) + } + }) + } + @Throws(NodeException::class) override fun `addressInfosForType`(`addressType`: AddressType, `keychain`: KeychainKind, `startIndex`: kotlin.UInt, `count`: kotlin.UInt): List { return FfiConverterSequenceTypeAddressInfo.lift(callWithPointer { @@ -9807,6 +9898,7 @@ object FfiConverterTypeConfig: FfiConverterRustBuffer { FfiConverterBoolean.read(buf), FfiConverterTypeAddressType.read(buf), FfiConverterSequenceTypeAddressType.read(buf), + FfiConverterSequenceTypeOnchainWalletAccountConfig.read(buf), ) } @@ -9824,7 +9916,8 @@ object FfiConverterTypeConfig: FfiConverterRustBuffer { FfiConverterOptionalTypeScoringDecayParameters.allocationSize(value.`scoringDecayParams`) + FfiConverterBoolean.allocationSize(value.`includeUntrustedPendingInSpendable`) + FfiConverterTypeAddressType.allocationSize(value.`addressType`) + - FfiConverterSequenceTypeAddressType.allocationSize(value.`addressTypesToMonitor`) + FfiConverterSequenceTypeAddressType.allocationSize(value.`addressTypesToMonitor`) + + FfiConverterSequenceTypeOnchainWalletAccountConfig.allocationSize(value.`onchainWalletAccounts`) ) override fun write(value: Config, buf: ByteBuffer) { @@ -9842,6 +9935,7 @@ object FfiConverterTypeConfig: FfiConverterRustBuffer { FfiConverterBoolean.write(value.`includeUntrustedPendingInSpendable`, buf) FfiConverterTypeAddressType.write(value.`addressType`, buf) FfiConverterSequenceTypeAddressType.write(value.`addressTypesToMonitor`, buf) + FfiConverterSequenceTypeOnchainWalletAccountConfig.write(value.`onchainWalletAccounts`, buf) } } @@ -9875,17 +9969,23 @@ object FfiConverterTypeElectrumSyncConfig: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): OnchainWalletAccountConfig { + return OnchainWalletAccountConfig( + FfiConverterTypeAddressType.read(buf), + FfiConverterUInt.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: OnchainWalletAccountConfig) = ( + FfiConverterTypeAddressType.allocationSize(value.`addressType`) + + FfiConverterUInt.allocationSize(value.`accountIndex`) + + FfiConverterString.allocationSize(value.`xpub`) + ) + + override fun write(value: OnchainWalletAccountConfig, buf: ByteBuffer) { + FfiConverterTypeAddressType.write(value.`addressType`, buf) + FfiConverterUInt.write(value.`accountIndex`, buf) + FfiConverterString.write(value.`xpub`, buf) + } +} + + + + object FfiConverterTypeOutPoint: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): OutPoint { return OutPoint( @@ -12086,7 +12211,8 @@ object FfiConverterTypeNodeError : FfiConverterRustBuffer { 64 -> NodeException.AddressTypeAlreadyMonitored(FfiConverterString.read(buf)) 65 -> NodeException.AddressTypeIsPrimary(FfiConverterString.read(buf)) 66 -> NodeException.AddressTypeNotMonitored(FfiConverterString.read(buf)) - 67 -> NodeException.InvalidSeedBytes(FfiConverterString.read(buf)) + 67 -> NodeException.OnchainWalletAccountNotRegistered(FfiConverterString.read(buf)) + 68 -> NodeException.InvalidSeedBytes(FfiConverterString.read(buf)) else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } } @@ -12361,10 +12487,14 @@ object FfiConverterTypeNodeError : FfiConverterRustBuffer { buf.putInt(66) Unit } - is NodeException.InvalidSeedBytes -> { + is NodeException.OnchainWalletAccountNotRegistered -> { buf.putInt(67) Unit } + is NodeException.InvalidSeedBytes -> { + buf.putInt(68) + Unit + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } } @@ -14395,6 +14525,31 @@ object FfiConverterSequenceTypeOnchainWalletAccount: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeOnchainWalletAccountConfig.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.sumOf { FfiConverterTypeOnchainWalletAccountConfig.allocationSize(it) } + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeOnchainWalletAccountConfig.write(it, buf) + } + } +} + + + + object FfiConverterSequenceTypePaymentDetails: FfiConverterRustBuffer> { override fun read(buf: ByteBuffer): List { val len = buf.getInt() 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 d433678eb..711d07683 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 b9362171c..48414c63b 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 e1256cf73..ea511083c 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -21,6 +21,7 @@ dictionary Config { boolean include_untrusted_pending_in_spendable; AddressType address_type; sequence address_types_to_monitor; + sequence onchain_wallet_accounts; }; dictionary ScoringFeeParameters { @@ -65,6 +66,8 @@ dictionary EsploraSyncConfig { dictionary ElectrumSyncConfig { BackgroundSyncConfig? background_sync_config; u64 connection_timeout_secs; + u32 additional_wallet_full_scan_batch_size; + u32 additional_wallet_full_scan_stop_gap; }; dictionary LSPS2ServiceConfig { @@ -100,6 +103,12 @@ dictionary OnchainWalletAccount { u32 account_index; }; +dictionary OnchainWalletAccountConfig { + AddressType address_type; + u32 account_index; + string xpub; +}; + enum KeychainKind { "External", "Internal", @@ -253,6 +262,8 @@ interface Node { [Throws=NodeError] void add_onchain_wallet_account(AddressType address_type, u32 account_index, string xpub); [Throws=NodeError] + void remove_onchain_wallet_account(AddressType address_type, u32 account_index); + [Throws=NodeError] AddressTypeBalance get_balance_for_onchain_wallet_account(AddressType address_type, u32 account_index); sequence list_onchain_wallet_accounts(); sequence list_payments(); @@ -359,8 +370,12 @@ interface OnchainPayment { [Throws=NodeError] AddressInfo address_info_for_type_at_index(AddressType address_type, KeychainKind keychain, u32 index); [Throws=NodeError] + AddressInfo address_info_for_account_at_index(AddressType address_type, u32 account_index, KeychainKind keychain, u32 index); + [Throws=NodeError] sequence address_infos_for_type(AddressType address_type, KeychainKind keychain, u32 start_index, u32 count); [Throws=NodeError] + sequence address_infos_for_account(AddressType address_type, u32 account_index, KeychainKind keychain, u32 start_index, u32 count); + [Throws=NodeError] void reveal_receive_addresses_to(AddressType address_type, u32 index); [Throws=NodeError] void reveal_receive_addresses_to_account(AddressType address_type, u32 account_index, u32 index); @@ -488,6 +503,7 @@ enum NodeError { "AddressTypeAlreadyMonitored", "AddressTypeIsPrimary", "AddressTypeNotMonitored", + "OnchainWalletAccountNotRegistered", "InvalidSeedBytes", }; diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index bfc2fb3f9..57bf0929a 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ldk_node" -version = "0.7.0-rc.54" +version = "0.7.0-rc.55" 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 20bf4703d..a7f064870 100644 --- a/bindings/python/src/ldk_node/ldk_node.py +++ b/bindings/python/src/ldk_node/ldk_node.py @@ -749,6 +749,8 @@ 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_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_onchain_wallet_account() != 21186: + 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: @@ -807,8 +809,12 @@ 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_onchainpayment_accelerate_by_cpfp() != 31954: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_account_at_index() != 63246: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_type_at_index() != 42692: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_account() != 39321: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_type() != 3701: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_ldk_node_checksum_method_onchainpayment_bump_fee_by_rbf() != 53877: @@ -2034,6 +2040,13 @@ class _UniffiVTableCallbackInterfaceVssHeaderProvider(ctypes.Structure): 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_onchain_wallet_account.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + ctypes.c_uint32, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_ldk_node_fn_method_node_remove_onchain_wallet_account.restype = None _UniffiLib.uniffi_ldk_node_fn_method_node_remove_payment.argtypes = ( ctypes.c_void_p, _UniffiRustBuffer, @@ -2252,6 +2265,15 @@ class _UniffiVTableCallbackInterfaceVssHeaderProvider(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_ldk_node_fn_method_onchainpayment_accelerate_by_cpfp.restype = _UniffiRustBuffer +_UniffiLib.uniffi_ldk_node_fn_method_onchainpayment_address_info_for_account_at_index.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + ctypes.c_uint32, + _UniffiRustBuffer, + ctypes.c_uint32, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_ldk_node_fn_method_onchainpayment_address_info_for_account_at_index.restype = _UniffiRustBuffer _UniffiLib.uniffi_ldk_node_fn_method_onchainpayment_address_info_for_type_at_index.argtypes = ( ctypes.c_void_p, _UniffiRustBuffer, @@ -2260,6 +2282,16 @@ class _UniffiVTableCallbackInterfaceVssHeaderProvider(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_ldk_node_fn_method_onchainpayment_address_info_for_type_at_index.restype = _UniffiRustBuffer +_UniffiLib.uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_account.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + ctypes.c_uint32, + _UniffiRustBuffer, + ctypes.c_uint32, + ctypes.c_uint32, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_account.restype = _UniffiRustBuffer _UniffiLib.uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_type.argtypes = ( ctypes.c_void_p, _UniffiRustBuffer, @@ -3281,6 +3313,9 @@ class _UniffiVTableCallbackInterfaceVssHeaderProvider(ctypes.Structure): _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_onchain_wallet_account.argtypes = ( +) +_UniffiLib.uniffi_ldk_node_checksum_method_node_remove_onchain_wallet_account.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 @@ -3368,9 +3403,15 @@ class _UniffiVTableCallbackInterfaceVssHeaderProvider(ctypes.Structure): _UniffiLib.uniffi_ldk_node_checksum_method_onchainpayment_accelerate_by_cpfp.argtypes = ( ) _UniffiLib.uniffi_ldk_node_checksum_method_onchainpayment_accelerate_by_cpfp.restype = ctypes.c_uint16 +_UniffiLib.uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_account_at_index.argtypes = ( +) +_UniffiLib.uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_account_at_index.restype = ctypes.c_uint16 _UniffiLib.uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_type_at_index.argtypes = ( ) _UniffiLib.uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_type_at_index.restype = ctypes.c_uint16 +_UniffiLib.uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_account.argtypes = ( +) +_UniffiLib.uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_account.restype = ctypes.c_uint16 _UniffiLib.uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_type.argtypes = ( ) _UniffiLib.uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_type.restype = ctypes.c_uint16 @@ -5885,6 +5926,8 @@ def payment(self, payment_id: "PaymentId"): raise NotImplementedError def remove_address_type_from_monitor(self, address_type: "AddressType"): raise NotImplementedError + def remove_onchain_wallet_account(self, address_type: "AddressType",account_index: "int"): + 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]"): @@ -6386,6 +6429,20 @@ def remove_address_type_from_monitor(self, address_type: "AddressType") -> None: + def remove_onchain_wallet_account(self, address_type: "AddressType",account_index: "int") -> None: + _UniffiConverterTypeAddressType.check_lower(address_type) + + _UniffiConverterUInt32.check_lower(account_index) + + _uniffi_rust_call_with_error(_UniffiConverterTypeNodeError,_UniffiLib.uniffi_ldk_node_fn_method_node_remove_onchain_wallet_account,self._uniffi_clone_pointer(), + _UniffiConverterTypeAddressType.lower(address_type), + _UniffiConverterUInt32.lower(account_index)) + + + + + + def remove_payment(self, payment_id: "PaymentId") -> None: _UniffiConverterTypePaymentId.check_lower(payment_id) @@ -6854,8 +6911,12 @@ def write(cls, value: OfferProtocol, buf: _UniffiRustBuffer): class OnchainPaymentProtocol(typing.Protocol): def accelerate_by_cpfp(self, txid: "Txid",fee_rate: "typing.Optional[FeeRate]",destination_address: "typing.Optional[Address]"): raise NotImplementedError + def address_info_for_account_at_index(self, address_type: "AddressType",account_index: "int",keychain: "KeychainKind",index: "int"): + raise NotImplementedError def address_info_for_type_at_index(self, address_type: "AddressType",keychain: "KeychainKind",index: "int"): raise NotImplementedError + def address_infos_for_account(self, address_type: "AddressType",account_index: "int",keychain: "KeychainKind",start_index: "int",count: "int"): + raise NotImplementedError def address_infos_for_type(self, address_type: "AddressType",keychain: "KeychainKind",start_index: "int",count: "int"): raise NotImplementedError def bump_fee_by_rbf(self, txid: "Txid",fee_rate: "FeeRate"): @@ -6935,6 +6996,27 @@ def accelerate_by_cpfp(self, txid: "Txid",fee_rate: "typing.Optional[FeeRate]",d + def address_info_for_account_at_index(self, address_type: "AddressType",account_index: "int",keychain: "KeychainKind",index: "int") -> "AddressInfo": + _UniffiConverterTypeAddressType.check_lower(address_type) + + _UniffiConverterUInt32.check_lower(account_index) + + _UniffiConverterTypeKeychainKind.check_lower(keychain) + + _UniffiConverterUInt32.check_lower(index) + + return _UniffiConverterTypeAddressInfo.lift( + _uniffi_rust_call_with_error(_UniffiConverterTypeNodeError,_UniffiLib.uniffi_ldk_node_fn_method_onchainpayment_address_info_for_account_at_index,self._uniffi_clone_pointer(), + _UniffiConverterTypeAddressType.lower(address_type), + _UniffiConverterUInt32.lower(account_index), + _UniffiConverterTypeKeychainKind.lower(keychain), + _UniffiConverterUInt32.lower(index)) + ) + + + + + def address_info_for_type_at_index(self, address_type: "AddressType",keychain: "KeychainKind",index: "int") -> "AddressInfo": _UniffiConverterTypeAddressType.check_lower(address_type) @@ -6953,6 +7035,30 @@ def address_info_for_type_at_index(self, address_type: "AddressType",keychain: " + def address_infos_for_account(self, address_type: "AddressType",account_index: "int",keychain: "KeychainKind",start_index: "int",count: "int") -> "typing.List[AddressInfo]": + _UniffiConverterTypeAddressType.check_lower(address_type) + + _UniffiConverterUInt32.check_lower(account_index) + + _UniffiConverterTypeKeychainKind.check_lower(keychain) + + _UniffiConverterUInt32.check_lower(start_index) + + _UniffiConverterUInt32.check_lower(count) + + return _UniffiConverterSequenceTypeAddressInfo.lift( + _uniffi_rust_call_with_error(_UniffiConverterTypeNodeError,_UniffiLib.uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_account,self._uniffi_clone_pointer(), + _UniffiConverterTypeAddressType.lower(address_type), + _UniffiConverterUInt32.lower(account_index), + _UniffiConverterTypeKeychainKind.lower(keychain), + _UniffiConverterUInt32.lower(start_index), + _UniffiConverterUInt32.lower(count)) + ) + + + + + def address_infos_for_type(self, address_type: "AddressType",keychain: "KeychainKind",start_index: "int",count: "int") -> "typing.List[AddressInfo]": _UniffiConverterTypeAddressType.check_lower(address_type) @@ -8540,7 +8646,8 @@ class Config: include_untrusted_pending_in_spendable: "bool" address_type: "AddressType" address_types_to_monitor: "typing.List[AddressType]" - def __init__(self, *, storage_dir_path: "str", network: "Network", listening_addresses: "typing.Optional[typing.List[SocketAddress]]", announcement_addresses: "typing.Optional[typing.List[SocketAddress]]", node_alias: "typing.Optional[NodeAlias]", trusted_peers_0conf: "typing.List[PublicKey]", probing_liquidity_limit_multiplier: "int", anchor_channels_config: "typing.Optional[AnchorChannelsConfig]", route_parameters: "typing.Optional[RouteParametersConfig]", scoring_fee_params: "typing.Optional[ScoringFeeParameters]", scoring_decay_params: "typing.Optional[ScoringDecayParameters]", include_untrusted_pending_in_spendable: "bool", address_type: "AddressType", address_types_to_monitor: "typing.List[AddressType]"): + onchain_wallet_accounts: "typing.List[OnchainWalletAccountConfig]" + def __init__(self, *, storage_dir_path: "str", network: "Network", listening_addresses: "typing.Optional[typing.List[SocketAddress]]", announcement_addresses: "typing.Optional[typing.List[SocketAddress]]", node_alias: "typing.Optional[NodeAlias]", trusted_peers_0conf: "typing.List[PublicKey]", probing_liquidity_limit_multiplier: "int", anchor_channels_config: "typing.Optional[AnchorChannelsConfig]", route_parameters: "typing.Optional[RouteParametersConfig]", scoring_fee_params: "typing.Optional[ScoringFeeParameters]", scoring_decay_params: "typing.Optional[ScoringDecayParameters]", include_untrusted_pending_in_spendable: "bool", address_type: "AddressType", address_types_to_monitor: "typing.List[AddressType]", onchain_wallet_accounts: "typing.List[OnchainWalletAccountConfig]"): self.storage_dir_path = storage_dir_path self.network = network self.listening_addresses = listening_addresses @@ -8555,9 +8662,10 @@ def __init__(self, *, storage_dir_path: "str", network: "Network", listening_add self.include_untrusted_pending_in_spendable = include_untrusted_pending_in_spendable self.address_type = address_type self.address_types_to_monitor = address_types_to_monitor + self.onchain_wallet_accounts = onchain_wallet_accounts def __str__(self): - return "Config(storage_dir_path={}, network={}, listening_addresses={}, announcement_addresses={}, node_alias={}, trusted_peers_0conf={}, probing_liquidity_limit_multiplier={}, anchor_channels_config={}, route_parameters={}, scoring_fee_params={}, scoring_decay_params={}, include_untrusted_pending_in_spendable={}, address_type={}, address_types_to_monitor={})".format(self.storage_dir_path, self.network, self.listening_addresses, self.announcement_addresses, self.node_alias, self.trusted_peers_0conf, self.probing_liquidity_limit_multiplier, self.anchor_channels_config, self.route_parameters, self.scoring_fee_params, self.scoring_decay_params, self.include_untrusted_pending_in_spendable, self.address_type, self.address_types_to_monitor) + return "Config(storage_dir_path={}, network={}, listening_addresses={}, announcement_addresses={}, node_alias={}, trusted_peers_0conf={}, probing_liquidity_limit_multiplier={}, anchor_channels_config={}, route_parameters={}, scoring_fee_params={}, scoring_decay_params={}, include_untrusted_pending_in_spendable={}, address_type={}, address_types_to_monitor={}, onchain_wallet_accounts={})".format(self.storage_dir_path, self.network, self.listening_addresses, self.announcement_addresses, self.node_alias, self.trusted_peers_0conf, self.probing_liquidity_limit_multiplier, self.anchor_channels_config, self.route_parameters, self.scoring_fee_params, self.scoring_decay_params, self.include_untrusted_pending_in_spendable, self.address_type, self.address_types_to_monitor, self.onchain_wallet_accounts) def __eq__(self, other): if self.storage_dir_path != other.storage_dir_path: @@ -8588,6 +8696,8 @@ def __eq__(self, other): return False if self.address_types_to_monitor != other.address_types_to_monitor: return False + if self.onchain_wallet_accounts != other.onchain_wallet_accounts: + return False return True class _UniffiConverterTypeConfig(_UniffiConverterRustBuffer): @@ -8608,6 +8718,7 @@ def read(buf): include_untrusted_pending_in_spendable=_UniffiConverterBool.read(buf), address_type=_UniffiConverterTypeAddressType.read(buf), address_types_to_monitor=_UniffiConverterSequenceTypeAddressType.read(buf), + onchain_wallet_accounts=_UniffiConverterSequenceTypeOnchainWalletAccountConfig.read(buf), ) @staticmethod @@ -8626,6 +8737,7 @@ def check_lower(value): _UniffiConverterBool.check_lower(value.include_untrusted_pending_in_spendable) _UniffiConverterTypeAddressType.check_lower(value.address_type) _UniffiConverterSequenceTypeAddressType.check_lower(value.address_types_to_monitor) + _UniffiConverterSequenceTypeOnchainWalletAccountConfig.check_lower(value.onchain_wallet_accounts) @staticmethod def write(value, buf): @@ -8643,6 +8755,7 @@ def write(value, buf): _UniffiConverterBool.write(value.include_untrusted_pending_in_spendable, buf) _UniffiConverterTypeAddressType.write(value.address_type, buf) _UniffiConverterSequenceTypeAddressType.write(value.address_types_to_monitor, buf) + _UniffiConverterSequenceTypeOnchainWalletAccountConfig.write(value.onchain_wallet_accounts, buf) class CustomTlvRecord: @@ -8684,18 +8797,26 @@ def write(value, buf): class ElectrumSyncConfig: background_sync_config: "typing.Optional[BackgroundSyncConfig]" connection_timeout_secs: "int" - def __init__(self, *, background_sync_config: "typing.Optional[BackgroundSyncConfig]", connection_timeout_secs: "int"): + additional_wallet_full_scan_batch_size: "int" + additional_wallet_full_scan_stop_gap: "int" + def __init__(self, *, background_sync_config: "typing.Optional[BackgroundSyncConfig]", connection_timeout_secs: "int", additional_wallet_full_scan_batch_size: "int", additional_wallet_full_scan_stop_gap: "int"): self.background_sync_config = background_sync_config self.connection_timeout_secs = connection_timeout_secs + self.additional_wallet_full_scan_batch_size = additional_wallet_full_scan_batch_size + self.additional_wallet_full_scan_stop_gap = additional_wallet_full_scan_stop_gap def __str__(self): - return "ElectrumSyncConfig(background_sync_config={}, connection_timeout_secs={})".format(self.background_sync_config, self.connection_timeout_secs) + return "ElectrumSyncConfig(background_sync_config={}, connection_timeout_secs={}, additional_wallet_full_scan_batch_size={}, additional_wallet_full_scan_stop_gap={})".format(self.background_sync_config, self.connection_timeout_secs, self.additional_wallet_full_scan_batch_size, self.additional_wallet_full_scan_stop_gap) def __eq__(self, other): if self.background_sync_config != other.background_sync_config: return False if self.connection_timeout_secs != other.connection_timeout_secs: return False + if self.additional_wallet_full_scan_batch_size != other.additional_wallet_full_scan_batch_size: + return False + if self.additional_wallet_full_scan_stop_gap != other.additional_wallet_full_scan_stop_gap: + return False return True class _UniffiConverterTypeElectrumSyncConfig(_UniffiConverterRustBuffer): @@ -8704,17 +8825,23 @@ def read(buf): return ElectrumSyncConfig( background_sync_config=_UniffiConverterOptionalTypeBackgroundSyncConfig.read(buf), connection_timeout_secs=_UniffiConverterUInt64.read(buf), + additional_wallet_full_scan_batch_size=_UniffiConverterUInt32.read(buf), + additional_wallet_full_scan_stop_gap=_UniffiConverterUInt32.read(buf), ) @staticmethod def check_lower(value): _UniffiConverterOptionalTypeBackgroundSyncConfig.check_lower(value.background_sync_config) _UniffiConverterUInt64.check_lower(value.connection_timeout_secs) + _UniffiConverterUInt32.check_lower(value.additional_wallet_full_scan_batch_size) + _UniffiConverterUInt32.check_lower(value.additional_wallet_full_scan_stop_gap) @staticmethod def write(value, buf): _UniffiConverterOptionalTypeBackgroundSyncConfig.write(value.background_sync_config, buf) _UniffiConverterUInt64.write(value.connection_timeout_secs, buf) + _UniffiConverterUInt32.write(value.additional_wallet_full_scan_batch_size, buf) + _UniffiConverterUInt32.write(value.additional_wallet_full_scan_stop_gap, buf) class EsploraSyncConfig: @@ -9459,6 +9586,49 @@ def write(value, buf): _UniffiConverterUInt32.write(value.account_index, buf) +class OnchainWalletAccountConfig: + address_type: "AddressType" + account_index: "int" + xpub: "str" + def __init__(self, *, address_type: "AddressType", account_index: "int", xpub: "str"): + self.address_type = address_type + self.account_index = account_index + self.xpub = xpub + + def __str__(self): + return "OnchainWalletAccountConfig(address_type={}, account_index={}, xpub={})".format(self.address_type, self.account_index, self.xpub) + + def __eq__(self, other): + if self.address_type != other.address_type: + return False + if self.account_index != other.account_index: + return False + if self.xpub != other.xpub: + return False + return True + +class _UniffiConverterTypeOnchainWalletAccountConfig(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return OnchainWalletAccountConfig( + address_type=_UniffiConverterTypeAddressType.read(buf), + account_index=_UniffiConverterUInt32.read(buf), + xpub=_UniffiConverterString.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiConverterTypeAddressType.check_lower(value.address_type) + _UniffiConverterUInt32.check_lower(value.account_index) + _UniffiConverterString.check_lower(value.xpub) + + @staticmethod + def write(value, buf): + _UniffiConverterTypeAddressType.write(value.address_type, buf) + _UniffiConverterUInt32.write(value.account_index, buf) + _UniffiConverterString.write(value.xpub, buf) + + class OutPoint: txid: "Txid" vout: "int" @@ -13364,6 +13534,11 @@ class AddressTypeNotMonitored(_UniffiTempNodeError): def __repr__(self): return "NodeError.AddressTypeNotMonitored({})".format(repr(str(self))) _UniffiTempNodeError.AddressTypeNotMonitored = AddressTypeNotMonitored # type: ignore + class OnchainWalletAccountNotRegistered(_UniffiTempNodeError): + + def __repr__(self): + return "NodeError.OnchainWalletAccountNotRegistered({})".format(repr(str(self))) + _UniffiTempNodeError.OnchainWalletAccountNotRegistered = OnchainWalletAccountNotRegistered # type: ignore class InvalidSeedBytes(_UniffiTempNodeError): def __repr__(self): @@ -13643,6 +13818,10 @@ def read(buf): _UniffiConverterString.read(buf), ) if variant == 67: + return NodeError.OnchainWalletAccountNotRegistered( + _UniffiConverterString.read(buf), + ) + if variant == 68: return NodeError.InvalidSeedBytes( _UniffiConverterString.read(buf), ) @@ -13782,6 +13961,8 @@ def check_lower(value): return if isinstance(value, NodeError.AddressTypeNotMonitored): return + if isinstance(value, NodeError.OnchainWalletAccountNotRegistered): + return if isinstance(value, NodeError.InvalidSeedBytes): return @@ -13919,8 +14100,10 @@ def write(value, buf): buf.write_i32(65) if isinstance(value, NodeError.AddressTypeNotMonitored): buf.write_i32(66) - if isinstance(value, NodeError.InvalidSeedBytes): + if isinstance(value, NodeError.OnchainWalletAccountNotRegistered): buf.write_i32(67) + if isinstance(value, NodeError.InvalidSeedBytes): + buf.write_i32(68) @@ -16481,6 +16664,31 @@ def read(cls, buf): +class _UniffiConverterSequenceTypeOnchainWalletAccountConfig(_UniffiConverterRustBuffer): + @classmethod + def check_lower(cls, value): + for item in value: + _UniffiConverterTypeOnchainWalletAccountConfig.check_lower(item) + + @classmethod + def write(cls, value, buf): + items = len(value) + buf.write_i32(items) + for item in value: + _UniffiConverterTypeOnchainWalletAccountConfig.write(item, buf) + + @classmethod + def read(cls, buf): + count = buf.read_i32() + if count < 0: + raise InternalError("Unexpected negative sequence length") + + return [ + _UniffiConverterTypeOnchainWalletAccountConfig.read(buf) for i in range(count) + ] + + + class _UniffiConverterSequenceTypePaymentDetails(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): @@ -17522,6 +17730,7 @@ def generate_entropy_mnemonic(word_count: "typing.Optional[WordCount]") -> "Mnem "NodeInfo", "NodeStatus", "OnchainWalletAccount", + "OnchainWalletAccountConfig", "OutPoint", "PaymentDetails", "PeerDetails", diff --git a/bindings/swift/Sources/LDKNode/LDKNode.swift b/bindings/swift/Sources/LDKNode/LDKNode.swift index 56470936c..f2ccaf687 100644 --- a/bindings/swift/Sources/LDKNode/LDKNode.swift +++ b/bindings/swift/Sources/LDKNode/LDKNode.swift @@ -2605,6 +2605,8 @@ public protocol NodeProtocol: AnyObject { func removeAddressTypeFromMonitor(addressType: AddressType) throws + func removeOnchainWalletAccount(addressType: AddressType, accountIndex: UInt32) throws + func removePayment(paymentId: PaymentId) throws func setPrimaryAddressType(addressType: AddressType, seedBytes: [UInt8]) throws @@ -2955,6 +2957,14 @@ open class Node: } } + open func removeOnchainWalletAccount(addressType: AddressType, accountIndex: UInt32) throws { + try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_remove_onchain_wallet_account(self.uniffiClonePointer(), + FfiConverterTypeAddressType.lower(addressType), + FfiConverterUInt32.lower(accountIndex), $0) + } + } + open func removePayment(paymentId: PaymentId) throws { try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_node_remove_payment(self.uniffiClonePointer(), @@ -3356,8 +3366,12 @@ public func FfiConverterTypeOffer_lower(_ value: Offer) -> UnsafeMutableRawPoint public protocol OnchainPaymentProtocol: AnyObject { func accelerateByCpfp(txid: Txid, feeRate: FeeRate?, destinationAddress: Address?) throws -> Txid + func addressInfoForAccountAtIndex(addressType: AddressType, accountIndex: UInt32, keychain: KeychainKind, index: UInt32) throws -> AddressInfo + func addressInfoForTypeAtIndex(addressType: AddressType, keychain: KeychainKind, index: UInt32) throws -> AddressInfo + func addressInfosForAccount(addressType: AddressType, accountIndex: UInt32, keychain: KeychainKind, startIndex: UInt32, count: UInt32) throws -> [AddressInfo] + func addressInfosForType(addressType: AddressType, keychain: KeychainKind, startIndex: UInt32, count: UInt32) throws -> [AddressInfo] func bumpFeeByRbf(txid: Txid, feeRate: FeeRate) throws -> Txid @@ -3451,6 +3465,16 @@ open class OnchainPayment: }) } + open func addressInfoForAccountAtIndex(addressType: AddressType, accountIndex: UInt32, keychain: KeychainKind, index: UInt32) throws -> AddressInfo { + return try FfiConverterTypeAddressInfo.lift(rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_onchainpayment_address_info_for_account_at_index(self.uniffiClonePointer(), + FfiConverterTypeAddressType.lower(addressType), + FfiConverterUInt32.lower(accountIndex), + FfiConverterTypeKeychainKind.lower(keychain), + FfiConverterUInt32.lower(index), $0) + }) + } + open func addressInfoForTypeAtIndex(addressType: AddressType, keychain: KeychainKind, index: UInt32) throws -> AddressInfo { return try FfiConverterTypeAddressInfo.lift(rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_onchainpayment_address_info_for_type_at_index(self.uniffiClonePointer(), @@ -3460,6 +3484,17 @@ open class OnchainPayment: }) } + open func addressInfosForAccount(addressType: AddressType, accountIndex: UInt32, keychain: KeychainKind, startIndex: UInt32, count: UInt32) throws -> [AddressInfo] { + return try FfiConverterSequenceTypeAddressInfo.lift(rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_account(self.uniffiClonePointer(), + FfiConverterTypeAddressType.lower(addressType), + FfiConverterUInt32.lower(accountIndex), + FfiConverterTypeKeychainKind.lower(keychain), + FfiConverterUInt32.lower(startIndex), + FfiConverterUInt32.lower(count), $0) + }) + } + open func addressInfosForType(addressType: AddressType, keychain: KeychainKind, startIndex: UInt32, count: UInt32) throws -> [AddressInfo] { return try FfiConverterSequenceTypeAddressInfo.lift(rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_onchainpayment_address_infos_for_type(self.uniffiClonePointer(), @@ -5319,10 +5354,11 @@ public struct Config { public var includeUntrustedPendingInSpendable: Bool public var addressType: AddressType public var addressTypesToMonitor: [AddressType] + public var onchainWalletAccounts: [OnchainWalletAccountConfig] /// Default memberwise initializers are never public by default, so we /// declare one manually. - public init(storageDirPath: String, network: Network, listeningAddresses: [SocketAddress]?, announcementAddresses: [SocketAddress]?, nodeAlias: NodeAlias?, trustedPeers0conf: [PublicKey], probingLiquidityLimitMultiplier: UInt64, anchorChannelsConfig: AnchorChannelsConfig?, routeParameters: RouteParametersConfig?, scoringFeeParams: ScoringFeeParameters?, scoringDecayParams: ScoringDecayParameters?, includeUntrustedPendingInSpendable: Bool, addressType: AddressType, addressTypesToMonitor: [AddressType]) { + public init(storageDirPath: String, network: Network, listeningAddresses: [SocketAddress]?, announcementAddresses: [SocketAddress]?, nodeAlias: NodeAlias?, trustedPeers0conf: [PublicKey], probingLiquidityLimitMultiplier: UInt64, anchorChannelsConfig: AnchorChannelsConfig?, routeParameters: RouteParametersConfig?, scoringFeeParams: ScoringFeeParameters?, scoringDecayParams: ScoringDecayParameters?, includeUntrustedPendingInSpendable: Bool, addressType: AddressType, addressTypesToMonitor: [AddressType], onchainWalletAccounts: [OnchainWalletAccountConfig]) { self.storageDirPath = storageDirPath self.network = network self.listeningAddresses = listeningAddresses @@ -5337,6 +5373,7 @@ public struct Config { self.includeUntrustedPendingInSpendable = includeUntrustedPendingInSpendable self.addressType = addressType self.addressTypesToMonitor = addressTypesToMonitor + self.onchainWalletAccounts = onchainWalletAccounts } } @@ -5384,6 +5421,9 @@ extension Config: Equatable, Hashable { if lhs.addressTypesToMonitor != rhs.addressTypesToMonitor { return false } + if lhs.onchainWalletAccounts != rhs.onchainWalletAccounts { + return false + } return true } @@ -5402,6 +5442,7 @@ extension Config: Equatable, Hashable { hasher.combine(includeUntrustedPendingInSpendable) hasher.combine(addressType) hasher.combine(addressTypesToMonitor) + hasher.combine(onchainWalletAccounts) } } @@ -5425,7 +5466,8 @@ public struct FfiConverterTypeConfig: FfiConverterRustBuffer { scoringDecayParams: FfiConverterOptionTypeScoringDecayParameters.read(from: &buf), includeUntrustedPendingInSpendable: FfiConverterBool.read(from: &buf), addressType: FfiConverterTypeAddressType.read(from: &buf), - addressTypesToMonitor: FfiConverterSequenceTypeAddressType.read(from: &buf) + addressTypesToMonitor: FfiConverterSequenceTypeAddressType.read(from: &buf), + onchainWalletAccounts: FfiConverterSequenceTypeOnchainWalletAccountConfig.read(from: &buf) ) } @@ -5444,6 +5486,7 @@ public struct FfiConverterTypeConfig: FfiConverterRustBuffer { FfiConverterBool.write(value.includeUntrustedPendingInSpendable, into: &buf) FfiConverterTypeAddressType.write(value.addressType, into: &buf) FfiConverterSequenceTypeAddressType.write(value.addressTypesToMonitor, into: &buf) + FfiConverterSequenceTypeOnchainWalletAccountConfig.write(value.onchainWalletAccounts, into: &buf) } } @@ -5525,12 +5568,16 @@ public func FfiConverterTypeCustomTlvRecord_lower(_ value: CustomTlvRecord) -> R public struct ElectrumSyncConfig { public var backgroundSyncConfig: BackgroundSyncConfig? public var connectionTimeoutSecs: UInt64 + public var additionalWalletFullScanBatchSize: UInt32 + public var additionalWalletFullScanStopGap: UInt32 /// Default memberwise initializers are never public by default, so we /// declare one manually. - public init(backgroundSyncConfig: BackgroundSyncConfig?, connectionTimeoutSecs: UInt64) { + public init(backgroundSyncConfig: BackgroundSyncConfig?, connectionTimeoutSecs: UInt64, additionalWalletFullScanBatchSize: UInt32, additionalWalletFullScanStopGap: UInt32) { self.backgroundSyncConfig = backgroundSyncConfig self.connectionTimeoutSecs = connectionTimeoutSecs + self.additionalWalletFullScanBatchSize = additionalWalletFullScanBatchSize + self.additionalWalletFullScanStopGap = additionalWalletFullScanStopGap } } @@ -5542,12 +5589,20 @@ extension ElectrumSyncConfig: Equatable, Hashable { if lhs.connectionTimeoutSecs != rhs.connectionTimeoutSecs { return false } + if lhs.additionalWalletFullScanBatchSize != rhs.additionalWalletFullScanBatchSize { + return false + } + if lhs.additionalWalletFullScanStopGap != rhs.additionalWalletFullScanStopGap { + return false + } return true } public func hash(into hasher: inout Hasher) { hasher.combine(backgroundSyncConfig) hasher.combine(connectionTimeoutSecs) + hasher.combine(additionalWalletFullScanBatchSize) + hasher.combine(additionalWalletFullScanStopGap) } } @@ -5559,13 +5614,17 @@ public struct FfiConverterTypeElectrumSyncConfig: FfiConverterRustBuffer { return try ElectrumSyncConfig( backgroundSyncConfig: FfiConverterOptionTypeBackgroundSyncConfig.read(from: &buf), - connectionTimeoutSecs: FfiConverterUInt64.read(from: &buf) + connectionTimeoutSecs: FfiConverterUInt64.read(from: &buf), + additionalWalletFullScanBatchSize: FfiConverterUInt32.read(from: &buf), + additionalWalletFullScanStopGap: FfiConverterUInt32.read(from: &buf) ) } public static func write(_ value: ElectrumSyncConfig, into buf: inout [UInt8]) { FfiConverterOptionTypeBackgroundSyncConfig.write(value.backgroundSyncConfig, into: &buf) FfiConverterUInt64.write(value.connectionTimeoutSecs, into: &buf) + FfiConverterUInt32.write(value.additionalWalletFullScanBatchSize, into: &buf) + FfiConverterUInt32.write(value.additionalWalletFullScanStopGap, into: &buf) } } @@ -6597,6 +6656,75 @@ public func FfiConverterTypeOnchainWalletAccount_lower(_ value: OnchainWalletAcc return FfiConverterTypeOnchainWalletAccount.lower(value) } +public struct OnchainWalletAccountConfig { + public var addressType: AddressType + public var accountIndex: UInt32 + public var xpub: String + + /// Default memberwise initializers are never public by default, so we + /// declare one manually. + public init(addressType: AddressType, accountIndex: UInt32, xpub: String) { + self.addressType = addressType + self.accountIndex = accountIndex + self.xpub = xpub + } +} + +extension OnchainWalletAccountConfig: Equatable, Hashable { + public static func == (lhs: OnchainWalletAccountConfig, rhs: OnchainWalletAccountConfig) -> Bool { + if lhs.addressType != rhs.addressType { + return false + } + if lhs.accountIndex != rhs.accountIndex { + return false + } + if lhs.xpub != rhs.xpub { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(addressType) + hasher.combine(accountIndex) + hasher.combine(xpub) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public struct FfiConverterTypeOnchainWalletAccountConfig: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> OnchainWalletAccountConfig { + return + try OnchainWalletAccountConfig( + addressType: FfiConverterTypeAddressType.read(from: &buf), + accountIndex: FfiConverterUInt32.read(from: &buf), + xpub: FfiConverterString.read(from: &buf) + ) + } + + public static func write(_ value: OnchainWalletAccountConfig, into buf: inout [UInt8]) { + FfiConverterTypeAddressType.write(value.addressType, into: &buf) + FfiConverterUInt32.write(value.accountIndex, into: &buf) + FfiConverterString.write(value.xpub, into: &buf) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeOnchainWalletAccountConfig_lift(_ buf: RustBuffer) throws -> OnchainWalletAccountConfig { + return try FfiConverterTypeOnchainWalletAccountConfig.lift(buf) +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeOnchainWalletAccountConfig_lower(_ value: OnchainWalletAccountConfig) -> RustBuffer { + return FfiConverterTypeOnchainWalletAccountConfig.lower(value) +} + public struct OutPoint { public var txid: Txid public var vout: UInt32 @@ -9191,6 +9319,8 @@ public enum NodeError { case AddressTypeNotMonitored(message: String) + case OnchainWalletAccountNotRegistered(message: String) + case InvalidSeedBytes(message: String) } @@ -9467,7 +9597,11 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer { message: FfiConverterString.read(from: &buf) ) - case 67: return try .InvalidSeedBytes( + case 67: return try .OnchainWalletAccountNotRegistered( + message: FfiConverterString.read(from: &buf) + ) + + case 68: return try .InvalidSeedBytes( message: FfiConverterString.read(from: &buf) ) @@ -9609,8 +9743,10 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer { writeInt(&buf, Int32(65)) case .AddressTypeNotMonitored(_ /* message is ignored*/ ): writeInt(&buf, Int32(66)) - case .InvalidSeedBytes(_ /* message is ignored*/ ): + case .OnchainWalletAccountNotRegistered(_ /* message is ignored*/ ): writeInt(&buf, Int32(67)) + case .InvalidSeedBytes(_ /* message is ignored*/ ): + writeInt(&buf, Int32(68)) } } } @@ -11589,6 +11725,31 @@ private struct FfiConverterSequenceTypeOnchainWalletAccount: FfiConverterRustBuf } } +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterSequenceTypeOnchainWalletAccountConfig: FfiConverterRustBuffer { + typealias SwiftType = [OnchainWalletAccountConfig] + + static func write(_ value: [OnchainWalletAccountConfig], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeOnchainWalletAccountConfig.write(item, into: &buf) + } + } + + static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [OnchainWalletAccountConfig] { + let len: Int32 = try readInt(&buf) + var seq = [OnchainWalletAccountConfig]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + try seq.append(FfiConverterTypeOnchainWalletAccountConfig.read(from: &buf)) + } + return seq + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -13327,6 +13488,9 @@ private var initializationResult: InitializationResult = { if uniffi_ldk_node_checksum_method_node_remove_address_type_from_monitor() != 37081 { return InitializationResult.apiChecksumMismatch } + if uniffi_ldk_node_checksum_method_node_remove_onchain_wallet_account() != 21186 { + return InitializationResult.apiChecksumMismatch + } if uniffi_ldk_node_checksum_method_node_remove_payment() != 47952 { return InitializationResult.apiChecksumMismatch } @@ -13414,9 +13578,15 @@ private var initializationResult: InitializationResult = { if uniffi_ldk_node_checksum_method_onchainpayment_accelerate_by_cpfp() != 31954 { return InitializationResult.apiChecksumMismatch } + if uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_account_at_index() != 63246 { + return InitializationResult.apiChecksumMismatch + } if uniffi_ldk_node_checksum_method_onchainpayment_address_info_for_type_at_index() != 42692 { return InitializationResult.apiChecksumMismatch } + if uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_account() != 39321 { + return InitializationResult.apiChecksumMismatch + } if uniffi_ldk_node_checksum_method_onchainpayment_address_infos_for_type() != 3701 { return InitializationResult.apiChecksumMismatch } diff --git a/src/builder.rs b/src/builder.rs index b0340e1fa..eed4b72cd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,7 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::convert::TryInto; use std::default::Default; use std::path::PathBuf; @@ -184,7 +184,7 @@ impl std::fmt::Debug for LogWriterConfig { /// [`Node`]: crate::Node #[derive(Debug, Clone, PartialEq)] pub enum BuildError { - /// The given seed bytes are invalid, e.g., have invalid length. + /// The seed bytes or a seed-derived wallet configuration are invalid. InvalidSeedBytes, /// The given seed file is invalid, e.g., has invalid length, or could not be read. InvalidSeedFile, @@ -232,7 +232,9 @@ pub enum BuildError { impl fmt::Display for BuildError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - Self::InvalidSeedBytes => write!(f, "Given seed bytes are invalid."), + Self::InvalidSeedBytes => { + write!(f, "Given seed bytes or seed-derived wallet configuration are invalid.") + }, Self::InvalidSeedFile => write!(f, "Given seed file is invalid or could not be read."), Self::InvalidSystemTime => { write!(f, "System time is invalid. Clocks might have gone back in time.") @@ -1451,56 +1453,91 @@ pub(crate) fn get_or_create_wallet_for_account( } } -fn build_additional_wallets( - config: &Config, xprv: Xpriv, kv_store: Arc, logger: Arc, - chain_tip_opt: Option<&BestBlock>, -) -> Vec<(OnchainWalletAccount, PersistedWallet, KVStoreWalletPersister)> { - let mut additional_wallets = Vec::new(); +fn validate_configured_onchain_wallet_accounts( + config: &Config, xprv: Xpriv, +) -> Result, BuildError> { + let mut seen = HashSet::new(); + let mut accounts = Vec::new(); + for configured_account in &config.onchain_wallet_accounts { + let account_index = configured_account.account_index; + if account_index == 0 || account_index > MAX_ONCHAIN_WALLET_ACCOUNT_INDEX { + return Err(BuildError::InvalidSeedBytes); + } - for address_type in config.additional_address_types() { - let wallet_account = OnchainWalletAccount::account_zero(address_type); - match (|| -> Result<_, BuildError> { - let mut persister = KVStoreWalletPersister::new( - Arc::clone(&kv_store), - Arc::clone(&logger), - wallet_account, - ); + let account = + OnchainWalletAccount { address_type: configured_account.address_type, account_index }; + let expected_xpub = derive_account_xpub_string( + xprv, + config.network, + configured_account.address_type, + account_index, + )?; + if configured_account.xpub.trim() != expected_xpub { + return Err(BuildError::InvalidSeedBytes); + } + if seen.insert(account) { + accounts.push(account); + } + } + Ok(accounts) +} - let (mut wallet, loaded_from_store) = get_or_create_wallet_for_account( - wallet_account, - xprv, - config.network, - &mut persister, - ) +fn build_wallet_for_account( + config: &Config, wallet_account: OnchainWalletAccount, xprv: Xpriv, kv_store: Arc, + logger: Arc, chain_tip_opt: Option<&BestBlock>, +) -> Result< + (OnchainWalletAccount, PersistedWallet, KVStoreWalletPersister), + BuildError, +> { + let mut persister = + KVStoreWalletPersister::new(Arc::clone(&kv_store), Arc::clone(&logger), wallet_account); + + let (mut wallet, loaded_from_store) = + get_or_create_wallet_for_account(wallet_account, xprv, config.network, &mut persister) .map_err(|e| { log_error!(logger, "Failed to setup wallet for {:?}: {}", wallet_account, e); e })?; - // Only checkpoint brand-new wallets to the current tip. Re-loaded wallets keep their - // persisted tip so chain backends can catch up without skipping history. - if !loaded_from_store { - if let Some(best_block) = chain_tip_opt { - let mut cp = wallet.latest_checkpoint(); - let block_id = bdk_chain::BlockId { - height: best_block.height, - hash: best_block.block_hash, - }; - 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 {:?}: {}", - wallet_account, - e - ); - BuildError::WalletSetupFailed - })?; - } - } - Ok((wallet_account, wallet, persister)) - })() { + // Re-loaded wallets keep their persisted tip so chain backends can catch up. New wallets start + // at the known tip; Esplora and Electrum recover history independently with a full scan, while + // Bitcoind cannot recover confirmed pre-registration history. + if !loaded_from_store { + if let Some(best_block) = chain_tip_opt { + let mut checkpoint = wallet.latest_checkpoint(); + let block_id = + bdk_chain::BlockId { height: best_block.height, hash: best_block.block_hash }; + checkpoint = checkpoint.insert(block_id); + let update = bdk_wallet::Update { chain: Some(checkpoint), ..Default::default() }; + wallet.apply_update(update).map_err(|e| { + log_error!(logger, "Failed to apply checkpoint for {:?}: {}", wallet_account, e); + BuildError::WalletSetupFailed + })?; + } + } + + Ok((wallet_account, wallet, persister)) +} + +fn build_additional_wallets( + config: &Config, configured_accounts: &[OnchainWalletAccount], xprv: Xpriv, + kv_store: Arc, logger: Arc, chain_tip_opt: Option<&BestBlock>, +) -> Result< + Vec<(OnchainWalletAccount, PersistedWallet, KVStoreWalletPersister)>, + BuildError, +> { + let mut additional_wallets = Vec::new(); + + for address_type in config.additional_address_types() { + let wallet_account = OnchainWalletAccount::account_zero(address_type); + match build_wallet_for_account( + config, + wallet_account, + xprv, + Arc::clone(&kv_store), + Arc::clone(&logger), + chain_tip_opt, + ) { Ok(tuple) => { log_info!(logger, "Created additional wallet for {:?}", tuple.0); additional_wallets.push(tuple); @@ -1516,7 +1553,20 @@ fn build_additional_wallets( } } - additional_wallets + for &wallet_account in configured_accounts { + let wallet = build_wallet_for_account( + config, + wallet_account, + xprv, + Arc::clone(&kv_store), + Arc::clone(&logger), + chain_tip_opt, + )?; + log_info!(logger, "Loaded configured wallet for {:?}", wallet_account); + additional_wallets.push(wallet); + } + + Ok(additional_wallets) } fn apply_channel_data_migration( @@ -1704,6 +1754,7 @@ fn build_with_store_internal( log_error!(logger, "Failed to derive master secret: {}", e); BuildError::InvalidSeedBytes })?; + let configured_accounts = validate_configured_onchain_wallet_accounts(&config, xprv)?; let tx_broadcaster = Arc::new(TransactionBroadcaster::new(Arc::clone(&logger))); let fee_estimator = Arc::new(OnchainFeeEstimator::new()); @@ -1784,8 +1835,10 @@ 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))); + let address_type_runtime_config = Arc::new(RwLock::new(AddressTypeRuntimeConfig::from_config( + &config, + configured_accounts.clone(), + ))); // Chain source setup let (chain_source, chain_tip_opt) = match chain_data_source_config { @@ -1893,11 +1946,12 @@ fn build_with_store_internal( let additional_wallets = build_additional_wallets( &config, + &configured_accounts, xprv, Arc::clone(&kv_store), Arc::clone(&logger), chain_tip_opt.as_ref(), - ); + )?; let wallet = Arc::new(Wallet::new( bdk_wallet, @@ -2671,6 +2725,8 @@ mod tests { use std::pin::Pin; use std::sync::Arc; + use bitcoin::bip32::Xpriv; + use bitcoin::Network; use lightning::io; use lightning::ln::functional_test_utils::{ create_announced_chan_between_nodes, create_chanmon_cfgs, create_network, create_node_cfgs, @@ -2687,9 +2743,11 @@ mod tests { use lightning::util::ser::Writeable; use super::{ - apply_channel_data_migration, sanitize_alias, BuildError, ChannelDataMigration, Config, + apply_channel_data_migration, derive_account_xpub_string, sanitize_alias, + validate_configured_onchain_wallet_accounts, BuildError, ChannelDataMigration, Config, NodeAlias, NodeBuilder, ScoringDecayParameters, ScoringFeeParameters, }; + use crate::config::{AddressType, OnchainWalletAccountConfig}; use crate::io::test_utils::InMemoryStore; use crate::logger::Logger; use crate::runtime::Runtime; @@ -2731,6 +2789,63 @@ mod tests { assert_eq!(node.err().unwrap(), BuildError::InvalidNodeAlias); } + #[test] + fn validates_and_deduplicates_configured_onchain_wallet_accounts() { + let xprv = Xpriv::new_master(Network::Regtest, &[42; 64]).unwrap(); + let xpub = + derive_account_xpub_string(xprv, Network::Regtest, AddressType::Taproot, 1).unwrap(); + let configured = OnchainWalletAccountConfig { + address_type: AddressType::Taproot, + account_index: 1, + xpub, + }; + let mut config = Config::default(); + config.network = Network::Regtest; + config.onchain_wallet_accounts = vec![configured.clone(), configured]; + + let accounts = validate_configured_onchain_wallet_accounts(&config, xprv).unwrap(); + assert_eq!(accounts.len(), 1); + assert_eq!(accounts[0].address_type, AddressType::Taproot); + assert_eq!(accounts[0].account_index, 1); + + config.onchain_wallet_accounts[0].xpub = "wrong xpub".to_string(); + assert_eq!( + validate_configured_onchain_wallet_accounts(&config, xprv), + Err(BuildError::InvalidSeedBytes) + ); + + config.onchain_wallet_accounts[0].account_index = 0; + assert_eq!( + validate_configured_onchain_wallet_accounts(&config, xprv), + Err(BuildError::InvalidSeedBytes) + ); + } + + #[test] + fn build_rejects_configured_account_xpub_from_another_seed() { + let foreign_xprv = Xpriv::new_master(Network::Regtest, &[43; 64]).unwrap(); + let foreign_xpub = derive_account_xpub_string( + foreign_xprv, + Network::Regtest, + AddressType::NativeSegwit, + 1, + ) + .unwrap(); + let mut config = Config::default(); + config.network = Network::Regtest; + config.onchain_wallet_accounts = vec![OnchainWalletAccountConfig { + address_type: AddressType::NativeSegwit, + account_index: 1, + xpub: foreign_xpub, + }]; + let mut builder = NodeBuilder::from_config(config); + builder.set_entropy_seed_bytes([42; 64]); + builder.set_log_facade_logger(); + let store: Arc = Arc::new(InMemoryStore::new()); + + assert!(matches!(builder.build_with_store(store), Err(BuildError::InvalidSeedBytes))); + } + #[test] fn set_scoring_params_updates_builder_config() { let mut builder = NodeBuilder::from_config(Config::default()); diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index b65270fdd..4fca28a02 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -27,8 +27,8 @@ use lightning_transaction_sync::ElectrumSyncClient; use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; use crate::config::{ 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, + BDK_ELECTRUM_CLIENT_BATCH_SIZE, 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::{ @@ -41,9 +41,24 @@ use crate::runtime::Runtime; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::NodeMetrics; -const BDK_ELECTRUM_CLIENT_BATCH_SIZE: usize = 5; const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct FullScanSettings { + stop_gap: usize, + batch_size: usize, +} + +const PRIMARY_WALLET_FULL_SCAN_SETTINGS: FullScanSettings = + FullScanSettings { stop_gap: BDK_CLIENT_STOP_GAP, batch_size: BDK_ELECTRUM_CLIENT_BATCH_SIZE }; + +fn additional_wallet_full_scan_settings(config: &ElectrumSyncConfig) -> FullScanSettings { + FullScanSettings { + stop_gap: config.additional_wallet_full_scan_stop_gap.max(1) as usize, + batch_size: config.additional_wallet_full_scan_batch_size.max(1) as usize, + } +} + pub(super) struct ElectrumChainSource { server_url: String, pub(super) sync_config: ElectrumSyncConfig, @@ -155,6 +170,7 @@ impl ElectrumChainSource { &self.node_metrics, &self.logger, )?; + let additional_full_scan_settings = additional_wallet_full_scan_settings(&self.sync_config); let primary_request: super::WalletSyncRequest = if primary_incremental { super::WalletSyncRequest::Incremental(onchain_wallet.get_incremental_sync_request()) @@ -189,7 +205,11 @@ impl ElectrumChainSource { super::WalletSyncRequest::FullScan(req) => { join_set.spawn(async move { let result: Result = client - .get_full_scan_wallet_update(req, txs.iter().cloned()) + .get_full_scan_wallet_update( + req, + txs.iter().cloned(), + PRIMARY_WALLET_FULL_SCAN_SETTINGS, + ) .await .map(|u| u.into()); (None, result) @@ -214,7 +234,11 @@ impl ElectrumChainSource { super::WalletSyncRequest::FullScan(req) => { join_set.spawn(async move { let result: Result = client - .get_full_scan_wallet_update(req, txs.iter().cloned()) + .get_full_scan_wallet_update( + req, + txs.iter().cloned(), + additional_full_scan_settings, + ) .await .map(|u| u.into()); (Some(wallet_account), result) @@ -641,17 +665,13 @@ impl ElectrumRuntimeClient { async fn get_full_scan_wallet_update( &self, request: BdkFullScanRequest, cached_txs: impl IntoIterator>>, + settings: FullScanSettings, ) -> Result, Error> { let bdk_electrum_client = Arc::clone(&self.bdk_electrum_client); bdk_electrum_client.populate_tx_cache(cached_txs); let spawn_fut = self.runtime.spawn_blocking(move || { - bdk_electrum_client.full_scan( - request, - BDK_CLIENT_STOP_GAP, - BDK_ELECTRUM_CLIENT_BATCH_SIZE, - true, - ) + bdk_electrum_client.full_scan(request, settings.stop_gap, settings.batch_size, true) }); let wallet_sync_timeout_fut = tokio::time::timeout(Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), spawn_fut); @@ -834,3 +854,45 @@ impl Filter for ElectrumRuntimeClient { self.tx_sync.register_output(output) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn additional_full_scan_settings_do_not_change_primary_defaults() { + let default_config = ElectrumSyncConfig::default(); + let config = ElectrumSyncConfig { + additional_wallet_full_scan_batch_size: 100, + additional_wallet_full_scan_stop_gap: 1_000, + ..Default::default() + }; + + assert_eq!( + PRIMARY_WALLET_FULL_SCAN_SETTINGS, + FullScanSettings { stop_gap: 20, batch_size: 5 } + ); + assert_eq!( + additional_wallet_full_scan_settings(&default_config), + PRIMARY_WALLET_FULL_SCAN_SETTINGS + ); + assert_eq!( + additional_wallet_full_scan_settings(&config), + FullScanSettings { stop_gap: 1_000, batch_size: 100 } + ); + } + + #[test] + fn additional_full_scan_settings_are_never_zero() { + let config = ElectrumSyncConfig { + additional_wallet_full_scan_batch_size: 0, + additional_wallet_full_scan_stop_gap: 0, + ..Default::default() + }; + + assert_eq!( + additional_wallet_full_scan_settings(&config), + FullScanSettings { stop_gap: 1, batch_size: 1 } + ); + } +} diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 9662ec500..65bbf3f3f 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -260,7 +260,7 @@ pub(super) fn apply_additional_sync_results( }; match onchain_wallet.apply_update_for_wallet_account(wallet_account, update) { - Ok(wallet_events) => { + Ok(Some(wallet_events)) => { any_applied = true; if let Some(ts) = SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()) @@ -272,6 +272,9 @@ pub(super) fn apply_additional_sync_results( } events.extend(wallet_events); }, + Ok(None) => { + log_debug!(logger, "Ignoring sync result for unloaded wallet {:?}", wallet_account); + }, Err(e) => { log_warn!(logger, "Failed to apply update to wallet {:?}: {}", wallet_account, e); error.get_or_insert(e); @@ -383,6 +386,40 @@ mod sync_tests { assert_eq!(outcome.events.len(), 1); assert_eq!(outcome.result(), Err(Error::WalletOperationTimeout)); } + + #[test] + fn late_sync_result_for_removed_account_is_ignored() { + let mut config = Config::default(); + config.network = Network::Regtest; + let mut builder = NodeBuilder::from_config(config); + builder.set_chain_source_esplora("http://127.0.0.1:1".to_string(), None); + builder.set_entropy_seed_bytes([42u8; 64]); + builder.set_log_facade_logger(); + let store: Arc = Arc::new(InMemoryStore::new()); + let node = builder.build_with_store(store).unwrap(); + let account = + OnchainWalletAccount { address_type: AddressType::NativeSegwit, account_index: 1 }; + let xpub = node + .export_onchain_wallet_account_xpub(account.address_type, account.account_index) + .unwrap(); + node.add_onchain_wallet_account(account.address_type, account.account_index, xpub).unwrap(); + node.remove_onchain_wallet_account(account.address_type, account.account_index).unwrap(); + + let outcome = apply_additional_sync_results( + vec![(account, Ok(BdkUpdate::default()))], + &node.wallet, + &node.node_metrics, + &node.logger, + ); + + assert!(!outcome.any_applied); + assert!(outcome.events.is_empty()); + assert_eq!(outcome.error, None); + assert_eq!( + node.remove_onchain_wallet_account(account.address_type, account.account_index), + Err(Error::OnchainWalletAccountNotRegistered) + ); + } } // Process BDK wallet events and emit corresponding ldk-node events via the event queue. diff --git a/src/config.rs b/src/config.rs index 6d8ab2c93..bff05e0e1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,6 +50,9 @@ pub(crate) const DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS: u64 = 10; // The default Electrum connection/read timeout we're using. pub(crate) const DEFAULT_ELECTRUM_CONNECTION_TIMEOUT_SECS: u64 = 10; +// The default number of scripts queried in each Electrum batch. +pub(crate) const BDK_ELECTRUM_CLIENT_BATCH_SIZE: usize = 5; + // The 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold // number of derivation indexes after which BDK stops looking for new scripts belonging to the wallet. pub(crate) const BDK_CLIENT_STOP_GAP: usize = 20; @@ -156,9 +159,8 @@ impl AddressType { /// /// Account `0` is the main account ([`Config::address_type`] / /// [`crate::Node::set_primary_address_type`]). Accounts `1+` are derived accounts -/// registered at runtime via [`crate::Node::add_onchain_wallet_account`]. -/// -/// Derived-account registration is not persisted across restarts. +/// loaded from [`Config::onchain_wallet_accounts`] or registered at runtime via +/// [`crate::Node::add_onchain_wallet_account`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct OnchainWalletAccount { /// The script/address type of this wallet account. @@ -191,6 +193,24 @@ impl OnchainWalletAccount { } } +/// Configures a derived on-chain wallet account to load when the node is built. +/// +/// The xpub must match the account derived from the node's seed. Account `0` is reserved for +/// [`Config::address_type`] and [`Config::address_types_to_monitor`]. +/// Esplora and Electrum recover confirmed history with a full scan. Bitcoind cannot recover +/// confirmed transactions from before a brand-new account's first load. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OnchainWalletAccountConfig { + /// The script/address type of this wallet account. + pub address_type: AddressType, + /// BIP44 account index between `1` and [`MAX_ONCHAIN_WALLET_ACCOUNT_INDEX`], inclusive. + /// + /// [`MAX_ONCHAIN_WALLET_ACCOUNT_INDEX`]: crate::config::MAX_ONCHAIN_WALLET_ACCOUNT_INDEX + pub account_index: u32, + /// Account-level extended public key derived from the node's seed. + pub xpub: String, +} + impl Default for AddressType { fn default() -> Self { AddressType::NativeSegwit @@ -237,28 +257,28 @@ impl Config { } } -/// Runtime address-type configuration, initialized from [`Config`] and updated at runtime. -/// -/// Derived accounts are tracked separately and are not restored from [`Config`]. +/// Runtime wallet configuration, initialized from [`Config`] and updated at runtime. #[derive(Debug, Clone)] pub(crate) struct AddressTypeRuntimeConfig { /// The primary address type for account 0 (new addresses and change outputs). pub(crate) primary: AddressType, /// Additional account-0 address types to monitor for existing funds. pub(crate) monitored: Vec, - /// Runtime-registered derived accounts (`account_index >= 1`). Not persisted. + /// Loaded derived accounts (`account_index >= 1`). pub(crate) derived_accounts: Vec, } impl AddressTypeRuntimeConfig { - pub(crate) fn from_config(config: &Config) -> Self { + pub(crate) fn from_config( + config: &Config, derived_accounts: Vec, + ) -> Self { let monitored = config .address_types_to_monitor .iter() .copied() .filter(|&at| at != config.address_type) .collect(); - Self { primary: config.address_type, monitored, derived_accounts: Vec::new() } + Self { primary: config.address_type, monitored, derived_accounts } } /// Returns the additional account-0 address types to monitor, deduplicating. @@ -390,12 +410,14 @@ pub struct Config { /// [`BalanceDetails::spendable_onchain_balance_sats`]: crate::BalanceDetails::spendable_onchain_balance_sats pub include_untrusted_pending_in_spendable: bool, /// The address type for the primary (account `0`) on-chain wallet. Default is `NativeSegwit`. - /// - /// Derived accounts are registered at runtime via [`crate::Node::add_onchain_wallet_account`] - /// and are never configured here. pub address_type: AddressType, /// Additional account-`0` address types to monitor for existing funds. pub address_types_to_monitor: Vec, + /// Derived on-chain wallet accounts to load when the node is built. + /// + /// Registrations are validated against the node's seed. Runtime additions and removals do not + /// mutate this list or persist registration changes. + pub onchain_wallet_accounts: Vec, } impl Default for Config { @@ -415,6 +437,7 @@ impl Default for Config { include_untrusted_pending_in_spendable: false, address_type: AddressType::default(), address_types_to_monitor: Vec::new(), + onchain_wallet_accounts: Vec::new(), } } } @@ -823,6 +846,16 @@ pub struct ElectrumSyncConfig { /// warning. Note that two TCP connections are opened at node start (on-chain and Lightning /// sync), so budget accordingly when tuning this value. Defaults to 10 seconds. pub connection_timeout_secs: u64, + /// Batch size for Electrum full scans of non-primary wallets. + /// + /// This applies to monitored account-`0` address types and derived accounts. It does not affect + /// the primary wallet or incremental sync. Values below `1` are treated as `1`. Defaults to `5`. + pub additional_wallet_full_scan_batch_size: u32, + /// Stop gap for Electrum full scans of non-primary wallets. + /// + /// This applies to monitored account-`0` address types and derived accounts. It does not affect + /// the primary wallet. Values below `1` are treated as `1`. Defaults to `20`. + pub additional_wallet_full_scan_stop_gap: u32, } impl Default for ElectrumSyncConfig { @@ -830,6 +863,8 @@ impl Default for ElectrumSyncConfig { Self { background_sync_config: Some(BackgroundSyncConfig::default()), connection_timeout_secs: DEFAULT_ELECTRUM_CONNECTION_TIMEOUT_SECS, + additional_wallet_full_scan_batch_size: BDK_ELECTRUM_CLIENT_BATCH_SIZE as u32, + additional_wallet_full_scan_stop_gap: BDK_CLIENT_STOP_GAP as u32, } } } diff --git a/src/error.rs b/src/error.rs index 7b516559b..c285162ca 100644 --- a/src/error.rs +++ b/src/error.rs @@ -152,7 +152,9 @@ pub enum Error { AddressTypeIsPrimary, /// The address type is not currently being monitored. AddressTypeNotMonitored, - /// The given seed bytes have an invalid length. + /// The derived on-chain wallet account is not currently registered. + OnchainWalletAccountNotRegistered, + /// The seed bytes or a seed-derived wallet account are invalid. InvalidSeedBytes, } @@ -256,8 +258,11 @@ impl fmt::Display for Error { Self::AddressTypeNotMonitored => { write!(f, "The address type is not currently being monitored.") }, + Self::OnchainWalletAccountNotRegistered => { + write!(f, "The on-chain wallet account is not currently registered.") + }, Self::InvalidSeedBytes => { - write!(f, "The given seed bytes have an invalid length.") + write!(f, "The seed bytes or seed-derived wallet account are invalid.") }, } } diff --git a/src/lib.rs b/src/lib.rs index 803aa73cb..8cf494ac4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -125,7 +125,7 @@ use config::{ default_user_config, may_announce_channel, AsyncPaymentsRole, BackgroundSyncConfig, ChannelConfig, Config, NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, }; -pub use config::{AddressType, OnchainWalletAccount}; +pub use config::{AddressType, OnchainWalletAccount, OnchainWalletAccountConfig}; use connection::ConnectionManager; pub use error::Error as NodeError; use error::Error; @@ -2047,9 +2047,8 @@ impl Node { /// Retrieves an overview of all known balances. /// - /// On-chain totals include derived accounts currently registered via - /// [`Node::add_onchain_wallet_account`]. Derived funds are omitted until the app re-registers the - /// account after a restart. + /// On-chain totals include all currently loaded derived accounts. Funds from unloaded accounts + /// are omitted until the account is loaded from configuration or registered at runtime. pub fn list_balances(&self) -> BalanceDetails { let cur_anchor_reserve_sats = total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); @@ -2219,8 +2218,8 @@ impl Node { /// Exports the account-level xpub for a derived (or main) on-chain wallet account. /// - /// Persist `{ address_type, account_index, xpub }` externally and re-register derived - /// accounts via [`Node::add_onchain_wallet_account`] after every node build. + /// Store the returned xpub in [`Config::onchain_wallet_accounts`] to load a derived account on + /// future builds, or register it at runtime with [`Node::add_onchain_wallet_account`]. pub fn export_onchain_wallet_account_xpub( &self, address_type: AddressType, account_index: u32, ) -> Result { @@ -2234,7 +2233,8 @@ impl Node { /// rejected with [`Error::InvalidQuantity`]. A mismatched xpub returns /// [`Error::InvalidSeedBytes`]. /// - /// Not persisted across restarts; re-add after each build. BDK data remains persisted. + /// Runtime registration is not persisted across restarts. BDK data remains persisted; use + /// [`Config::onchain_wallet_accounts`] to load the account on each build. /// /// Registered funds participate in normal wallet coin selection and signing. Change remains on /// the configured primary account-`0` wallet. @@ -2249,6 +2249,18 @@ impl Node { self.wallet.add_onchain_wallet_account(address_type, account_index, &xpub) } + /// Unloads a derived on-chain wallet account (`account_index >= 1`). + /// + /// Persisted BDK data is retained, so re-registering the account recovers its state on the next + /// sync. Accounts listed in [`Config::onchain_wallet_accounts`] are loaded again on restart. + /// Account `0` and out-of-range indexes return [`Error::InvalidQuantity`]; an unloaded account + /// returns [`Error::OnchainWalletAccountNotRegistered`]. + pub fn remove_onchain_wallet_account( + &self, address_type: AddressType, account_index: u32, + ) -> Result<(), Error> { + self.wallet.remove_onchain_wallet_account(address_type, account_index) + } + /// Retrieves the on-chain balance for a specific wallet account. /// /// Funds in a registered account are available to normal wallet payments, whose change is sent to @@ -2263,10 +2275,6 @@ impl Node { } /// Returns all currently loaded on-chain wallet accounts, including derived accounts. - /// - /// Derived accounts must be re-registered after restart via - /// [`Node::add_onchain_wallet_account`]. Until then, they are absent from this list and from - /// aggregate balances. pub fn list_onchain_wallet_accounts(&self) -> Vec { self.wallet.get_loaded_onchain_wallet_accounts() } diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index 8a11b2c0a..faa1e534d 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -70,8 +70,7 @@ impl From for bdk_wallet::KeychainKind { /// Metadata for an address derived by the on-chain wallet. /// /// The `index` is the BIP32 child index within `keychain`. For receive addresses generated by -/// [`OnchainPayment::new_address_info`] and [`OnchainPayment::new_address_info_for_type`], -/// `keychain` is expected to be [`KeychainKind::External`]. +/// `new_address_info` methods, `keychain` is expected to be [`KeychainKind::External`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AddressInfo { /// Child index of this address within its keychain. @@ -220,6 +219,20 @@ impl OnchainPayment { .map(Into::into) } + /// Derive address metadata for a wallet account without advancing its cursor. + pub fn address_info_for_account_at_index( + &self, address_type: AddressType, account_index: u32, keychain: KeychainKind, index: u32, + ) -> Result { + if !*self.is_running.read().unwrap() { + return Err(Error::NotRunning); + } + + let wallet_account = OnchainWalletAccount { address_type, account_index }; + self.wallet + .get_address_info_for_account_at_index(wallet_account, keychain, index) + .map(Into::into) + } + /// Derive address metadata for a contiguous account-`0` range without advancing any wallet /// cursor. /// @@ -237,6 +250,23 @@ impl OnchainPayment { .map(|infos| infos.into_iter().map(Into::into).collect()) } + /// Derive address metadata for a contiguous wallet-account range without advancing its cursor. + /// + /// Batch requests are capped at 10,000 addresses per call. + pub fn address_infos_for_account( + &self, address_type: AddressType, account_index: u32, keychain: KeychainKind, + start_index: u32, count: u32, + ) -> Result, Error> { + if !*self.is_running.read().unwrap() { + return Err(Error::NotRunning); + } + + let wallet_account = OnchainWalletAccount { address_type, account_index }; + self.wallet + .get_address_infos_for_account(wallet_account, keychain, start_index, count) + .map(|infos| infos.into_iter().map(Into::into).collect()) + } + /// Reveal external receive addresses for account-`0` `address_type` up to and including /// `index`. /// diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index b21aa6aef..d5388dfa1 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -604,7 +604,7 @@ impl Wallet { /// Registers a derived on-chain wallet account (`account_index >= 1`). /// - /// Not persisted across restarts; re-add after each build. Idempotent for the same xpub. + /// Runtime registration is not persisted. Idempotent for the same xpub. pub(crate) fn add_onchain_wallet_account( &self, address_type: AddressType, account_index: u32, xpub: &str, ) -> Result<(), Error> { @@ -689,6 +689,57 @@ impl Wallet { Ok(()) } + /// Unloads a derived on-chain wallet account while retaining its persisted state. + pub(crate) fn remove_onchain_wallet_account( + &self, address_type: AddressType, account_index: u32, + ) -> Result<(), Error> { + if account_index == 0 || account_index > crate::config::MAX_ONCHAIN_WALLET_ACCOUNT_INDEX { + return Err(Error::InvalidQuantity); + } + + let wallet_account = OnchainWalletAccount { address_type, account_index }; + let _op = self.operation_lock.lock().unwrap(); + if !self + .address_type_runtime_config + .read() + .unwrap() + .derived_accounts + .contains(&wallet_account) + { + return Err(Error::OnchainWalletAccountNotRegistered); + } + + let removed = { + let mut aggregate = self.inner.lock().unwrap(); + match aggregate.remove_wallet(wallet_account) { + Ok(()) => true, + Err(bdk_wallet_aggregate::Error::WalletNotFound) => false, + Err(e) => { + log_error!( + self.logger, + "Failed to remove derived account {:?}: {}", + wallet_account, + e + ); + return Err(Error::WalletOperationFailed); + }, + } + }; + + self.address_type_runtime_config + .write() + .unwrap() + .derived_accounts + .retain(|account| *account != wallet_account); + self.synced_derived_accounts.lock().unwrap().remove(&wallet_account); + if removed { + self.account_generation.fetch_add(1, Ordering::AcqRel); + } + + log_info!(self.logger, "Removed derived on-chain wallet account {:?}", wallet_account); + Ok(()) + } + /// Callers are responsible for calling `update_payment_store_for_all_transactions` /// after all updates have been applied. pub(crate) fn apply_update( @@ -712,10 +763,14 @@ impl Wallet { /// after all updates have been applied. pub(crate) fn apply_update_for_wallet_account( &self, wallet_account: OnchainWalletAccount, update: impl Into, - ) -> Result, Error> { + ) -> Result>, Error> { + let _op = self.operation_lock.lock().unwrap(); let mut locked_wallet = self.inner.lock().unwrap(); + if locked_wallet.wallet(&wallet_account).is_none() { + return Ok(None); + } match locked_wallet.apply_update_to_wallet(wallet_account, update) { - Ok((events, _txids)) => Ok(events), + Ok((events, _txids)) => Ok(Some(events)), Err(e) => { log_error!( self.logger, @@ -1099,16 +1154,25 @@ impl Wallet { pub(crate) fn get_address_info_for_type_at_index( &self, address_type: AddressType, keychain: KeychainKind, index: u32, + ) -> Result { + self.get_address_info_for_account_at_index( + OnchainWalletAccount::account_zero(address_type), + keychain, + index, + ) + } + + pub(crate) fn get_address_info_for_account_at_index( + &self, wallet_account: OnchainWalletAccount, keychain: KeychainKind, index: u32, ) -> Result { validate_derivation_index(index)?; - let wallet_account = OnchainWalletAccount::account_zero(address_type); let locked_wallet = self.inner.lock().unwrap(); locked_wallet.address_info_for(&wallet_account, keychain.into(), index).map_err(|e| { log_error!( self.logger, - "Failed to get address info for type {:?} keychain {:?} at index {}: {}", - address_type, + "Failed to get address info for account {:?} keychain {:?} at index {}: {}", + wallet_account, keychain, index, e @@ -1119,18 +1183,29 @@ impl Wallet { pub(crate) fn get_address_infos_for_type( &self, address_type: AddressType, keychain: KeychainKind, start_index: u32, count: u32, + ) -> Result, Error> { + self.get_address_infos_for_account( + OnchainWalletAccount::account_zero(address_type), + keychain, + start_index, + count, + ) + } + + pub(crate) fn get_address_infos_for_account( + &self, wallet_account: OnchainWalletAccount, keychain: KeychainKind, start_index: u32, + count: u32, ) -> Result, Error> { validate_derivation_range(start_index, count)?; - let wallet_account = OnchainWalletAccount::account_zero(address_type); let locked_wallet = self.inner.lock().unwrap(); locked_wallet .address_infos_for(&wallet_account, keychain.into(), start_index, count) .map_err(|e| { log_error!( self.logger, - "Failed to get address infos for type {:?} keychain {:?} at indexes {}..{}: {}", - address_type, + "Failed to get address infos for account {:?} keychain {:?} at indexes {}..{}: {}", + wallet_account, keychain, start_index, start_index.saturating_add(count), diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 9c0393430..f368cab78 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -3305,8 +3305,11 @@ async fn electrum_connection_timeout_config() { let electrum_url = format!("tcp://{}", electrsd.electrum_url); let config = random_config(false); setup_builder!(builder, config.node_config); - let sync_config = - ElectrumSyncConfig { background_sync_config: None, connection_timeout_secs: 20 }; + let sync_config = ElectrumSyncConfig { + background_sync_config: None, + connection_timeout_secs: 20, + ..Default::default() + }; builder.set_chain_source_electrum(electrum_url, Some(sync_config)); let node = builder.build().unwrap(); node.start().unwrap(); @@ -3323,8 +3326,11 @@ async fn electrum_connection_timeout_zero_disables() { let electrum_url = format!("tcp://{}", electrsd.electrum_url); let config = random_config(false); setup_builder!(builder, config.node_config); - let sync_config = - ElectrumSyncConfig { background_sync_config: None, connection_timeout_secs: 0 }; + let sync_config = ElectrumSyncConfig { + background_sync_config: None, + connection_timeout_secs: 0, + ..Default::default() + }; builder.set_chain_source_electrum(electrum_url, Some(sync_config)); let node = builder.build().unwrap(); node.start().unwrap(); @@ -3340,8 +3346,11 @@ async fn electrum_connection_timeout_above_max_is_capped() { let electrum_url = format!("tcp://{}", electrsd.electrum_url); let config = random_config(false); setup_builder!(builder, config.node_config); - let sync_config = - ElectrumSyncConfig { background_sync_config: None, connection_timeout_secs: 300 }; + let sync_config = ElectrumSyncConfig { + background_sync_config: None, + connection_timeout_secs: 300, + ..Default::default() + }; builder.set_chain_source_electrum(electrum_url, Some(sync_config)); let node = builder.build().unwrap(); node.start().unwrap(); diff --git a/tests/multi_address_types_tests.rs b/tests/multi_address_types_tests.rs index 466db457c..08b21e5b5 100644 --- a/tests/multi_address_types_tests.rs +++ b/tests/multi_address_types_tests.rs @@ -3562,9 +3562,9 @@ mod derived_accounts { use bitcoin::{Address, Amount, CompressedPublicKey, FeeRate, Network}; use electrsd::corepc_node::{Conf as BitcoindConf, Node as BitcoinD}; use electrum_client::ElectrumApi; - use ldk_node::config::AddressType; + use ldk_node::config::{AddressType, ElectrumSyncConfig, OnchainWalletAccountConfig}; use ldk_node::payment::KeychainKind; - use ldk_node::NodeError; + use ldk_node::{Builder, NodeError}; use crate::common::{ distribute_funds_unconfirmed, generate_blocks_and_wait, invalidate_blocks, open_channel, @@ -3753,6 +3753,183 @@ mod derived_accounts { node2.stop().unwrap(); } + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_config_load_and_runtime_remove_preserve_derived_account_state() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let mut config = node_config(AddressType::NativeSegwit, vec![]); + config.store_type = crate::common::TestStoreType::Sqlite; + let storage_path = config.node_config.storage_dir_path.clone(); + let node = setup_node(&chain_source, config, None); + let seed = read_node_seed(&node); + let xpub = node.export_onchain_wallet_account_xpub(AddressType::Taproot, 1).unwrap(); + node.add_onchain_wallet_account(AddressType::Taproot, 1, xpub.clone()).unwrap(); + let address = + node.onchain_payment().new_address_for_account(AddressType::Taproot, 1).unwrap(); + fund_and_sync(&bitcoind, &electrsd, &node, address, 120_000).await; + let balance_before = node + .get_balance_for_onchain_wallet_account(AddressType::Taproot, 1) + .unwrap() + .total_sats; + node.stop().unwrap(); + drop(node); + + let mut config = node_config(AddressType::NativeSegwit, vec![]); + config.store_type = crate::common::TestStoreType::Sqlite; + config.node_config.storage_dir_path = storage_path; + config.node_config.onchain_wallet_accounts = vec![OnchainWalletAccountConfig { + address_type: AddressType::Taproot, + account_index: 1, + xpub: xpub.clone(), + }]; + let node = setup_node(&chain_source, config, Some(seed.to_vec())); + + assert!(node.list_onchain_wallet_accounts().iter().any(|account| { + account.address_type == AddressType::Taproot && account.account_index == 1 + })); + node.sync_wallets().unwrap(); + let loaded_balance = node + .get_balance_for_onchain_wallet_account(AddressType::Taproot, 1) + .unwrap() + .total_sats; + assert_eq!(loaded_balance, balance_before); + + node.remove_onchain_wallet_account(AddressType::Taproot, 1).unwrap(); + assert!(!node.list_onchain_wallet_accounts().iter().any(|account| { + account.address_type == AddressType::Taproot && account.account_index == 1 + })); + assert_eq!(node.list_balances().total_onchain_balance_sats, 0); + assert_eq!( + node.remove_onchain_wallet_account(AddressType::Taproot, 1), + Err(NodeError::OnchainWalletAccountNotRegistered) + ); + assert_eq!( + node.remove_onchain_wallet_account(AddressType::Taproot, 0), + Err(NodeError::InvalidQuantity) + ); + + node.add_onchain_wallet_account(AddressType::Taproot, 1, xpub).unwrap(); + assert_eq!( + node.get_balance_for_onchain_wallet_account(AddressType::Taproot, 1) + .unwrap() + .total_sats, + balance_before + ); + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_derived_account_address_lookup_by_index_and_range() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let node = setup_node(&chain_source, node_config(AddressType::NativeSegwit, vec![]), None); + let xpub = node.export_onchain_wallet_account_xpub(AddressType::NativeSegwit, 2).unwrap(); + node.add_onchain_wallet_account(AddressType::NativeSegwit, 2, xpub.clone()).unwrap(); + + let indexed = node + .onchain_payment() + .address_info_for_account_at_index( + AddressType::NativeSegwit, + 2, + KeychainKind::External, + 7, + ) + .unwrap(); + let range = node + .onchain_payment() + .address_infos_for_account(AddressType::NativeSegwit, 2, KeychainKind::External, 6, 3) + .unwrap(); + assert_eq!(range.iter().map(|info| info.index).collect::>(), vec![6, 7, 8]); + assert_eq!(range[1], indexed); + + let account_xpub = Xpub::from_str(&xpub).unwrap(); + let child = account_xpub + .derive_pub( + &Secp256k1::new(), + &[ + ChildNumber::from_normal_idx(0).unwrap(), + ChildNumber::from_normal_idx(7).unwrap(), + ], + ) + .unwrap(); + let expected = Address::p2wpkh(&CompressedPublicKey(child.public_key), Network::Regtest); + assert_eq!(indexed.address, expected); + assert_eq!(indexed.keychain, KeychainKind::External); + let internal = node + .onchain_payment() + .address_info_for_account_at_index( + AddressType::NativeSegwit, + 2, + KeychainKind::Internal, + 7, + ) + .unwrap(); + assert_ne!(internal.address, indexed.address); + assert_eq!(internal.keychain, KeychainKind::Internal); + + let next = node + .onchain_payment() + .new_address_info_for_account(AddressType::NativeSegwit, 2) + .unwrap(); + assert_eq!(next.index, 0); + assert_eq!(next.keychain, KeychainKind::External); + node.stop().unwrap(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_electrum_configured_account_full_scan_uses_custom_gap_and_batch_size() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Electrum(&electrsd); + let mut initial_config = node_config(AddressType::NativeSegwit, vec![]); + initial_config.store_type = crate::common::TestStoreType::Sqlite; + let node = setup_node(&chain_source, initial_config, None); + + let xpub = node.export_onchain_wallet_account_xpub(AddressType::NativeSegwit, 1).unwrap(); + let address = native_segwit_receive_address_from_xpub(&xpub, 30); + let mut restart_config = node.config(); + node.stop().unwrap(); + drop(node); + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address], + Amount::from_sat(100_000), + ) + .await; + + restart_config.onchain_wallet_accounts = vec![OnchainWalletAccountConfig { + address_type: AddressType::NativeSegwit, + account_index: 1, + xpub, + }]; + crate::common::setup_builder!(builder, restart_config); + let electrum_url = format!("tcp://{}", electrsd.electrum_url); + let sync_config = ElectrumSyncConfig { + background_sync_config: None, + additional_wallet_full_scan_batch_size: 100, + additional_wallet_full_scan_stop_gap: 1_000, + ..Default::default() + }; + builder.set_chain_source_electrum(electrum_url, Some(sync_config)); + builder.set_log_facade_logger(); + let node = builder.build().unwrap(); + node.start().unwrap(); + node.sync_wallets().unwrap(); + assert!(node.list_onchain_wallet_accounts().iter().any(|account| { + account.address_type == AddressType::NativeSegwit && account.account_index == 1 + })); + assert_eq!( + node.get_balance_for_onchain_wallet_account(AddressType::NativeSegwit, 1) + .unwrap() + .total_sats, + 100_000 + ); + + node.stop().unwrap(); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_derived_account_rejects_wrong_seed_xpub_and_account_zero() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd();