diff --git a/.gitignore b/.gitignore index df16119fd..9d8b73d65 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,3 @@ swift.swiftdoc .cursor .claude *.local.* -CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..65a46ab93 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,232 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +LDK Node is a ready-to-go Lightning node library built using LDK (Lightning Development Kit) and BDK (Bitcoin Development Kit). It provides a high-level interface for running a Lightning node with an integrated on-chain wallet. + +## Development Commands + +### Building +```bash +# Build the project +cargo build + +# Build with release optimizations +cargo build --release + +# Build with size optimizations +cargo build --profile=release-smaller +``` + +### Testing +```bash +# Run all tests +cargo test + +# Run a specific test +cargo test test_name + +# Run tests with specific features +cargo test --features "uniffi" + +# Integration tests with specific backends +cargo test --cfg cln_test # Core Lightning tests +cargo test --cfg lnd_test # LND tests +cargo test --cfg vss_test # VSS (Versioned Storage Service) tests +``` + +### Code Quality +```bash +# Format code +cargo fmt + +# Check formatting without modifying +cargo fmt --check + +# Run clippy for linting +cargo clippy + +# Run clippy and fix issues +cargo clippy --fix +``` + +### Language Bindings +```bash +# Generate Kotlin bindings +./scripts/uniffi_bindgen_generate_kotlin.sh + +# Generate Android bindings +./scripts/uniffi_bindgen_generate_kotlin_android.sh + +# Generate Python bindings +./scripts/uniffi_bindgen_generate_python.sh + +# Generate Swift bindings +./scripts/uniffi_bindgen_generate_swift.sh +``` + +## Architecture + +### Core Components + +1. **Node** (`src/lib.rs`): The main entry point and primary abstraction. Manages the Lightning node's lifecycle and provides high-level operations like opening channels, sending payments, and handling events. + +2. **Builder** (`src/builder.rs`): Configures and constructs a Node instance with customizable settings for network, chain source, storage backend, and entropy source. + +3. **Payment System** (`src/payment/`): + - `bolt11.rs`: BOLT-11 invoice payments + - `bolt12.rs`: BOLT-12 offer payments + - `spontaneous.rs`: Spontaneous payments without invoices + - `onchain.rs`: On-chain Bitcoin transactions + - `unified_qr.rs`: Unified QR code generation for payments + +4. **Storage Backends** (`src/io/`): + - `sqlite_store/`: SQLite-based persistent storage + - `vss_store.rs`: Versioned Storage Service for remote backups + - FilesystemStore: File-based storage (via lightning-persister) + +5. **Chain Integration** (`src/chain/`): + - `bitcoind_rpc.rs`: Bitcoin Core RPC interface + - `electrum.rs`: Electrum server integration + - `esplora.rs`: Esplora block explorer API + +6. **Event System** (`src/event.rs`): Asynchronous event handling for channel updates, payments, and other node activities. + +### Key Design Patterns + +- **Modular Chain Sources**: Supports multiple chain data sources (Bitcoin Core, Electrum, Esplora) through a unified interface +- **Pluggable Storage**: Storage backend abstraction allows SQLite, filesystem, or custom implementations +- **Event-Driven Architecture**: Core operations emit events that must be handled by the application +- **Builder Pattern**: Node configuration uses a builder for flexible setup + +### Dependencies Structure + +The project heavily relies on the Lightning Development Kit ecosystem: +- `lightning-*`: Core LDK functionality (channel management, routing, invoices) +- `bdk_*`: Bitcoin wallet functionality +- `uniffi`: Multi-language bindings generation + +### Critical Files + +- `src/lib.rs`: Node struct and primary API +- `src/builder.rs`: Node configuration and initialization +- `src/payment/mod.rs`: Payment handling coordination +- `src/io/sqlite_store/mod.rs`: Primary storage implementation +- `bindings/ldk_node.udl`: UniFFI interface definition for language bindings + +--- +## PERSONA +You are an extremely strict senior Rust systems engineer with 15+ years shipping production cryptographic and distributed systems (e.g. HSM-backed consensus protocols, libp2p meshes, zk-proof coordinators, TLS implementations, hypercore, pubky, dht, blockchain nodes). + +Your job is not just to write or review code — it is to deliver code that would pass a full Trail of Bits + Rust unsafe + Jepsen-level audit on the first try. + +Follow this exact multi-stage process and never skip or summarize any stage: + +Stage 1 – Threat Model & Architecture Review +- Explicitly write a concise threat model (adversaries, trust boundaries, failure modes). +- Check if the architecture is overly complex. Suggest simpler, proven designs if they exist (cite papers or real systems). +- Flag any violation of "pit of success" Rust design (fighting the borrow checker, over-use of Rc/RefCell, unnecessary async, etc.). + +Stage 2 – Cryptography Audit (zero tolerance) +- Constant-time execution +- Side-channel resistance (timing, cache, branching) +- Misuse-resistant API design (libsodium / rustls style) +- Nonce/IV uniqueness & randomness +- Key management, rotation, separation +- Authenticated encryption mandatory +- No banned primitives (MD5, SHA1, RSA-PKCS1v1_5, ECDSA deterministic nonce, etc.) +- Every crypto operation must be justified and cited + +Stage 3 – Rust Safety & Correctness Audit +- Every `unsafe` block justified with miri-proof invariants +- Send/Sync, Pin, lifetime, variance, interior mutability checks +- Panic safety, drop order, leak freedom +- Cancellation safety for async +- All public APIs have `#![forbid(unsafe_code)]` where possible + +Stage 4 – Testing Requirements (non-negotiable) +You must generate and show: +- 100% line and branch coverage (you will estimate and require missing tests) +- Property-based tests with proptest or proptest for all non-trivial logic +- Fuzz targets (afl/libfuzzer) for all parsers and crypto boundaries +- Integration tests that spawn multiple nodes and inject partitions (use loom or tokyo for concurrency, manual partitions for distributed) +- All tests must be shown in the final output and marked as passing (you will mentally execute or describe expected outcome) + +Stage 5 – Documentation & Commenting (audit-ready) +- Every public item has a top-level doc comment with: + - Purpose + - Safety preconditions + - Threat model considerations + - Examples (must compile with doctest) +- Every non-obvious private function has a short comment +- crate-level README with build instructions, threat model, and fuzzing guide +- All documentation must be shown and marked as doctests passing + +Stage 6 – Build & CI Verification +- Provide exact `Cargo.toml` changes needed +- Add required features/flags (e.g. `cargo miri test`, `cargo fuzz`, `cargo nextest`, etc.) +- Explicitly state that `cargo build --all-targets --locked` and `cargo test --all-targets` pass with no warnings + +Stage 7 – Final Structured Output +Only after completing all stages above, output in this exact order: + +1. Threat model & architecture improvements (or "none required") +2. Critical issues found (or "none") +3. Full refactored Cargo.toml +4. Full refactored source files (complete, copy-paste ready) +5. All new tests (property, fuzz, integration) shown in full +6. Documentation excerpts proving completeness +7. Final verification checklist with ✅ or ❌ for: + - Builds cleanly + - All tests pass + - Zero unsafe without justification + - Zero crypto footguns + - Documentation complete and doctests pass + - Architecture is minimal and correct + +Never say "trust me" or "in practice this would pass". You must demonstrate everything above explicitly. +If anything is missing or cannot be verified, you must fix it before declaring success. + +--- +## RULES +- NEVER suggest manually adding @Serializable annotations to generated Kotlin bindings +- ALWAYS run `cargo fmt` before committing to ensure consistent code formatting +- ALWAYS move imports to the top of the file when applicable (no inline imports in functions) +- To regenerate ALL bindings (Swift, Kotlin, Python), use this command: + ```bash + RUSTFLAGS="--cfg no_download" cargo build && ./scripts/uniffi_bindgen_generate.sh && ./scripts/swift_create_xcframework_archive.sh && sh scripts/uniffi_bindgen_generate_kotlin_android.sh + ``` + +## Version Bumping Checklist +When bumping the version, ALWAYS update ALL of these files: +1. `Cargo.toml` - main crate version +2. `bindings/kotlin/ldk-node-android/gradle.properties` - Android libraryVersion +3. `bindings/kotlin/ldk-node-jvm/gradle.properties` - JVM libraryVersion +4. `bindings/python/pyproject.toml` - Python version +5. `Package.swift` - Swift tag (and checksum after building) +6. `CHANGELOG.md` - Add release notes section at top + +## CHANGELOG +- The Synonym fork maintains a SINGLE section at the top: `# X.X.X (Synonym Fork)` +- When bumping version, update the version in the existing heading (don't create new sections) +- All Synonym fork additions go under ONE `## Synonym Fork Additions` subsection +- New additions should be added at the TOP of the Synonym Fork Additions list +- Do NOT create separate sections for each rc version +- Use the CHANGELOG content as the GitHub release notes body + +## PR Release Workflow +- For PRs that bump version, ALWAYS create a release on the PR branch BEFORE merge +- Tag the last commit on the PR branch with the version from Cargo.toml (e.g., `v0.7.0-rc.6`) +- **CRITICAL: Before uploading `LDKNodeFFI.xcframework.zip`, ALWAYS verify the checksum matches `Package.swift`:** + ```bash + shasum -a 256 bindings/swift/LDKNodeFFI.xcframework.zip + # Compare output with the checksum value in Package.swift - they MUST match + ``` +- Create GitHub release with same name as the tag, upload `LDKNodeFFI.xcframework.zip` +- Add release link at the end of PR description (or as a comment if not your PR): + ``` + ### Release + - Release [vN.N.N](link_to_release) + ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a55abfe5..10a32d96e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ -# 0.7.0-rc.6 (Synonym Fork) +# 0.7.0-rc.7 (Synonym Fork) ## Synonym Fork Additions +- Added `claimable_on_close_sats` field to `ChannelDetails` struct. This field contains the + amount (in satoshis) that would be claimable if the channel were force-closed now, computed + from the channel monitor's `ClaimableOnChannelClose` balance. Returns `None` if no monitor + exists yet (pre-funding). This replaces the workaround of approximating the claimable amount + using `outbound_capacity_msat + counterparty_reserve`. - Added reactive event system for wallet monitoring without polling: - **Onchain Transaction Events** (fully implemented): - `OnchainTransactionReceived`: Emitted when a new unconfirmed transaction is diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 1291ed3d8..2f45e7a8f 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ldk-node" -version = "0.7.0-rc.6" +version = "0.7.0-rc.7" authors = ["Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Package.swift b/Package.swift index 550824a6d..baeae02e2 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.7.0-rc.6" -let checksum = "4ea23aedbf918a1c93539168f34e626cbe867c1d5e827b7b7fd0e84225970b91" +let tag = "v0.7.0-rc.7" +let checksum = "102ad31d567fdb176ba92ae4453ca67772383b95f4fa250951f1bdf4228da45e" let url = "https://github.com/synonymdev/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" let package = Package( diff --git a/WARP.md b/WARP.md new file mode 120000 index 000000000..47dc3e3d8 --- /dev/null +++ b/WARP.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/bindings/kotlin/ldk-node-android/gradle.properties b/bindings/kotlin/ldk-node-android/gradle.properties index c8d14b96a..ededbded7 100644 --- a/bindings/kotlin/ldk-node-android/gradle.properties +++ b/bindings/kotlin/ldk-node-android/gradle.properties @@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official -libraryVersion=0.7.0-rc.6 +libraryVersion=0.7.0-rc.7 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 aa68863ca..67f288155 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 f11c119f8..32b23eb52 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 30cab16a8..93f57e7c3 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 7ed347b0b..7651a5c0a 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 @@ -8873,6 +8873,7 @@ object FfiConverterTypeChannelDetails: FfiConverterRustBuffer { FfiConverterULong.read(buf), FfiConverterOptionalULong.read(buf), FfiConverterTypeChannelConfig.read(buf), + FfiConverterOptionalULong.read(buf), ) } @@ -8907,7 +8908,8 @@ object FfiConverterTypeChannelDetails: FfiConverterRustBuffer { FfiConverterOptionalUShort.allocationSize(value.`forceCloseSpendDelay`) + FfiConverterULong.allocationSize(value.`inboundHtlcMinimumMsat`) + FfiConverterOptionalULong.allocationSize(value.`inboundHtlcMaximumMsat`) + - FfiConverterTypeChannelConfig.allocationSize(value.`config`) + FfiConverterTypeChannelConfig.allocationSize(value.`config`) + + FfiConverterOptionalULong.allocationSize(value.`claimableOnCloseSats`) ) override fun write(value: ChannelDetails, buf: ByteBuffer) { @@ -8942,6 +8944,7 @@ object FfiConverterTypeChannelDetails: FfiConverterRustBuffer { FfiConverterULong.write(value.`inboundHtlcMinimumMsat`, buf) FfiConverterOptionalULong.write(value.`inboundHtlcMaximumMsat`, buf) FfiConverterTypeChannelConfig.write(value.`config`, buf) + FfiConverterOptionalULong.write(value.`claimableOnCloseSats`, buf) } } 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 f3a2273c0..529c4a9fd 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 @@ -756,7 +756,8 @@ data class ChannelDetails ( val `forceCloseSpendDelay`: kotlin.UShort?, val `inboundHtlcMinimumMsat`: kotlin.ULong, val `inboundHtlcMaximumMsat`: kotlin.ULong?, - val `config`: ChannelConfig + val `config`: ChannelConfig, + val `claimableOnCloseSats`: kotlin.ULong? ) { companion object } diff --git a/bindings/kotlin/ldk-node-jvm/gradle.properties b/bindings/kotlin/ldk-node-jvm/gradle.properties index f50e6bac1..08d43fde2 100644 --- a/bindings/kotlin/ldk-node-jvm/gradle.properties +++ b/bindings/kotlin/ldk-node-jvm/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs=-Xmx1536m kotlin.code.style=official -libraryVersion=0.7.0-rc.6 +libraryVersion=0.7.0-rc.7 diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 04a5d56bb..f0122fa7c 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -678,6 +678,7 @@ dictionary ChannelDetails { u64 inbound_htlc_minimum_msat; u64? inbound_htlc_maximum_msat; ChannelConfig config; + u64? claimable_on_close_sats; }; dictionary PeerDetails { diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 1deb18204..cc5473978 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ldk_node" -version = "0.7.0-rc.6" +version = "0.7.0-rc.7" 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 f357f2ed0..4514f2eb8 100644 --- a/bindings/python/src/ldk_node/ldk_node.py +++ b/bindings/python/src/ldk_node/ldk_node.py @@ -7341,7 +7341,8 @@ class ChannelDetails: inbound_htlc_minimum_msat: "int" inbound_htlc_maximum_msat: "typing.Optional[int]" config: "ChannelConfig" - def __init__(self, *, channel_id: "ChannelId", counterparty_node_id: "PublicKey", funding_txo: "typing.Optional[OutPoint]", short_channel_id: "typing.Optional[int]", outbound_scid_alias: "typing.Optional[int]", inbound_scid_alias: "typing.Optional[int]", channel_value_sats: "int", unspendable_punishment_reserve: "typing.Optional[int]", user_channel_id: "UserChannelId", feerate_sat_per_1000_weight: "int", outbound_capacity_msat: "int", inbound_capacity_msat: "int", confirmations_required: "typing.Optional[int]", confirmations: "typing.Optional[int]", is_outbound: "bool", is_channel_ready: "bool", is_usable: "bool", is_announced: "bool", cltv_expiry_delta: "typing.Optional[int]", counterparty_unspendable_punishment_reserve: "int", counterparty_outbound_htlc_minimum_msat: "typing.Optional[int]", counterparty_outbound_htlc_maximum_msat: "typing.Optional[int]", counterparty_forwarding_info_fee_base_msat: "typing.Optional[int]", counterparty_forwarding_info_fee_proportional_millionths: "typing.Optional[int]", counterparty_forwarding_info_cltv_expiry_delta: "typing.Optional[int]", next_outbound_htlc_limit_msat: "int", next_outbound_htlc_minimum_msat: "int", force_close_spend_delay: "typing.Optional[int]", inbound_htlc_minimum_msat: "int", inbound_htlc_maximum_msat: "typing.Optional[int]", config: "ChannelConfig"): + claimable_on_close_sats: "typing.Optional[int]" + def __init__(self, *, channel_id: "ChannelId", counterparty_node_id: "PublicKey", funding_txo: "typing.Optional[OutPoint]", short_channel_id: "typing.Optional[int]", outbound_scid_alias: "typing.Optional[int]", inbound_scid_alias: "typing.Optional[int]", channel_value_sats: "int", unspendable_punishment_reserve: "typing.Optional[int]", user_channel_id: "UserChannelId", feerate_sat_per_1000_weight: "int", outbound_capacity_msat: "int", inbound_capacity_msat: "int", confirmations_required: "typing.Optional[int]", confirmations: "typing.Optional[int]", is_outbound: "bool", is_channel_ready: "bool", is_usable: "bool", is_announced: "bool", cltv_expiry_delta: "typing.Optional[int]", counterparty_unspendable_punishment_reserve: "int", counterparty_outbound_htlc_minimum_msat: "typing.Optional[int]", counterparty_outbound_htlc_maximum_msat: "typing.Optional[int]", counterparty_forwarding_info_fee_base_msat: "typing.Optional[int]", counterparty_forwarding_info_fee_proportional_millionths: "typing.Optional[int]", counterparty_forwarding_info_cltv_expiry_delta: "typing.Optional[int]", next_outbound_htlc_limit_msat: "int", next_outbound_htlc_minimum_msat: "int", force_close_spend_delay: "typing.Optional[int]", inbound_htlc_minimum_msat: "int", inbound_htlc_maximum_msat: "typing.Optional[int]", config: "ChannelConfig", claimable_on_close_sats: "typing.Optional[int]"): self.channel_id = channel_id self.counterparty_node_id = counterparty_node_id self.funding_txo = funding_txo @@ -7373,9 +7374,10 @@ def __init__(self, *, channel_id: "ChannelId", counterparty_node_id: "PublicKey" self.inbound_htlc_minimum_msat = inbound_htlc_minimum_msat self.inbound_htlc_maximum_msat = inbound_htlc_maximum_msat self.config = config + self.claimable_on_close_sats = claimable_on_close_sats def __str__(self): - return "ChannelDetails(channel_id={}, counterparty_node_id={}, funding_txo={}, short_channel_id={}, outbound_scid_alias={}, inbound_scid_alias={}, channel_value_sats={}, unspendable_punishment_reserve={}, user_channel_id={}, feerate_sat_per_1000_weight={}, outbound_capacity_msat={}, inbound_capacity_msat={}, confirmations_required={}, confirmations={}, is_outbound={}, is_channel_ready={}, is_usable={}, is_announced={}, cltv_expiry_delta={}, counterparty_unspendable_punishment_reserve={}, counterparty_outbound_htlc_minimum_msat={}, counterparty_outbound_htlc_maximum_msat={}, counterparty_forwarding_info_fee_base_msat={}, counterparty_forwarding_info_fee_proportional_millionths={}, counterparty_forwarding_info_cltv_expiry_delta={}, next_outbound_htlc_limit_msat={}, next_outbound_htlc_minimum_msat={}, force_close_spend_delay={}, inbound_htlc_minimum_msat={}, inbound_htlc_maximum_msat={}, config={})".format(self.channel_id, self.counterparty_node_id, self.funding_txo, self.short_channel_id, self.outbound_scid_alias, self.inbound_scid_alias, self.channel_value_sats, self.unspendable_punishment_reserve, self.user_channel_id, self.feerate_sat_per_1000_weight, self.outbound_capacity_msat, self.inbound_capacity_msat, self.confirmations_required, self.confirmations, self.is_outbound, self.is_channel_ready, self.is_usable, self.is_announced, self.cltv_expiry_delta, self.counterparty_unspendable_punishment_reserve, self.counterparty_outbound_htlc_minimum_msat, self.counterparty_outbound_htlc_maximum_msat, self.counterparty_forwarding_info_fee_base_msat, self.counterparty_forwarding_info_fee_proportional_millionths, self.counterparty_forwarding_info_cltv_expiry_delta, self.next_outbound_htlc_limit_msat, self.next_outbound_htlc_minimum_msat, self.force_close_spend_delay, self.inbound_htlc_minimum_msat, self.inbound_htlc_maximum_msat, self.config) + return "ChannelDetails(channel_id={}, counterparty_node_id={}, funding_txo={}, short_channel_id={}, outbound_scid_alias={}, inbound_scid_alias={}, channel_value_sats={}, unspendable_punishment_reserve={}, user_channel_id={}, feerate_sat_per_1000_weight={}, outbound_capacity_msat={}, inbound_capacity_msat={}, confirmations_required={}, confirmations={}, is_outbound={}, is_channel_ready={}, is_usable={}, is_announced={}, cltv_expiry_delta={}, counterparty_unspendable_punishment_reserve={}, counterparty_outbound_htlc_minimum_msat={}, counterparty_outbound_htlc_maximum_msat={}, counterparty_forwarding_info_fee_base_msat={}, counterparty_forwarding_info_fee_proportional_millionths={}, counterparty_forwarding_info_cltv_expiry_delta={}, next_outbound_htlc_limit_msat={}, next_outbound_htlc_minimum_msat={}, force_close_spend_delay={}, inbound_htlc_minimum_msat={}, inbound_htlc_maximum_msat={}, config={}, claimable_on_close_sats={})".format(self.channel_id, self.counterparty_node_id, self.funding_txo, self.short_channel_id, self.outbound_scid_alias, self.inbound_scid_alias, self.channel_value_sats, self.unspendable_punishment_reserve, self.user_channel_id, self.feerate_sat_per_1000_weight, self.outbound_capacity_msat, self.inbound_capacity_msat, self.confirmations_required, self.confirmations, self.is_outbound, self.is_channel_ready, self.is_usable, self.is_announced, self.cltv_expiry_delta, self.counterparty_unspendable_punishment_reserve, self.counterparty_outbound_htlc_minimum_msat, self.counterparty_outbound_htlc_maximum_msat, self.counterparty_forwarding_info_fee_base_msat, self.counterparty_forwarding_info_fee_proportional_millionths, self.counterparty_forwarding_info_cltv_expiry_delta, self.next_outbound_htlc_limit_msat, self.next_outbound_htlc_minimum_msat, self.force_close_spend_delay, self.inbound_htlc_minimum_msat, self.inbound_htlc_maximum_msat, self.config, self.claimable_on_close_sats) def __eq__(self, other): if self.channel_id != other.channel_id: @@ -7440,6 +7442,8 @@ def __eq__(self, other): return False if self.config != other.config: return False + if self.claimable_on_close_sats != other.claimable_on_close_sats: + return False return True class _UniffiConverterTypeChannelDetails(_UniffiConverterRustBuffer): @@ -7477,6 +7481,7 @@ def read(buf): inbound_htlc_minimum_msat=_UniffiConverterUInt64.read(buf), inbound_htlc_maximum_msat=_UniffiConverterOptionalUInt64.read(buf), config=_UniffiConverterTypeChannelConfig.read(buf), + claimable_on_close_sats=_UniffiConverterOptionalUInt64.read(buf), ) @staticmethod @@ -7512,6 +7517,7 @@ def check_lower(value): _UniffiConverterUInt64.check_lower(value.inbound_htlc_minimum_msat) _UniffiConverterOptionalUInt64.check_lower(value.inbound_htlc_maximum_msat) _UniffiConverterTypeChannelConfig.check_lower(value.config) + _UniffiConverterOptionalUInt64.check_lower(value.claimable_on_close_sats) @staticmethod def write(value, buf): @@ -7546,6 +7552,7 @@ def write(value, buf): _UniffiConverterUInt64.write(value.inbound_htlc_minimum_msat, buf) _UniffiConverterOptionalUInt64.write(value.inbound_htlc_maximum_msat, buf) _UniffiConverterTypeChannelConfig.write(value.config, buf) + _UniffiConverterOptionalUInt64.write(value.claimable_on_close_sats, buf) class ChannelInfo: diff --git a/bindings/swift/Sources/LDKNode/LDKNode.swift b/bindings/swift/Sources/LDKNode/LDKNode.swift index 3b0f56064..d4df72e36 100644 --- a/bindings/swift/Sources/LDKNode/LDKNode.swift +++ b/bindings/swift/Sources/LDKNode/LDKNode.swift @@ -4417,10 +4417,11 @@ public struct ChannelDetails { public var inboundHtlcMinimumMsat: UInt64 public var inboundHtlcMaximumMsat: UInt64? public var config: ChannelConfig + public var claimableOnCloseSats: UInt64? // Default memberwise initializers are never public by default, so we // declare one manually. - public init(channelId: ChannelId, counterpartyNodeId: PublicKey, fundingTxo: OutPoint?, shortChannelId: UInt64?, outboundScidAlias: UInt64?, inboundScidAlias: UInt64?, channelValueSats: UInt64, unspendablePunishmentReserve: UInt64?, userChannelId: UserChannelId, feerateSatPer1000Weight: UInt32, outboundCapacityMsat: UInt64, inboundCapacityMsat: UInt64, confirmationsRequired: UInt32?, confirmations: UInt32?, isOutbound: Bool, isChannelReady: Bool, isUsable: Bool, isAnnounced: Bool, cltvExpiryDelta: UInt16?, counterpartyUnspendablePunishmentReserve: UInt64, counterpartyOutboundHtlcMinimumMsat: UInt64?, counterpartyOutboundHtlcMaximumMsat: UInt64?, counterpartyForwardingInfoFeeBaseMsat: UInt32?, counterpartyForwardingInfoFeeProportionalMillionths: UInt32?, counterpartyForwardingInfoCltvExpiryDelta: UInt16?, nextOutboundHtlcLimitMsat: UInt64, nextOutboundHtlcMinimumMsat: UInt64, forceCloseSpendDelay: UInt16?, inboundHtlcMinimumMsat: UInt64, inboundHtlcMaximumMsat: UInt64?, config: ChannelConfig) { + public init(channelId: ChannelId, counterpartyNodeId: PublicKey, fundingTxo: OutPoint?, shortChannelId: UInt64?, outboundScidAlias: UInt64?, inboundScidAlias: UInt64?, channelValueSats: UInt64, unspendablePunishmentReserve: UInt64?, userChannelId: UserChannelId, feerateSatPer1000Weight: UInt32, outboundCapacityMsat: UInt64, inboundCapacityMsat: UInt64, confirmationsRequired: UInt32?, confirmations: UInt32?, isOutbound: Bool, isChannelReady: Bool, isUsable: Bool, isAnnounced: Bool, cltvExpiryDelta: UInt16?, counterpartyUnspendablePunishmentReserve: UInt64, counterpartyOutboundHtlcMinimumMsat: UInt64?, counterpartyOutboundHtlcMaximumMsat: UInt64?, counterpartyForwardingInfoFeeBaseMsat: UInt32?, counterpartyForwardingInfoFeeProportionalMillionths: UInt32?, counterpartyForwardingInfoCltvExpiryDelta: UInt16?, nextOutboundHtlcLimitMsat: UInt64, nextOutboundHtlcMinimumMsat: UInt64, forceCloseSpendDelay: UInt16?, inboundHtlcMinimumMsat: UInt64, inboundHtlcMaximumMsat: UInt64?, config: ChannelConfig, claimableOnCloseSats: UInt64?) { self.channelId = channelId self.counterpartyNodeId = counterpartyNodeId self.fundingTxo = fundingTxo @@ -4452,6 +4453,7 @@ public struct ChannelDetails { self.inboundHtlcMinimumMsat = inboundHtlcMinimumMsat self.inboundHtlcMaximumMsat = inboundHtlcMaximumMsat self.config = config + self.claimableOnCloseSats = claimableOnCloseSats } } @@ -4550,6 +4552,9 @@ extension ChannelDetails: Equatable, Hashable { if lhs.config != rhs.config { return false } + if lhs.claimableOnCloseSats != rhs.claimableOnCloseSats { + return false + } return true } @@ -4585,6 +4590,7 @@ extension ChannelDetails: Equatable, Hashable { hasher.combine(inboundHtlcMinimumMsat) hasher.combine(inboundHtlcMaximumMsat) hasher.combine(config) + hasher.combine(claimableOnCloseSats) } } @@ -4625,7 +4631,8 @@ public struct FfiConverterTypeChannelDetails: FfiConverterRustBuffer { forceCloseSpendDelay: FfiConverterOptionUInt16.read(from: &buf), inboundHtlcMinimumMsat: FfiConverterUInt64.read(from: &buf), inboundHtlcMaximumMsat: FfiConverterOptionUInt64.read(from: &buf), - config: FfiConverterTypeChannelConfig.read(from: &buf) + config: FfiConverterTypeChannelConfig.read(from: &buf), + claimableOnCloseSats: FfiConverterOptionUInt64.read(from: &buf) ) } @@ -4661,6 +4668,7 @@ public struct FfiConverterTypeChannelDetails: FfiConverterRustBuffer { FfiConverterUInt64.write(value.inboundHtlcMinimumMsat, into: &buf) FfiConverterOptionUInt64.write(value.inboundHtlcMaximumMsat, into: &buf) FfiConverterTypeChannelConfig.write(value.config, into: &buf) + FfiConverterOptionUInt64.write(value.claimableOnCloseSats, into: &buf) } } diff --git a/src/builder.rs b/src/builder.rs index d1abdc9bf..7e0b58f73 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -25,7 +25,6 @@ use lightning::io::Cursor; use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs}; use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler}; -use lightning::{log_info, log_trace}; use lightning::routing::gossip::NodeAlias; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ @@ -34,12 +33,13 @@ use lightning::routing::scoring::{ }; use lightning::sign::{EntropySource, InMemorySigner, NodeSigner}; use lightning::util::persist::{ - KVStore, KVStoreSync, CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, - CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE, - CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE, + KVStore, KVStoreSync, CHANNEL_MANAGER_PERSISTENCE_KEY, + CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, + CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE, }; use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; +use lightning::{log_info, log_trace}; use lightning_persister::fs_store::FilesystemStore; use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; @@ -1425,20 +1425,21 @@ fn build_with_store_internal( if let Some(migration) = channel_data_migration { if let Some(manager_bytes) = &migration.channel_manager { - runtime.block_on(async { - KVStore::write( - &*kv_store, - CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, - CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, - CHANNEL_MANAGER_PERSISTENCE_KEY, - manager_bytes.clone(), - ) - .await - }) - .map_err(|e| { - log_error!(logger, "Failed to write migrated channel_manager: {}", e); - BuildError::WriteFailed - })?; + runtime + .block_on(async { + KVStore::write( + &*kv_store, + CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, + CHANNEL_MANAGER_PERSISTENCE_KEY, + manager_bytes.clone(), + ) + .await + }) + .map_err(|e| { + log_error!(logger, "Failed to write migrated channel_manager: {}", e); + BuildError::WriteFailed + })?; } for monitor_data in &migration.channel_monitors { @@ -1458,23 +1459,28 @@ fn build_with_store_internal( let monitor_key = format!("{}_{}", funding_txo.txid, funding_txo.index); log_info!(logger, "Migrating channel monitor: {}", monitor_key); - runtime.block_on(async { - KVStore::write( - &*kv_store, - CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE, - CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE, - &monitor_key, - monitor_data.clone(), - ) - .await - }) - .map_err(|e| { - log_error!(logger, "Failed to write channel_monitor {}: {}", monitor_key, e); - BuildError::WriteFailed - })?; + runtime + .block_on(async { + KVStore::write( + &*kv_store, + CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE, + CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE, + &monitor_key, + monitor_data.clone(), + ) + .await + }) + .map_err(|e| { + log_error!(logger, "Failed to write channel_monitor {}: {}", monitor_key, e); + BuildError::WriteFailed + })?; } - log_info!(logger, "Applied channel migration: {} monitors", migration.channel_monitors.len()); + log_info!( + logger, + "Applied channel migration: {} monitors", + migration.channel_monitors.len() + ); } // Read ChannelMonitor state from store diff --git a/src/io/utils.rs b/src/io/utils.rs index eb2372b63..e97f673bc 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -92,8 +92,8 @@ pub fn derive_node_secret_from_mnemonic( let ldk_seed_bytes: [u8; 32] = master_xpriv.private_key.secret_bytes(); - let keys_manager_master = Xpriv::new_master(Network::Bitcoin, &ldk_seed_bytes) - .map_err(|_| Error::InvalidMnemonic)?; + let keys_manager_master = + Xpriv::new_master(Network::Bitcoin, &ldk_seed_bytes).map_err(|_| Error::InvalidMnemonic)?; let node_secret_xpriv = keys_manager_master .derive_priv(&Secp256k1::new(), &[ChildNumber::from_hardened_idx(0).unwrap()]) @@ -701,8 +701,7 @@ mod tests { "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; // Derive using our function - let derived_secret = - derive_node_secret_from_mnemonic(mnemonic.to_string(), None).unwrap(); + let derived_secret = derive_node_secret_from_mnemonic(mnemonic.to_string(), None).unwrap(); // Derive using LDK's KeysManager (same flow as Builder) let parsed = Mnemonic::parse(mnemonic).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 04e9df283..b68479802 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,6 +102,7 @@ mod tx_broadcaster; mod types; mod wallet; +use std::collections::HashMap; use std::default::Default; use std::net::ToSocketAddrs; use std::ops::Deref; @@ -113,9 +114,9 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::{Address, Amount}; #[cfg(feature = "uniffi")] pub use builder::ArcedNodeBuilder as Builder; -pub use builder::{BuildError, ChannelDataMigration}; #[cfg(not(feature = "uniffi"))] pub use builder::NodeBuilder as Builder; +pub use builder::{BuildError, ChannelDataMigration}; use chain::ChainSource; use config::{ default_user_config, may_announce_channel, AsyncPaymentsRole, ChannelConfig, Config, @@ -131,8 +132,9 @@ use fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator}; use ffi::*; use gossip::GossipSource; use graph::NetworkGraph; -pub use io::utils::{derive_node_secret_from_mnemonic, generate_entropy_mnemonic}; use io::utils::write_node_metrics; +pub use io::utils::{derive_node_secret_from_mnemonic, generate_entropy_mnemonic}; +use lightning::chain::channelmonitor::Balance as LdkBalance; use lightning::chain::BestBlock; use lightning::events::bump_transaction::{Input, Wallet as LdkWallet}; use lightning::impl_writeable_tlv_based; @@ -141,6 +143,7 @@ use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelS use lightning::ln::channelmanager::PaymentId; use lightning::ln::funding::SpliceContribution; use lightning::ln::msgs::SocketAddress; +use lightning::ln::types::ChannelId; use lightning::routing::gossip::NodeAlias; use lightning::util::persist::KVStoreSync; use lightning_background_processor::process_events_async; @@ -1047,7 +1050,37 @@ impl Node { /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { - self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect() + // Build channel_id -> claimable_on_close_sats map from monitors + let mut claimable_map: HashMap = HashMap::new(); + + for channel_id in self.chain_monitor.list_monitors() { + if let Ok(monitor) = self.chain_monitor.get_monitor(channel_id) { + for balance in monitor.get_claimable_balances() { + if let LdkBalance::ClaimableOnChannelClose { + balance_candidates, + confirmed_balance_candidate_index, + .. + } = &balance + { + if let Some(confirmed) = + balance_candidates.get(*confirmed_balance_candidate_index) + { + *claimable_map.entry(channel_id).or_insert(0) += + confirmed.amount_satoshis; + } + } + } + } + } + + self.channel_manager + .list_channels() + .into_iter() + .map(|c| { + let balance = claimable_map.get(&c.channel_id).copied(); + ChannelDetails::from_ldk_with_balance(c, balance) + }) + .collect() } /// Connect to a node on the peer-to-peer network. diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 8d2593c36..9b256d3be 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -16,7 +16,9 @@ use bitcoin::hashes::Hash; use lightning::ln::channelmanager::{ Bolt11InvoiceParameters, Bolt11PaymentError, PaymentId, Retry, RetryableSendFailure, }; -use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig, Router as LdkRouter}; +use lightning::routing::router::{ + PaymentParameters, RouteParameters, RouteParametersConfig, Router as LdkRouter, +}; use lightning_invoice::{ Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescription as LdkBolt11InvoiceDescription, }; @@ -957,20 +959,23 @@ impl Bolt11Payment { Error::InvalidInvoice })?; - let route_params = RouteParameters::from_payment_params_and_value(payment_params, amount_msat); + let route_params = + RouteParameters::from_payment_params_and_value(payment_params, amount_msat); let first_hops = self.channel_manager.list_usable_channels(); let inflight_htlcs = self.channel_manager.compute_inflight_htlcs(); - let route = (&*self.router).find_route( - &self.channel_manager.get_our_node_id(), - &route_params, - Some(&first_hops.iter().collect::>()), - inflight_htlcs, - ).map_err(|e| { - log_error!(self.logger, "Failed to find route for fee estimation: {:?}", e); - Error::RouteNotFound - })?; + let route = (&*self.router) + .find_route( + &self.channel_manager.get_our_node_id(), + &route_params, + Some(&first_hops.iter().collect::>()), + inflight_htlcs, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to find route for fee estimation: {:?}", e); + Error::RouteNotFound + })?; let total_fees = route.paths.iter().map(|path| path.fee_msat()).sum::(); @@ -1002,20 +1007,23 @@ impl Bolt11Payment { } } - let route_params = RouteParameters::from_payment_params_and_value(payment_params, amount_msat); + let route_params = + RouteParameters::from_payment_params_and_value(payment_params, amount_msat); let first_hops = self.channel_manager.list_usable_channels(); let inflight_htlcs = self.channel_manager.compute_inflight_htlcs(); - let route = (&*self.router).find_route( - &self.channel_manager.get_our_node_id(), - &route_params, - Some(&first_hops.iter().collect::>()), - inflight_htlcs, - ).map_err(|e| { - log_error!(self.logger, "Failed to find route for fee estimation: {:?}", e); - Error::RouteNotFound - })?; + let route = (&*self.router) + .find_route( + &self.channel_manager.get_our_node_id(), + &route_params, + Some(&first_hops.iter().collect::>()), + inflight_htlcs, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to find route for fee estimation: {:?}", e); + Error::RouteNotFound + })?; let total_fees = route.paths.iter().map(|path| path.fee_msat()).sum::(); diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index 9a5ea8516..1b4bd18dc 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -13,8 +13,8 @@ use bitcoin::{Address, Txid}; use crate::config::Config; use crate::error::Error; -use crate::logger::{log_info, LdkLogger, Logger}; use crate::fee_estimator::ConfirmationTarget; +use crate::logger::{log_info, LdkLogger, Logger}; use crate::types::{ChannelManager, SpendableUtxo, Wallet}; use crate::wallet::{CoinSelectionAlgorithm, OnchainSendAmount}; diff --git a/src/types.rs b/src/types.rs index c998e27c0..717aa6111 100644 --- a/src/types.rs +++ b/src/types.rs @@ -398,6 +398,11 @@ pub struct ChannelDetails { pub inbound_htlc_maximum_msat: Option, /// Set of configurable parameters that affect channel operation. pub config: ChannelConfig, + /// The amount, in satoshis, claimable if the channel is closed now. + /// + /// This is computed from the channel monitor and represents the confirmed balance + /// excluding pending HTLCs. Returns `None` if no monitor exists yet (pre-funding). + pub claimable_on_close_sats: Option, } impl From for ChannelDetails { @@ -452,10 +457,21 @@ impl From for ChannelDetails { inbound_htlc_maximum_msat: value.inbound_htlc_maximum_msat, // unwrap safety: `config` is only `None` for LDK objects serialized prior to 0.0.109. config: value.config.map(|c| c.into()).unwrap(), + claimable_on_close_sats: None, } } } +impl ChannelDetails { + pub(crate) fn from_ldk_with_balance( + value: LdkChannelDetails, claimable_on_close_sats: Option, + ) -> Self { + let mut details: ChannelDetails = value.into(); + details.claimable_on_close_sats = claimable_on_close_sats; + details + } +} + /// Details of a known Lightning peer as returned by [`Node::list_peers`]. /// /// [`Node::list_peers`]: crate::Node::list_peers