From b9a377e9ff69d3710bd4065712886f93cae30ed9 Mon Sep 17 00:00:00 2001 From: Fernando Ledesma Date: Thu, 12 Feb 2026 09:36:38 -0500 Subject: [PATCH 1/4] Output preimage after successful payment --- index.d.ts | 14 ++++++++-- src/lib.rs | 76 ++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/index.d.ts b/index.d.ts index 9a9fadc..2f8724b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -35,6 +35,16 @@ export interface ReceivedPayment { paymentHash: string amount: number } +/** Result of a successful outbound payment. */ +export interface PaymentResult { + /** + * The payment hash from the invoice/offer (identifies the HTLC). + * Available immediately for BOLT11; populated from the PaymentSuccessful event for BOLT12. + */ + paymentHash?: string + /** The payment preimage (proof of payment). Available after the payment succeeds. */ + preimage?: string +} export interface PaymentEvent { eventType: PaymentEventType paymentHash: string @@ -147,7 +157,7 @@ export declare class MdkNode { destination: string, amountMsat?: number | undefined | null, waitForPaymentSecs?: number | undefined | null, - ): string + ): PaymentResult /** * Unified payment method that auto-detects the destination type. * Use this when the node is already running via start_receiving(). @@ -165,5 +175,5 @@ export declare class MdkNode { destination: string, amountMsat?: number | undefined | null, waitForPaymentSecs?: number | undefined | null, - ): string + ): PaymentResult } diff --git a/src/lib.rs b/src/lib.rs index 87fcef2..6fe8fd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -282,6 +282,22 @@ pub struct ReceivedPayment { pub amount: i64, } +/// Result of a successful outbound payment. +#[napi(object)] +pub struct PaymentResult { + /// The payment hash from the invoice/offer (identifies the HTLC). + /// Available immediately for BOLT11; populated from the PaymentSuccessful event for BOLT12. + pub payment_hash: Option, + /// The payment preimage (proof of payment). Available after the payment succeeds. + pub preimage: Option, +} + +/// Internal result from wait_for_payment_outcome. Not exported via NAPI. +struct PaymentOutcome { + payment_hash: Option, + preimage: Option, +} + #[napi(object)] pub struct PaymentEvent { pub event_type: PaymentEventType, @@ -953,11 +969,12 @@ impl MdkNode { Ok(()) } + /// Wait for a payment to succeed or fail, returning the hash and preimage on success. fn wait_for_payment_outcome( &self, payment_id: &PaymentId, timeout_secs: u64, - ) -> napi::Result<()> { + ) -> napi::Result { eprintln!( "[lightning-js] wait_for_payment_outcome start payment_id={} timeout_secs={}", bytes_to_hex(&payment_id.0), @@ -975,6 +992,8 @@ impl MdkNode { match event { Event::PaymentSuccessful { payment_id: event_payment_id, + payment_hash, + payment_preimage, .. } => { self @@ -983,8 +1002,17 @@ impl MdkNode { .map_err(|err| napi::Error::new(Status::GenericFailure, err.to_string()))?; if event_payment_id == Some(*payment_id) { - eprintln!("[lightning-js] wait_for_payment_outcome success"); - return Ok(()); + let hash_hex = bytes_to_hex(&payment_hash.0); + let preimage_hex = payment_preimage.map(|p| bytes_to_hex(&p.0)); + eprintln!( + "[lightning-js] wait_for_payment_outcome success hash={} preimage={}", + hash_hex, + preimage_hex.as_deref().unwrap_or("none"), + ); + return Ok(PaymentOutcome { + payment_hash: Some(hash_hex), + preimage: preimage_hex, + }); } } Event::PaymentFailed { @@ -1005,7 +1033,7 @@ impl MdkNode { eprintln!("[lightning-js] wait_for_payment_outcome failure reason={reason_str}"); return Err(napi::Error::new( Status::GenericFailure, - format!("lnurl payment failed: {reason_str}"), + format!("payment failed: {reason_str}"), )); } } @@ -1023,11 +1051,12 @@ impl MdkNode { } if Instant::now() >= deadline { - eprintln!( - "[lightning-js] Timed out waiting {timeout_secs}s for lnurl payment confirmation" - ); + eprintln!("[lightning-js] Timed out waiting {timeout_secs}s for payment confirmation"); eprintln!("[lightning-js] wait_for_payment_outcome finished with timeout"); - return Ok(()); + return Ok(PaymentOutcome { + payment_hash: None, + preimage: None, + }); } std::thread::sleep(POLL_INTERVAL); @@ -1050,7 +1079,7 @@ impl MdkNode { destination: String, amount_msat: Option, wait_for_payment_secs: Option, - ) -> napi::Result { + ) -> napi::Result { eprintln!( "[lightning-js] pay called destination={} amount_msat={:?} wait_for_payment_secs={:?}", destination, amount_msat, wait_for_payment_secs @@ -1095,7 +1124,7 @@ impl MdkNode { destination: String, amount_msat: Option, wait_for_payment_secs: Option, - ) -> napi::Result { + ) -> napi::Result { eprintln!( "[lightning-js] pay_while_running called destination={} amount_msat={:?} wait_for_payment_secs={:?}", destination, amount_msat, wait_for_payment_secs @@ -1266,7 +1295,7 @@ impl MdkNode { &self, target: &PaymentTarget, wait_secs: Option, - ) -> napi::Result { + ) -> napi::Result { // BOLT12 requires full RGS sync for onion message routing if matches!(target, PaymentTarget::Bolt12(_, _)) { eprintln!("[lightning-js] doing full RGS sync for BOLT12"); @@ -1294,6 +1323,13 @@ impl MdkNode { )); } + // Extract payment hash from BOLT11 invoice (known before sending). + // For BOLT12, the hash is only available after the PaymentSuccessful event. + let known_payment_hash = match target { + PaymentTarget::Bolt11(invoice, _) => Some(invoice.payment_hash().to_string()), + PaymentTarget::Bolt12(_, _) => None, + }; + // Send payment let payment_id = match target { PaymentTarget::Bolt11(invoice, amount) => { @@ -1326,16 +1362,24 @@ impl MdkNode { })?; eprintln!( - "[lightning-js] payment sent, id={}", + "[lightning-js] payment sent, payment_id={}", bytes_to_hex(&payment_id.0) ); - // Wait for outcome if requested + // Wait for outcome if requested, capturing the payment hash and preimage if let Some(secs) = wait_secs { - self.wait_for_payment_outcome(&payment_id, secs)?; + let outcome = self.wait_for_payment_outcome(&payment_id, secs)?; + Ok(PaymentResult { + // Prefer the hash from the invoice (BOLT11); fall back to the event hash (BOLT12) + payment_hash: known_payment_hash.or(outcome.payment_hash), + preimage: outcome.preimage, + }) + } else { + Ok(PaymentResult { + payment_hash: known_payment_hash, + preimage: None, + }) } - - Ok(bytes_to_hex(&payment_id.0)) } } From 26a3c3e230744d572217a4eaad21a07d7fa51596 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 12 Feb 2026 15:17:30 -0300 Subject: [PATCH 2/4] Pin ubuntu-22.04 for docker-based CI builds ubuntu-latest now ships Docker 29.x (min API 1.44) which breaks addnab/docker-run-action@v3's docker:20.10 client (API 1.41). --- .github/workflows/CI.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dc68f8d..84b52ca 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -67,7 +67,7 @@ jobs: - host: windows-latest target: i686-pc-windows-msvc build: yarn build --target i686-pc-windows-msvc - - host: ubuntu-latest + - host: ubuntu-22.04 target: x86_64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian build: | @@ -75,7 +75,7 @@ jobs: rustup default $RUST_VERSION rustup target add x86_64-unknown-linux-gnu yarn build --target x86_64-unknown-linux-gnu - - host: ubuntu-latest + - host: ubuntu-22.04 target: x86_64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | @@ -88,7 +88,7 @@ jobs: - host: macos-latest target: aarch64-apple-darwin build: yarn build --target aarch64-apple-darwin - - host: ubuntu-latest + - host: ubuntu-22.04 target: aarch64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 build: | @@ -114,7 +114,7 @@ jobs: export AR_aarch64_linux_android="$TOOLCHAIN/bin/llvm-ar" export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/aarch64-linux-android28-clang" yarn build --target aarch64-linux-android - - host: ubuntu-latest + - host: ubuntu-22.04 target: aarch64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | From d7d0f2cbde08d949dae831956e5d84c9cd8e029c Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 12 Feb 2026 15:23:48 -0300 Subject: [PATCH 3/4] Replace addnab/docker-run-action with direct docker run addnab/docker-run-action@v3 uses docker:20.10 (API 1.41) which is incompatible with Docker 29.x (min API 1.44) now shipped on all GitHub-hosted Ubuntu runners since Feb 9 2026. Ref: https://github.com/actions/runner-images/issues/13474 --- .github/workflows/CI.yml | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 84b52ca..4e96563 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -67,7 +67,7 @@ jobs: - host: windows-latest target: i686-pc-windows-msvc build: yarn build --target i686-pc-windows-msvc - - host: ubuntu-22.04 + - host: ubuntu-latest target: x86_64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian build: | @@ -75,7 +75,7 @@ jobs: rustup default $RUST_VERSION rustup target add x86_64-unknown-linux-gnu yarn build --target x86_64-unknown-linux-gnu - - host: ubuntu-22.04 + - host: ubuntu-latest target: x86_64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | @@ -88,7 +88,7 @@ jobs: - host: macos-latest target: aarch64-apple-darwin build: yarn build --target aarch64-apple-darwin - - host: ubuntu-22.04 + - host: ubuntu-latest target: aarch64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 build: | @@ -114,7 +114,7 @@ jobs: export AR_aarch64_linux_android="$TOOLCHAIN/bin/llvm-ar" export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$TOOLCHAIN/bin/aarch64-linux-android28-clang" yarn build --target aarch64-linux-android - - host: ubuntu-22.04 + - host: ubuntu-latest target: aarch64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | @@ -160,12 +160,22 @@ jobs: - name: Install dependencies run: yarn install - name: Build in docker - uses: addnab/docker-run-action@v3 if: ${{ matrix.settings.docker }} - with: - image: ${{ matrix.settings.docker }} - options: '--user 0:0 -e RUST_VERSION=${{ env.RUST_VERSION }} -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build' - run: ${{ matrix.settings.build }} + env: + BUILD_SCRIPT: ${{ matrix.settings.build }} + run: | + printf '%s\n' "$BUILD_SCRIPT" > /tmp/build.sh + docker run --rm \ + --user 0:0 \ + -e RUST_VERSION=${{ env.RUST_VERSION }} \ + -v /tmp/build.sh:/tmp/build.sh \ + -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db \ + -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache \ + -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index \ + -v ${{ github.workspace }}:/build \ + -w /build \ + ${{ matrix.settings.docker }} \ + sh /tmp/build.sh - name: Build run: ${{ matrix.settings.build }} if: ${{ !matrix.settings.docker }} From 44bdd9ff5917df9f4ecb721fe016a561bff5bd46 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 12 Feb 2026 15:47:25 -0300 Subject: [PATCH 4/4] Add payment_id to PaymentResult and stop logging preimages PaymentResult now always includes payment_id so callers can track async BOLT12 payments even without wait_secs. Previously, BOLT12 payments returned an empty PaymentResult with no tracking identifier. Also removes preimage from stderr logs - it's sensitive payment proof material that shouldn't be in logs. Still returned via the API. --- index.d.ts | 2 ++ src/lib.rs | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 2f8724b..205e59c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -37,6 +37,8 @@ export interface ReceivedPayment { } /** Result of a successful outbound payment. */ export interface PaymentResult { + /** Opaque payment identifier. Always present - can be used to correlate async BOLT12 payments. */ + paymentId: string /** * The payment hash from the invoice/offer (identifies the HTLC). * Available immediately for BOLT11; populated from the PaymentSuccessful event for BOLT12. diff --git a/src/lib.rs b/src/lib.rs index 6fe8fd1..f4ce87f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -285,6 +285,8 @@ pub struct ReceivedPayment { /// Result of a successful outbound payment. #[napi(object)] pub struct PaymentResult { + /// Opaque payment identifier. Always present - can be used to correlate async BOLT12 payments. + pub payment_id: String, /// The payment hash from the invoice/offer (identifies the HTLC). /// Available immediately for BOLT11; populated from the PaymentSuccessful event for BOLT12. pub payment_hash: Option, @@ -1005,9 +1007,8 @@ impl MdkNode { let hash_hex = bytes_to_hex(&payment_hash.0); let preimage_hex = payment_preimage.map(|p| bytes_to_hex(&p.0)); eprintln!( - "[lightning-js] wait_for_payment_outcome success hash={} preimage={}", + "[lightning-js] wait_for_payment_outcome success hash={}", hash_hex, - preimage_hex.as_deref().unwrap_or("none"), ); return Ok(PaymentOutcome { payment_hash: Some(hash_hex), @@ -1366,16 +1367,20 @@ impl MdkNode { bytes_to_hex(&payment_id.0) ); + let payment_id_hex = bytes_to_hex(&payment_id.0); + // Wait for outcome if requested, capturing the payment hash and preimage if let Some(secs) = wait_secs { let outcome = self.wait_for_payment_outcome(&payment_id, secs)?; Ok(PaymentResult { + payment_id: payment_id_hex, // Prefer the hash from the invoice (BOLT11); fall back to the event hash (BOLT12) payment_hash: known_payment_hash.or(outcome.payment_hash), preimage: outcome.preimage, }) } else { Ok(PaymentResult { + payment_id: payment_id_hex, payment_hash: known_payment_hash, preimage: None, })