From 43e84f95fa858b9365e909e7c178008a9289613b Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Fri, 12 Jul 2024 08:48:43 +0200 Subject: [PATCH 1/8] bump greenlight --- libs/Cargo.lock | 2 +- libs/sdk-core/Cargo.toml | 2 +- tools/sdk-cli/Cargo.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/Cargo.lock b/libs/Cargo.lock index 96ab8a428..693e276c7 100644 --- a/libs/Cargo.lock +++ b/libs/Cargo.lock @@ -1386,7 +1386,7 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gl-client" version = "0.2.0" -source = "git+https://github.com/Blockstream/greenlight.git?rev=b9d1ecb9ea7325b041d08ddc171486fdad646a63#b9d1ecb9ea7325b041d08ddc171486fdad646a63" +source = "git+https://github.com/Blockstream/greenlight.git?rev=43f02207182e28a4d62e7aa1c79cd098f9b300ba#43f02207182e28a4d62e7aa1c79cd098f9b300ba" dependencies = [ "anyhow", "async-trait", diff --git a/libs/sdk-core/Cargo.toml b/libs/sdk-core/Cargo.toml index 17a7a50f2..72eb258f1 100644 --- a/libs/sdk-core/Cargo.toml +++ b/libs/sdk-core/Cargo.toml @@ -16,7 +16,7 @@ hex = { workspace = true } # The switch to 0.2 will happen with https://github.com/breez/breez-sdk/pull/724 gl-client = { git = "https://github.com/Blockstream/greenlight.git", features = [ "permissive", -], rev = "b9d1ecb9ea7325b041d08ddc171486fdad646a63" } +], rev = "43f02207182e28a4d62e7aa1c79cd098f9b300ba" } zbase32 = "0.1.2" base64 = { workspace = true } chrono = "0.4" diff --git a/tools/sdk-cli/Cargo.lock b/tools/sdk-cli/Cargo.lock index b3132e8e6..211e401be 100644 --- a/tools/sdk-cli/Cargo.lock +++ b/tools/sdk-cli/Cargo.lock @@ -1247,7 +1247,7 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gl-client" version = "0.2.0" -source = "git+https://github.com/Blockstream/greenlight.git?rev=b9d1ecb9ea7325b041d08ddc171486fdad646a63#b9d1ecb9ea7325b041d08ddc171486fdad646a63" +source = "git+https://github.com/Blockstream/greenlight.git?rev=43f02207182e28a4d62e7aa1c79cd098f9b300ba#43f02207182e28a4d62e7aa1c79cd098f9b300ba" dependencies = [ "anyhow", "async-trait", From df816046df37be759c6afbc42db0676755324f9a Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 12 Aug 2024 12:07:48 +0200 Subject: [PATCH 2/8] persist the lsp pubkey in cache --- libs/sdk-core/src/breez_services.rs | 77 +++++++++++++++------------ libs/sdk-core/src/persist/settings.rs | 9 ++++ 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index a91d1f7b4..9a62bf0c8 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -673,19 +673,22 @@ impl BreezServices { /// Select the LSP to be used and provide inbound liquidity pub async fn connect_lsp(&self, lsp_id: String) -> SdkResult<()> { - match self.list_lsps().await?.iter().any(|lsp| lsp.id == lsp_id) { - true => { - self.persister.set_lsp_id(lsp_id)?; - self.sync().await?; - if let Some(webhook_url) = self.persister.get_webhook_url()? { - self.register_payment_notifications(webhook_url).await? - } - Ok(()) + let lsp_pubkey = match self.list_lsps().await?.iter().find(|lsp| lsp.id == lsp_id) { + Some(lsp) => lsp.pubkey.clone(), + None => { + return Err(SdkError::Generic { + err: format!("Unknown LSP: {lsp_id}"), + }) } - false => Err(SdkError::Generic { - err: format!("Unknown LSP: {lsp_id}"), - }), + }; + + self.persister.set_lsp_id(lsp_id)?; + self.persister.set_lsp_pubkey(lsp_pubkey)?; + self.sync().await?; + if let Some(webhook_url) = self.persister.get_webhook_url()? { + self.register_payment_notifications(webhook_url).await? } + Ok(()) } /// Get the current LSP's ID @@ -1171,32 +1174,40 @@ impl BreezServices { /// If not or no LSP is selected, it selects the first LSP in [`list_lsps`]. async fn connect_lsp_peer(&self, node_pubkey: String) -> SdkResult<()> { let lsps = self.lsp_api.list_lsps(node_pubkey).await?; - if let Some(lsp) = self + let lsp = match self .persister .get_lsp_id()? - .and_then(|lsp_id| lsps.clone().into_iter().find(|lsp| lsp.id == lsp_id)) - .or_else(|| lsps.first().cloned()) + .and_then(|lsp_id| lsps.iter().find(|lsp| lsp.id == lsp_id)) + .or_else(|| lsps.first()) { - self.persister.set_lsp_id(lsp.id)?; - if let Ok(node_state) = self.node_info() { - let node_id = lsp.pubkey; - let address = lsp.host; - let lsp_connected = node_state - .connected_peers - .iter() - .any(|e| e == node_id.as_str()); - if !lsp_connected { - debug!("connecting to lsp {}@{}", node_id.clone(), address.clone()); - self.node_api - .connect_peer(node_id.clone(), address.clone()) - .await - .map_err(|e| SdkError::ServiceConnectivity { - err: format!("(LSP: {node_id}) Failed to connect: {e}"), - })?; - } - debug!("connected to lsp {node_id}@{address}"); - } + Some(lsp) => lsp.clone(), + None => return Ok(()), + }; + + self.persister.set_lsp_id(lsp.id)?; + self.persister.set_lsp_pubkey(lsp.pubkey.clone())?; + let node_state = match self.node_info() { + Ok(node_state) => node_state, + Err(_) => return Ok(()), + }; + + let node_id = lsp.pubkey; + let address = lsp.host; + let lsp_connected = node_state + .connected_peers + .iter() + .any(|e| e == node_id.as_str()); + if !lsp_connected { + debug!("connecting to lsp {}@{}", node_id.clone(), address.clone()); + self.node_api + .connect_peer(node_id.clone(), address.clone()) + .await + .map_err(|e| SdkError::ServiceConnectivity { + err: format!("(LSP: {node_id}) Failed to connect: {e}"), + })?; + debug!("connected to lsp {node_id}@{address}"); } + Ok(()) } diff --git a/libs/sdk-core/src/persist/settings.rs b/libs/sdk-core/src/persist/settings.rs index 82710456f..2272a7ddd 100644 --- a/libs/sdk-core/src/persist/settings.rs +++ b/libs/sdk-core/src/persist/settings.rs @@ -58,6 +58,15 @@ impl SqliteStorage { pub fn get_lsp_id(&self) -> PersistResult> { self.get_setting("lsp".to_string()) } + + pub fn set_lsp_pubkey(&self, pubkey: String) -> PersistResult<()> { + self.update_setting("lsp-pubkey".to_string(), pubkey) + } + + #[allow(dead_code)] + pub fn get_lsp_pubkey(&self) -> PersistResult> { + self.get_setting("lsp-pubkey".to_string()) + } } #[test] From b48b29cf7d9dc801776feb0497e4abbf9122c761 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Thu, 11 Jul 2024 14:43:56 +0200 Subject: [PATCH 3/8] connect to lsp also with existing node --- libs/sdk-core/src/breez_services.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 9a62bf0c8..5f18360c8 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -1412,15 +1412,15 @@ impl BreezServices { .await; // Sync node state - let sync_breez_services = self.clone(); - match sync_breez_services.persister.get_node_state()? { + match self.persister.get_node_state()? { Some(node) => { - info!("Starting existing node {}", node.id) + info!("Starting existing node {}", node.id); + self.connect_lsp_peer(node.id).await?; } None => { // In case it is a first run we sync in foreground to get the node state. info!("First run, syncing in foreground"); - sync_breez_services.sync().await?; + self.sync().await?; info!("First run, finished running syncing in foreground"); } } From 69a3f986b3d1265f70fcc03f6776bb57e8b65e6f Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Fri, 12 Jul 2024 10:15:58 +0200 Subject: [PATCH 4/8] add `use_trampoline` flags to `send_payment` and `lnurl_pay` --- libs/sdk-bindings/src/breez_sdk.udl | 2 ++ libs/sdk-common/src/lnurl/specs/pay.rs | 4 ++++ libs/sdk-core/src/binding.rs | 1 + libs/sdk-core/src/breez_services.rs | 1 + libs/sdk-core/src/bridge_generated.io.rs | 6 ++++++ libs/sdk-core/src/lnurl/pay.rs | 11 +++++++++++ libs/sdk-core/src/models.rs | 4 ++++ libs/sdk-flutter/ios/Classes/bridge_generated.h | 2 ++ libs/sdk-flutter/lib/bridge_generated.dart | 16 ++++++++++++++++ .../src/main/java/com/breezsdk/BreezSDKMapper.kt | 8 ++++++++ libs/sdk-react-native/ios/BreezSDKMapper.swift | 10 ++++++++++ libs/sdk-react-native/src/index.ts | 2 ++ tools/sdk-cli/src/command_handlers.rs | 4 ++++ tools/sdk-cli/src/commands.rs | 8 ++++++++ 14 files changed, 79 insertions(+) diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl index 69175c1a4..21d8d758e 100644 --- a/libs/sdk-bindings/src/breez_sdk.udl +++ b/libs/sdk-bindings/src/breez_sdk.udl @@ -634,6 +634,7 @@ dictionary LnUrlPayErrorData { dictionary LnUrlPayRequest { LnUrlPayRequestData data; u64 amount_msat; + boolean use_trampoline; string? comment = null; string? payment_label = null; boolean? validate_success_action_url = null; @@ -766,6 +767,7 @@ dictionary RedeemOnchainFundsResponse { dictionary SendPaymentRequest { string bolt11; + boolean use_trampoline; u64? amount_msat = null; string? label = null; }; diff --git a/libs/sdk-common/src/lnurl/specs/pay.rs b/libs/sdk-common/src/lnurl/specs/pay.rs index 358e36e34..adebfbef6 100644 --- a/libs/sdk-common/src/lnurl/specs/pay.rs +++ b/libs/sdk-common/src/lnurl/specs/pay.rs @@ -132,6 +132,10 @@ pub mod model { pub data: LnUrlPayRequestData, /// The amount in millisatoshis for this payment pub amount_msat: u64, + /// Trampoline payments outsource pathfinding to the LSP. Trampoline payments can improve + /// payment performance, but are generally more expensive in terms of fees and they + /// compromise on privacy. + pub use_trampoline: bool, /// An optional comment for this payment pub comment: Option, /// The external label or identifier of the [Payment] diff --git a/libs/sdk-core/src/binding.rs b/libs/sdk-core/src/binding.rs index 45b77825a..4d401d1df 100644 --- a/libs/sdk-core/src/binding.rs +++ b/libs/sdk-core/src/binding.rs @@ -123,6 +123,7 @@ pub struct _RouteHintHop { pub struct _LnUrlPayRequest { pub data: LnUrlPayRequestData, pub amount_msat: u64, + pub use_trampoline: bool, pub comment: Option, pub payment_label: Option, pub validate_success_action_url: Option, diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 5f18360c8..181c9b955 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -365,6 +365,7 @@ impl BreezServices { let pay_req = SendPaymentRequest { bolt11: cb.pr.clone(), amount_msat: None, + use_trampoline: req.use_trampoline, label: req.payment_label, }; let invoice = parse_invoice(cb.pr.as_str())?; diff --git a/libs/sdk-core/src/bridge_generated.io.rs b/libs/sdk-core/src/bridge_generated.io.rs index c11b00b63..f67df9ca1 100644 --- a/libs/sdk-core/src/bridge_generated.io.rs +++ b/libs/sdk-core/src/bridge_generated.io.rs @@ -868,6 +868,7 @@ impl Wire2Api for wire_LnUrlPayRequest { LnUrlPayRequest { data: self.data.wire2api(), amount_msat: self.amount_msat.wire2api(), + use_trampoline: self.use_trampoline.wire2api(), comment: self.comment.wire2api(), payment_label: self.payment_label.wire2api(), validate_success_action_url: self.validate_success_action_url.wire2api(), @@ -1082,6 +1083,7 @@ impl Wire2Api for wire_SendPaymentRequest { fn wire2api(self) -> SendPaymentRequest { SendPaymentRequest { bolt11: self.bolt11.wire2api(), + use_trampoline: self.use_trampoline.wire2api(), amount_msat: self.amount_msat.wire2api(), label: self.label.wire2api(), } @@ -1238,6 +1240,7 @@ pub struct wire_LnUrlAuthRequestData { pub struct wire_LnUrlPayRequest { data: wire_LnUrlPayRequestData, amount_msat: u64, + use_trampoline: bool, comment: *mut wire_uint_8_list, payment_label: *mut wire_uint_8_list, validate_success_action_url: *mut bool, @@ -1402,6 +1405,7 @@ pub struct wire_SendOnchainRequest { #[derive(Clone)] pub struct wire_SendPaymentRequest { bolt11: *mut wire_uint_8_list, + use_trampoline: bool, amount_msat: *mut u64, label: *mut wire_uint_8_list, } @@ -1647,6 +1651,7 @@ impl NewWithNullPtr for wire_LnUrlPayRequest { Self { data: Default::default(), amount_msat: Default::default(), + use_trampoline: Default::default(), comment: core::ptr::null_mut(), payment_label: core::ptr::null_mut(), validate_success_action_url: core::ptr::null_mut(), @@ -2011,6 +2016,7 @@ impl NewWithNullPtr for wire_SendPaymentRequest { fn new_with_null_ptr() -> Self { Self { bolt11: core::ptr::null_mut(), + use_trampoline: Default::default(), amount_msat: core::ptr::null_mut(), label: core::ptr::null_mut(), } diff --git a/libs/sdk-core/src/lnurl/pay.rs b/libs/sdk-core/src/lnurl/pay.rs index e62afa800..50d4b6d30 100644 --- a/libs/sdk-core/src/lnurl/pay.rs +++ b/libs/sdk-core/src/lnurl/pay.rs @@ -402,6 +402,7 @@ pub(crate) mod tests { .lnurl_pay(LnUrlPayRequest { data: pay_req, amount_msat: user_amount_msat, + use_trampoline: false, comment: Some(comment), payment_label: None, validate_success_action_url: None, @@ -447,6 +448,7 @@ pub(crate) mod tests { .lnurl_pay(LnUrlPayRequest { data: pay_req, amount_msat: user_amount_msat, + use_trampoline: false, comment: Some(comment), payment_label: None, validate_success_action_url: None, @@ -478,6 +480,7 @@ pub(crate) mod tests { .lnurl_pay(LnUrlPayRequest { data: pay_req, amount_msat: user_amount_msat, + use_trampoline: false, comment: Some(comment), payment_label: None, validate_success_action_url: None, @@ -512,6 +515,7 @@ pub(crate) mod tests { .lnurl_pay(LnUrlPayRequest { data: pay_req, amount_msat: user_amount_msat, + use_trampoline: false, comment: Some(comment), payment_label: None, validate_success_action_url: None, @@ -561,6 +565,7 @@ pub(crate) mod tests { .lnurl_pay(LnUrlPayRequest { data: pay_req, amount_msat: user_amount_msat, + use_trampoline: false, comment: Some(comment), payment_label: None, validate_success_action_url: None, @@ -592,6 +597,7 @@ pub(crate) mod tests { .lnurl_pay(LnUrlPayRequest { data: pay_req, amount_msat: user_amount_msat, + use_trampoline: false, comment: Some(comment), payment_label: None, validate_success_action_url: None, @@ -633,6 +639,7 @@ pub(crate) mod tests { .lnurl_pay(LnUrlPayRequest { data: pay_req, amount_msat: user_amount_msat, + use_trampoline: false, comment: Some(comment), payment_label: None, validate_success_action_url: None, @@ -693,6 +700,7 @@ pub(crate) mod tests { comment: Some(comment), payment_label: None, validate_success_action_url: Some(true), + use_trampoline: false, }) .await; // An invalid Success Action URL results in an error @@ -727,6 +735,7 @@ pub(crate) mod tests { comment: Some(comment), payment_label: None, validate_success_action_url: Some(false), + use_trampoline: false, }) .await? { @@ -811,6 +820,7 @@ pub(crate) mod tests { .lnurl_pay(LnUrlPayRequest { data: pay_req, amount_msat: user_amount_msat, + use_trampoline: false, comment: Some(comment), payment_label: None, validate_success_action_url: None, @@ -896,6 +906,7 @@ pub(crate) mod tests { .lnurl_pay(LnUrlPayRequest { data: pay_req, amount_msat: user_amount_msat, + use_trampoline: false, comment: Some(comment), payment_label: None, validate_success_action_url: None, diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index 951556ecf..533d77a11 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -847,6 +847,10 @@ pub struct ReceivePaymentResponse { pub struct SendPaymentRequest { /// The bolt11 invoice pub bolt11: String, + /// Trampoline payments outsource pathfinding to the LSP. Trampoline payments can improve + /// payment performance, but are generally more expensive in terms of fees and they + /// compromise on privacy. + pub use_trampoline: bool, /// The amount to pay in millisatoshis. Should only be set when `bolt11` is a zero-amount invoice. pub amount_msat: Option, /// The external label or identifier of the [Payment] diff --git a/libs/sdk-flutter/ios/Classes/bridge_generated.h b/libs/sdk-flutter/ios/Classes/bridge_generated.h index bd79e19cd..61c276508 100644 --- a/libs/sdk-flutter/ios/Classes/bridge_generated.h +++ b/libs/sdk-flutter/ios/Classes/bridge_generated.h @@ -114,6 +114,7 @@ typedef struct wire_ListPaymentsRequest { typedef struct wire_SendPaymentRequest { struct wire_uint_8_list *bolt11; + bool use_trampoline; uint64_t *amount_msat; struct wire_uint_8_list *label; } wire_SendPaymentRequest; @@ -169,6 +170,7 @@ typedef struct wire_LnUrlPayRequestData { typedef struct wire_LnUrlPayRequest { struct wire_LnUrlPayRequestData data; uint64_t amount_msat; + bool use_trampoline; struct wire_uint_8_list *comment; struct wire_uint_8_list *payment_label; bool *validate_success_action_url; diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart index 467cd1248..732ef3261 100644 --- a/libs/sdk-flutter/lib/bridge_generated.dart +++ b/libs/sdk-flutter/lib/bridge_generated.dart @@ -862,6 +862,7 @@ class LnUrlPayErrorData { class LnUrlPayRequest { final LnUrlPayRequestData data; final int amountMsat; + final bool useTrampoline; final String? comment; final String? paymentLabel; final bool? validateSuccessActionUrl; @@ -869,6 +870,7 @@ class LnUrlPayRequest { const LnUrlPayRequest({ required this.data, required this.amountMsat, + required this.useTrampoline, this.comment, this.paymentLabel, this.validateSuccessActionUrl, @@ -1704,6 +1706,11 @@ class SendPaymentRequest { /// The bolt11 invoice final String bolt11; + /// Trampoline payments outsource pathfinding to the LSP. Trampoline payments can improve + /// payment performance, but are generally more expensive in terms of fees and they + /// compromise on privacy. + final bool useTrampoline; + /// The amount to pay in millisatoshis. Should only be set when `bolt11` is a zero-amount invoice. final int? amountMsat; @@ -1712,6 +1719,7 @@ class SendPaymentRequest { const SendPaymentRequest({ required this.bolt11, + required this.useTrampoline, this.amountMsat, this.label, }); @@ -4840,6 +4848,7 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase { void _api_fill_to_wire_ln_url_pay_request(LnUrlPayRequest apiObj, wire_LnUrlPayRequest wireObj) { _api_fill_to_wire_ln_url_pay_request_data(apiObj.data, wireObj.data); wireObj.amount_msat = api2wire_u64(apiObj.amountMsat); + wireObj.use_trampoline = api2wire_bool(apiObj.useTrampoline); wireObj.comment = api2wire_opt_String(apiObj.comment); wireObj.payment_label = api2wire_opt_String(apiObj.paymentLabel); wireObj.validate_success_action_url = api2wire_opt_box_autoadd_bool(apiObj.validateSuccessActionUrl); @@ -4999,6 +5008,7 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase { void _api_fill_to_wire_send_payment_request(SendPaymentRequest apiObj, wire_SendPaymentRequest wireObj) { wireObj.bolt11 = api2wire_String(apiObj.bolt11); + wireObj.use_trampoline = api2wire_bool(apiObj.useTrampoline); wireObj.amount_msat = api2wire_opt_box_autoadd_u64(apiObj.amountMsat); wireObj.label = api2wire_opt_String(apiObj.label); } @@ -6576,6 +6586,9 @@ final class wire_ListPaymentsRequest extends ffi.Struct { final class wire_SendPaymentRequest extends ffi.Struct { external ffi.Pointer bolt11; + @ffi.Bool() + external bool use_trampoline; + external ffi.Pointer amount_msat; external ffi.Pointer label; @@ -6671,6 +6684,9 @@ final class wire_LnUrlPayRequest extends ffi.Struct { @ffi.Uint64() external int amount_msat; + @ffi.Bool() + external bool use_trampoline; + external ffi.Pointer comment; external ffi.Pointer payment_label; diff --git a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt index 6fe004123..181899eb2 100644 --- a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt +++ b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt @@ -1150,6 +1150,7 @@ fun asLnUrlPayRequest(lnUrlPayRequest: ReadableMap): LnUrlPayRequest? { arrayOf( "data", "amountMsat", + "useTrampoline", ), ) ) { @@ -1157,6 +1158,7 @@ fun asLnUrlPayRequest(lnUrlPayRequest: ReadableMap): LnUrlPayRequest? { } val data = lnUrlPayRequest.getMap("data")?.let { asLnUrlPayRequestData(it) }!! val amountMsat = lnUrlPayRequest.getDouble("amountMsat").toULong() + val useTrampoline = lnUrlPayRequest.getBoolean("useTrampoline") val comment = if (hasNonNullKey(lnUrlPayRequest, "comment")) lnUrlPayRequest.getString("comment") else null val paymentLabel = if (hasNonNullKey(lnUrlPayRequest, "paymentLabel")) lnUrlPayRequest.getString("paymentLabel") else null val validateSuccessActionUrl = @@ -1172,6 +1174,7 @@ fun asLnUrlPayRequest(lnUrlPayRequest: ReadableMap): LnUrlPayRequest? { return LnUrlPayRequest( data, amountMsat, + useTrampoline, comment, paymentLabel, validateSuccessActionUrl, @@ -1182,6 +1185,7 @@ fun readableMapOf(lnUrlPayRequest: LnUrlPayRequest): ReadableMap = readableMapOf( "data" to readableMapOf(lnUrlPayRequest.data), "amountMsat" to lnUrlPayRequest.amountMsat, + "useTrampoline" to lnUrlPayRequest.useTrampoline, "comment" to lnUrlPayRequest.comment, "paymentLabel" to lnUrlPayRequest.paymentLabel, "validateSuccessActionUrl" to lnUrlPayRequest.validateSuccessActionUrl, @@ -3199,16 +3203,19 @@ fun asSendPaymentRequest(sendPaymentRequest: ReadableMap): SendPaymentRequest? { sendPaymentRequest, arrayOf( "bolt11", + "useTrampoline", ), ) ) { return null } val bolt11 = sendPaymentRequest.getString("bolt11")!! + val useTrampoline = sendPaymentRequest.getBoolean("useTrampoline") val amountMsat = if (hasNonNullKey(sendPaymentRequest, "amountMsat")) sendPaymentRequest.getDouble("amountMsat").toULong() else null val label = if (hasNonNullKey(sendPaymentRequest, "label")) sendPaymentRequest.getString("label") else null return SendPaymentRequest( bolt11, + useTrampoline, amountMsat, label, ) @@ -3217,6 +3224,7 @@ fun asSendPaymentRequest(sendPaymentRequest: ReadableMap): SendPaymentRequest? { fun readableMapOf(sendPaymentRequest: SendPaymentRequest): ReadableMap = readableMapOf( "bolt11" to sendPaymentRequest.bolt11, + "useTrampoline" to sendPaymentRequest.useTrampoline, "amountMsat" to sendPaymentRequest.amountMsat, "label" to sendPaymentRequest.label, ) diff --git a/libs/sdk-react-native/ios/BreezSDKMapper.swift b/libs/sdk-react-native/ios/BreezSDKMapper.swift index 3031b3c9f..e864f4e40 100644 --- a/libs/sdk-react-native/ios/BreezSDKMapper.swift +++ b/libs/sdk-react-native/ios/BreezSDKMapper.swift @@ -1322,6 +1322,9 @@ enum BreezSDKMapper { guard let amountMsat = lnUrlPayRequest["amountMsat"] as? UInt64 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "amountMsat", typeName: "LnUrlPayRequest")) } + guard let useTrampoline = lnUrlPayRequest["useTrampoline"] as? Bool else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "useTrampoline", typeName: "LnUrlPayRequest")) + } var comment: String? if hasNonNilKey(data: lnUrlPayRequest, key: "comment") { guard let commentTmp = lnUrlPayRequest["comment"] as? String else { @@ -1347,6 +1350,7 @@ enum BreezSDKMapper { return LnUrlPayRequest( data: data, amountMsat: amountMsat, + useTrampoline: useTrampoline, comment: comment, paymentLabel: paymentLabel, validateSuccessActionUrl: validateSuccessActionUrl @@ -1357,6 +1361,7 @@ enum BreezSDKMapper { return [ "data": dictionaryOf(lnUrlPayRequestData: lnUrlPayRequest.data), "amountMsat": lnUrlPayRequest.amountMsat, + "useTrampoline": lnUrlPayRequest.useTrampoline, "comment": lnUrlPayRequest.comment == nil ? nil : lnUrlPayRequest.comment, "paymentLabel": lnUrlPayRequest.paymentLabel == nil ? nil : lnUrlPayRequest.paymentLabel, "validateSuccessActionUrl": lnUrlPayRequest.validateSuccessActionUrl == nil ? nil : lnUrlPayRequest.validateSuccessActionUrl, @@ -3602,6 +3607,9 @@ enum BreezSDKMapper { guard let bolt11 = sendPaymentRequest["bolt11"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "bolt11", typeName: "SendPaymentRequest")) } + guard let useTrampoline = sendPaymentRequest["useTrampoline"] as? Bool else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "useTrampoline", typeName: "SendPaymentRequest")) + } var amountMsat: UInt64? if hasNonNilKey(data: sendPaymentRequest, key: "amountMsat") { guard let amountMsatTmp = sendPaymentRequest["amountMsat"] as? UInt64 else { @@ -3619,6 +3627,7 @@ enum BreezSDKMapper { return SendPaymentRequest( bolt11: bolt11, + useTrampoline: useTrampoline, amountMsat: amountMsat, label: label ) @@ -3627,6 +3636,7 @@ enum BreezSDKMapper { static func dictionaryOf(sendPaymentRequest: SendPaymentRequest) -> [String: Any?] { return [ "bolt11": sendPaymentRequest.bolt11, + "useTrampoline": sendPaymentRequest.useTrampoline, "amountMsat": sendPaymentRequest.amountMsat == nil ? nil : sendPaymentRequest.amountMsat, "label": sendPaymentRequest.label == nil ? nil : sendPaymentRequest.label, ] diff --git a/libs/sdk-react-native/src/index.ts b/libs/sdk-react-native/src/index.ts index cfcf50f2c..d434d039d 100644 --- a/libs/sdk-react-native/src/index.ts +++ b/libs/sdk-react-native/src/index.ts @@ -191,6 +191,7 @@ export interface LnUrlPayErrorData { export interface LnUrlPayRequest { data: LnUrlPayRequestData amountMsat: number + useTrampoline: boolean comment?: string paymentLabel?: string validateSuccessActionUrl?: boolean @@ -498,6 +499,7 @@ export interface SendOnchainResponse { export interface SendPaymentRequest { bolt11: string + useTrampoline: boolean amountMsat?: number label?: string } diff --git a/tools/sdk-cli/src/command_handlers.rs b/tools/sdk-cli/src/command_handlers.rs index 19a4e4a71..0c00e081e 100644 --- a/tools/sdk-cli/src/command_handlers.rs +++ b/tools/sdk-cli/src/command_handlers.rs @@ -245,12 +245,14 @@ pub(crate) async fn handle_command( bolt11, amount_msat, label, + use_trampoline, } => { let payment = sdk()? .send_payment(SendPaymentRequest { bolt11, amount_msat, label, + use_trampoline, }) .await?; serde_json::to_string_pretty(&payment).map_err(|e| e.into()) @@ -470,6 +472,7 @@ pub(crate) async fn handle_command( lnurl, label, validate_success_url, + use_trampoline, } => match parse(&lnurl).await? { LnUrlPay { data: pd } => { let prompt = format!( @@ -482,6 +485,7 @@ pub(crate) async fn handle_command( .lnurl_pay(LnUrlPayRequest { data: pd, amount_msat: amount_msat.parse::()?, + use_trampoline, comment: None, payment_label: label, validate_success_action_url: validate_success_url, diff --git a/tools/sdk-cli/src/commands.rs b/tools/sdk-cli/src/commands.rs index ef8cbc4a4..5b69dd792 100644 --- a/tools/sdk-cli/src/commands.rs +++ b/tools/sdk-cli/src/commands.rs @@ -50,6 +50,10 @@ pub(crate) enum Commands { /// The external label or identifier of the payment #[clap(name = "label", short = 'l', long = "label")] label: Option, + + /// If use_trampoline is set, trampoline payments will be attempted. + #[clap(long, action)] + use_trampoline: bool, }, /// [pay] Send a spontaneous (keysend) payment @@ -97,6 +101,10 @@ pub(crate) enum Commands { /// Validates the success action URL #[clap(name = "validate_success_url", short = 'v', long = "validate")] validate_success_url: Option, + + /// If use_trampoline is set, trampoline payments will be attempted. + #[clap(long, action)] + use_trampoline: bool, }, /// [lnurl] Withdraw using lnurl withdraw From 75a21b3dd3f78c1d8321fd168e3b1182d3bd4074 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 12 Aug 2024 12:20:54 +0200 Subject: [PATCH 5/8] add trampoline client support extract trampoline payment data from label --- libs/sdk-core/src/greenlight/node_api.rs | 76 +++++++++++++++++++++--- libs/sdk-core/src/node_api.rs | 7 +++ libs/sdk-core/src/test_utils.rs | 11 ++++ 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index 8d7129093..ba47d239e 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -22,7 +22,7 @@ use gl_client::pb::cln::{ }; use gl_client::pb::scheduler::scheduler_client::SchedulerClient; use gl_client::pb::scheduler::{NodeInfoRequest, UpgradeRequest}; -use gl_client::pb::{OffChainPayment, PayStatus}; +use gl_client::pb::{OffChainPayment, PayStatus, TrampolinePayRequest}; use gl_client::scheduler::Scheduler; use gl_client::signer::model::greenlight::{amount, scheduler}; use gl_client::signer::{Error, Signer}; @@ -72,6 +72,14 @@ struct InvoiceLabel { pub payer_amount_msat: Option, } +#[derive(Serialize, Deserialize)] +struct PaymentLabel { + pub unix_nano: u128, + pub trampoline: bool, + pub client_label: Option, + pub amount_msat: u64, +} + impl Greenlight { /// Connects to a live node using the provided seed and config. /// If the node is not registered, it will try to recover it using the seed. @@ -1147,6 +1155,47 @@ impl NodeAPI for Greenlight { payment.try_into() } + async fn send_trampoline_payment( + &self, + bolt11: String, + amount_msat: u64, + label: Option, + trampoline_node_id: Vec, + ) -> NodeResult { + let invoice = parse_invoice(&bolt11)?; + validate_network(invoice.clone(), self.sdk_config.network)?; + let label = serde_json::to_string(&PaymentLabel { + trampoline: true, + client_label: label, + unix_nano: SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(), + amount_msat, + })?; + let mut client = self.get_client().await?; + let request = TrampolinePayRequest { + bolt11, + trampoline_node_id, + amount_msat, + label, + maxdelay: u32::default(), + description: String::default(), + maxfeepercent: f32::default(), + }; + let result = self + .with_keep_alive(client.trampoline_pay(request)) + .await? + .into_inner(); + + let client = self.get_node_client().await?; + + // Before returning from send_payment we need to make sure it is + // persisted in the backend node. We do so by polling for the payment. + // TODO: Ensure this works with trampoline payments + // NOTE: If this doesn't work with trampoline payments, the sync also + // needs updating. + let payment = Self::fetch_outgoing_payment_with_retry(client, result.payment_hash).await?; + payment.try_into() + } + async fn send_spontaneous_payment( &self, node_id: String, @@ -2114,16 +2163,29 @@ impl TryFrom for Payment { .as_ref() .ok_or(InvoiceError::generic("No bolt11 invoice")) .and_then(|b| parse_invoice(b)); - let payment_amount = payment - .amount_msat - .clone() - .map(|a| a.msat) - .unwrap_or_default(); let payment_amount_sent = payment .amount_sent_msat .clone() .map(|a| a.msat) .unwrap_or_default(); + + // For trampoline payments the amount_msat doesn't match the actual + // amount. If it's a trampoline payment, take the amount from the label. + let (payment_amount, client_label) = serde_json::from_str::(payment.label()) + .ok() + .and_then(|label| { + label + .trampoline + .then_some((label.amount_msat, label.client_label)) + }) + .unwrap_or(( + payment + .amount_msat + .clone() + .map(|a| a.msat) + .unwrap_or_default(), + payment.label.clone(), + )); let status = payment.status().into(); Ok(Payment { @@ -2143,7 +2205,7 @@ impl TryFrom for Payment { details: PaymentDetails::Ln { data: LnPaymentDetails { payment_hash: hex::encode(payment.payment_hash), - label: payment.label.unwrap_or_default(), + label: client_label.unwrap_or_default(), destination_pubkey: payment.destination.map(hex::encode).unwrap_or_default(), payment_preimage: payment.preimage.map(hex::encode).unwrap_or_default(), keysend: payment.bolt11.is_none(), diff --git a/libs/sdk-core/src/node_api.rs b/libs/sdk-core/src/node_api.rs index 5de808263..171a935e1 100644 --- a/libs/sdk-core/src/node_api.rs +++ b/libs/sdk-core/src/node_api.rs @@ -137,6 +137,13 @@ pub trait NodeAPI: Send + Sync { extra_tlvs: Option>, label: Option, ) -> NodeResult; + async fn send_trampoline_payment( + &self, + bolt11: String, + amount_msat: u64, + label: Option, + trampoline_node_id: Vec, + ) -> NodeResult; async fn start(&self) -> NodeResult; /// Attempts to find a payment path "manually" and send the htlcs in a way that will drain diff --git a/libs/sdk-core/src/test_utils.rs b/libs/sdk-core/src/test_utils.rs index 5d3ae7ab5..626b1ab20 100644 --- a/libs/sdk-core/src/test_utils.rs +++ b/libs/sdk-core/src/test_utils.rs @@ -381,6 +381,17 @@ impl NodeAPI for MockNodeAPI { Ok(payment) } + async fn send_trampoline_payment( + &self, + bolt11: String, + _amount_msat: u64, + _label: Option, + _trampoline_id: Vec, + ) -> NodeResult { + let payment = self.add_dummy_payment_for(bolt11, None, None).await?; + Ok(payment) + } + async fn send_spontaneous_payment( &self, _node_id: String, From 42c0c074dfcd00ab175f26f4effe9256dbb34184 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 12 Aug 2024 12:21:04 +0200 Subject: [PATCH 6/8] add trampoline payment support --- libs/sdk-core/src/breez_services.rs | 102 ++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 181c9b955..8ed964118 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -289,33 +289,105 @@ impl BreezServices { } }; - match self + if self .persister .get_completed_payment_by_hash(&parsed_invoice.payment_hash)? + .is_some() { - Some(_) => Err(SendPaymentError::AlreadyPaid), + return Err(SendPaymentError::AlreadyPaid); + } + + // If there is an lsp, the invoice route hint does not contain the + // lsp in the hint, and trampoline payments are requested, attempt a + // trampoline payment. + let maybe_trampoline_id = self.get_trampoline_id(&req, &parsed_invoice)?; + + self.persist_pending_payment(&parsed_invoice, amount_msat, req.label.clone())?; + + // If trampoline is an option, try trampoline first. + let trampoline_result = if let Some(trampoline_id) = maybe_trampoline_id { + debug!("attempting trampoline payment"); + match self + .node_api + .send_trampoline_payment( + parsed_invoice.bolt11.clone(), + amount_msat, + req.label.clone(), + trampoline_id, + ) + .await + { + Ok(res) => Some(res), + Err(e) => { + warn!("trampoline payment failed: {:?}", e); + None + } + } + } else { + debug!("not attempting trampoline payment"); + None + }; + + // If trampoline failed or didn't happen, fall back to regular payment. + let payment_res = match trampoline_result { + Some(res) => Ok(res), None => { - self.persist_pending_payment(&parsed_invoice, amount_msat, req.label.clone())?; - let payment_res = self - .node_api + debug!("attempting normal payment"); + self.node_api .send_payment( parsed_invoice.bolt11.clone(), req.amount_msat, req.label.clone(), ) .map_err(Into::into) - .await; - let payment = self - .on_payment_completed( - parsed_invoice.payee_pubkey.clone(), - Some(parsed_invoice), - req.label, - payment_res, - ) - .await?; - Ok(SendPaymentResponse { payment }) + .await } + }; + + let payment = self + .on_payment_completed( + parsed_invoice.payee_pubkey.clone(), + Some(parsed_invoice), + req.label, + payment_res, + ) + .await?; + Ok(SendPaymentResponse { payment }) + } + + fn get_trampoline_id( + &self, + req: &SendPaymentRequest, + invoice: &LNInvoice, + ) -> Result>, SendPaymentError> { + // If trampoline is turned off, return immediately + if !req.use_trampoline { + return Ok(None); } + + // Get the persisted LSP id. If no LSP, return early. + let lsp_pubkey = match self.persister.get_lsp_pubkey()? { + Some(lsp_pubkey) => lsp_pubkey, + None => return Ok(None), + }; + + // If the LSP is in the routing hint, don't use trampoline, but rather + // pay directly to the destination. + if invoice.routing_hints.iter().any(|hint| { + hint.hops + .last() + .map(|hop| hop.src_node_id == lsp_pubkey) + .unwrap_or(false) + }) { + return Ok(None); + } + + // If ended up here, this payment will attempt trampoline. + Ok(Some(hex::decode(lsp_pubkey).map_err(|_| { + SendPaymentError::Generic { + err: "failed to decode lsp pubkey".to_string(), + } + })?)) } /// Pay directly to a node id using keysend From 1fd05f3489fa065626a9fdc3a8a2d7c34aca1e53 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Fri, 12 Jul 2024 17:04:08 +0200 Subject: [PATCH 7/8] saturating sub payment fee --- libs/sdk-core/src/greenlight/node_api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index ba47d239e..df3b10945 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -2198,7 +2198,7 @@ impl TryFrom for Payment { .as_ref() .map_or(0, |i| i.amount_msat.unwrap_or_default()), }, - fee_msat: payment_amount_sent - payment_amount, + fee_msat: payment_amount_sent.saturating_sub(payment_amount), status, error: None, description: ln_invoice.map(|i| i.description).unwrap_or_default(), From 985bfedac357ca435f1a1a2bfff32a11ab9700e5 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Thu, 15 Aug 2024 10:25:45 +0200 Subject: [PATCH 8/8] set lsp id and pubkey in the same function This update ensures that lsp pubkey and lsp id are always set in the same call. Sometimes the lsp pubkey is not yet available. Then only the lsp id will be set. If the lsp id changes, the pubkey is then removed. If the lsp id remains the same, the node pubkey will still be persisted. --- libs/sdk-core/src/breez_services.rs | 12 +++++------- libs/sdk-core/src/persist/settings.rs | 28 ++++++++++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 8ed964118..1818a7b43 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -755,8 +755,7 @@ impl BreezServices { } }; - self.persister.set_lsp_id(lsp_id)?; - self.persister.set_lsp_pubkey(lsp_pubkey)?; + self.persister.set_lsp(lsp_id, Some(lsp_pubkey))?; self.sync().await?; if let Some(webhook_url) = self.persister.get_webhook_url()? { self.register_payment_notifications(webhook_url).await? @@ -1257,8 +1256,7 @@ impl BreezServices { None => return Ok(()), }; - self.persister.set_lsp_id(lsp.id)?; - self.persister.set_lsp_pubkey(lsp.pubkey.clone())?; + self.persister.set_lsp(lsp.id, Some(lsp.pubkey.clone()))?; let node_state = match self.node_info() { Ok(node_state) => node_state, Err(_) => return Ok(()), @@ -2409,7 +2407,7 @@ impl BreezServicesBuilder { let current_lsp_id = persister.get_lsp_id()?; if current_lsp_id.is_none() && self.config.default_lsp_id.is_some() { - persister.set_lsp_id(self.config.default_lsp_id.clone().unwrap())?; + persister.set_lsp(self.config.default_lsp_id.clone().unwrap(), None)?; } let payment_receiver = Arc::new(PaymentReceiver { @@ -3233,7 +3231,7 @@ pub(crate) mod tests { let node_api = Arc::new(MockNodeAPI::new(dummy_node_state.clone())); let breez_server = Arc::new(MockBreezServer {}); - persister.set_lsp_id(breez_server.lsp_id()).unwrap(); + persister.set_lsp(breez_server.lsp_id(), None).unwrap(); persister.set_node_state(&dummy_node_state).unwrap(); let receiver: Arc = Arc::new(PaymentReceiver { @@ -3340,7 +3338,7 @@ pub(crate) mod tests { let persister = Arc::new(create_test_persister(test_config.clone())); persister.init()?; persister.insert_or_update_payments(&known_payments, false)?; - persister.set_lsp_id(MockBreezServer {}.lsp_id())?; + persister.set_lsp(MockBreezServer {}.lsp_id(), None)?; let mut builder = BreezServicesBuilder::new(test_config.clone()); let breez_services = builder diff --git a/libs/sdk-core/src/persist/settings.rs b/libs/sdk-core/src/persist/settings.rs index 2272a7ddd..6dccc5996 100644 --- a/libs/sdk-core/src/persist/settings.rs +++ b/libs/sdk-core/src/persist/settings.rs @@ -51,19 +51,33 @@ impl SqliteStorage { Ok(vec) } - pub fn set_lsp_id(&self, lsp_id: String) -> PersistResult<()> { - self.update_setting("lsp".to_string(), lsp_id) + pub fn set_lsp(&self, lsp_id: String, pubkey: Option) -> PersistResult<()> { + if let Some(pubkey) = pubkey { + self.update_setting("lsp".to_string(), lsp_id)?; + self.update_setting("lsp-pubkey".to_string(), pubkey)?; + return Ok(()); + } + + match self.get_setting("lsp".to_string())? { + Some(old_lsp_id) => { + if old_lsp_id != lsp_id { + self.update_setting("lsp".to_string(), lsp_id)?; + self.delete_setting("lsp-pubkey".to_string())?; + } + } + None => { + self.update_setting("lsp".to_string(), lsp_id)?; + self.delete_setting("lsp-pubkey".to_string())?; + } + }; + + Ok(()) } pub fn get_lsp_id(&self) -> PersistResult> { self.get_setting("lsp".to_string()) } - pub fn set_lsp_pubkey(&self, pubkey: String) -> PersistResult<()> { - self.update_setting("lsp-pubkey".to_string(), pubkey) - } - - #[allow(dead_code)] pub fn get_lsp_pubkey(&self) -> PersistResult> { self.get_setting("lsp-pubkey".to_string()) }