diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dc68f8d..4e96563 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -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 }} diff --git a/index.d.ts b/index.d.ts index 9a9fadc..205e59c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -35,6 +35,18 @@ export interface ReceivedPayment { paymentHash: string amount: number } +/** 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. + */ + paymentHash?: string + /** The payment preimage (proof of payment). Available after the payment succeeds. */ + preimage?: string +} export interface PaymentEvent { eventType: PaymentEventType paymentHash: string @@ -147,7 +159,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 +177,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..f4ce87f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -282,6 +282,24 @@ pub struct ReceivedPayment { pub amount: i64, } +/// 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, + /// 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 +971,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 +994,8 @@ impl MdkNode { match event { Event::PaymentSuccessful { payment_id: event_payment_id, + payment_hash, + payment_preimage, .. } => { self @@ -983,8 +1004,16 @@ 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={}", + hash_hex, + ); + return Ok(PaymentOutcome { + payment_hash: Some(hash_hex), + preimage: preimage_hex, + }); } } Event::PaymentFailed { @@ -1005,7 +1034,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 +1052,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 +1080,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 +1125,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 +1296,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 +1324,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 +1363,28 @@ 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 + 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 { - self.wait_for_payment_outcome(&payment_id, 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, + }) } - - Ok(bytes_to_hex(&payment_id.0)) } }