From e5fab1aedc415153ea01e2b7ecb5f7f92ae9ec12 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 7 Oct 2024 15:41:03 +0200 Subject: [PATCH 01/14] =?UTF-8?q?Strip=20=E2=82=BF=20prefix=20from=20light?= =?UTF-8?q?ning=20address?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/sdk-common/src/input_parser.rs | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/libs/sdk-common/src/input_parser.rs b/libs/sdk-common/src/input_parser.rs index 3170f8a97..fc8b8d3cb 100644 --- a/libs/sdk-common/src/input_parser.rs +++ b/libs/sdk-common/src/input_parser.rs @@ -248,6 +248,13 @@ fn ln_address_decode(ln_address: &str) -> Result<(String, String, String)> { if ln_address.contains('@') { let split = ln_address.split('@').collect::>(); let user = split[0].to_lowercase(); + + // BIP-353 addresses have a ₿ prefix. Some users will want to use it as + // lnurl, so strip the prefix if it's there. + let user = user + .strip_prefix('₿') + .map(|p| p.to_string()) + .unwrap_or(user); // It is safe to downcase the domains since they are case-insensitive. // https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 let domain = split[1].to_lowercase(); @@ -1411,6 +1418,29 @@ pub(crate) mod tests { Ok(()) } + #[tokio::test] + async fn test_lnurl_pay_lud_16_ln_address_with_prefix() -> Result<(), Box> + { + // Covers cases in LUD-16, with BIP-353 prefix. + + let ln_address = "₿user@domain.net"; + let server_ln_address = "user@domain.net"; + let _m = mock_lnurl_ln_address_endpoint(server_ln_address, None)?; + + if let InputType::LnUrlPay { data: pd } = parse(ln_address).await? { + assert_eq!(pd.callback, "https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"); + assert_eq!(pd.max_sendable, 16000); + assert_eq!(pd.min_sendable, 4000); + assert_eq!(pd.comment_allowed, 0); + assert_eq!(pd.domain, "domain.net"); + assert_eq!(pd.ln_address, Some(server_ln_address.to_string())); + } else { + panic!("input was not ln address") + } + + Ok(()) + } + #[tokio::test] async fn test_lnurl_pay_lud_16_ln_address_error() -> Result<()> { // Covers cases in LUD-16: Paying to static internet identifiers (LN Address) From ea31c1ffa337915b07618da5956c1ceae2345bac Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:49:45 +0000 Subject: [PATCH 02/14] Bump gl-client dependency (#1100) --- libs/Cargo.lock | 4 ++-- libs/sdk-core/Cargo.toml | 2 +- tools/sdk-cli/Cargo.lock | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/Cargo.lock b/libs/Cargo.lock index 02a7e5269..3a866f66d 100644 --- a/libs/Cargo.lock +++ b/libs/Cargo.lock @@ -1470,8 +1470,8 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gl-client" -version = "0.2.0" -source = "git+https://github.com/Blockstream/greenlight.git?rev=7540980dd8f1630b1b148a9eb8b44e2b05ca7e6d#7540980dd8f1630b1b148a9eb8b44e2b05ca7e6d" +version = "0.3.0" +source = "git+https://github.com/Blockstream/greenlight.git?rev=a0e4faf389484e426fcc197f8726c72a27eef32e#a0e4faf389484e426fcc197f8726c72a27eef32e" dependencies = [ "anyhow", "async-stream", diff --git a/libs/sdk-core/Cargo.toml b/libs/sdk-core/Cargo.toml index 1a1fb5495..599d42231 100644 --- a/libs/sdk-core/Cargo.toml +++ b/libs/sdk-core/Cargo.toml @@ -14,7 +14,7 @@ anyhow = { workspace = true } hex = { workspace = true } gl-client = { git = "https://github.com/Blockstream/greenlight.git", features = [ "permissive", -], rev = "7540980dd8f1630b1b148a9eb8b44e2b05ca7e6d" } +], rev = "a0e4faf389484e426fcc197f8726c72a27eef32e" } 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 b9cd3423a..35f5b7eb4 100644 --- a/tools/sdk-cli/Cargo.lock +++ b/tools/sdk-cli/Cargo.lock @@ -1331,8 +1331,8 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gl-client" -version = "0.2.0" -source = "git+https://github.com/Blockstream/greenlight.git?rev=7540980dd8f1630b1b148a9eb8b44e2b05ca7e6d#7540980dd8f1630b1b148a9eb8b44e2b05ca7e6d" +version = "0.3.0" +source = "git+https://github.com/Blockstream/greenlight.git?rev=a0e4faf389484e426fcc197f8726c72a27eef32e#a0e4faf389484e426fcc197f8726c72a27eef32e" dependencies = [ "anyhow", "async-stream", From d0c8e7caf543cd6ef76185a490585f767b4053da Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Tue, 8 Oct 2024 16:37:00 +0300 Subject: [PATCH 03/14] Optimize fetching claim tx by querying the lockup address --- libs/sdk-core/src/swap_out/reverseswap.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/libs/sdk-core/src/swap_out/reverseswap.rs b/libs/sdk-core/src/swap_out/reverseswap.rs index bc7a94146..b61120ca6 100644 --- a/libs/sdk-core/src/swap_out/reverseswap.rs +++ b/libs/sdk-core/src/swap_out/reverseswap.rs @@ -533,13 +533,17 @@ impl BTCSendSwap { let lockup_addr = rsi.get_lockup_address(self.config.network)?; Ok(self .chain_service - .address_transactions(rsi.claim_pubkey.clone()) + .address_transactions(lockup_addr.to_string()) .await? .into_iter() .find(|tx| { tx.vin .iter() .any(|vin| vin.prevout.scriptpubkey_address == lockup_addr.to_string()) + && tx + .vout + .iter() + .any(|vout| vout.scriptpubkey_address == rsi.claim_pubkey.clone()) })) } @@ -646,6 +650,12 @@ impl BTCSendSwap { let claim_tx = self.get_claim_tx(&rsi).await?; let claim_tx_status = TxStatus::from(&claim_tx); + if let Some(tx) = &claim_tx { + info!( + "Cound claim tx for reverse swap {:?}: {:?}, status: {:?}", + rsi.id, tx.txid, claim_tx_status + ); + } // Update cached state when new state is detected if let Some(new_status) = self .get_status_update_for_monitored(&rsi, claim_tx_status) From a38ed8ef87677a38bbcc86bd0a76e92522d66c6f Mon Sep 17 00:00:00 2001 From: Ross Savage <551697+dangeross@users.noreply.github.com> Date: Wed, 9 Oct 2024 08:08:34 +0200 Subject: [PATCH 04/14] Merge pull request #1086 from breez/dependabot/npm_and_yarn/libs/sdk-react-native/serve-static-1.16.2 Bump serve-static from 1.15.0 to 1.16.2 in /libs/sdk-react-native --- libs/sdk-react-native/yarn.lock | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/libs/sdk-react-native/yarn.lock b/libs/sdk-react-native/yarn.lock index 15a4fee2d..2c5d5e5ec 100644 --- a/libs/sdk-react-native/yarn.lock +++ b/libs/sdk-react-native/yarn.lock @@ -4351,6 +4351,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -9243,10 +9248,10 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -9268,14 +9273,14 @@ serialize-error@^2.1.0: integrity sha1-ULZ51WNc34Rme9yOWa9OW4HV9go= serve-static@^1.13.1: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" set-blocking@^2.0.0: version "2.0.0" From d9a5bc98c82d7a9f0935562c16d263afdcc0d655 Mon Sep 17 00:00:00 2001 From: Ross Savage <551697+dangeross@users.noreply.github.com> Date: Wed, 9 Oct 2024 08:09:03 +0200 Subject: [PATCH 05/14] Merge pull request #1085 from breez/dependabot/npm_and_yarn/libs/sdk-react-native/example/serve-static-1.16.2 Bump serve-static from 1.15.0 to 1.16.2 in /libs/sdk-react-native/example --- libs/sdk-react-native/example/yarn.lock | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/libs/sdk-react-native/example/yarn.lock b/libs/sdk-react-native/example/yarn.lock index 5467dc3bf..04194659d 100644 --- a/libs/sdk-react-native/example/yarn.lock +++ b/libs/sdk-react-native/example/yarn.lock @@ -2949,6 +2949,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -6586,10 +6591,10 @@ semver@^7.2.1, semver@^7.3.2: dependencies: lru-cache "^6.0.0" -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -6611,14 +6616,14 @@ serialize-error@^2.1.0: integrity sha1-ULZ51WNc34Rme9yOWa9OW4HV9go= serve-static@^1.13.1: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" set-blocking@^2.0.0: version "2.0.0" From b3e8fff05819e9f7a9e4b1b9bb0c83c343ae6554 Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Wed, 9 Oct 2024 10:31:47 +0300 Subject: [PATCH 06/14] fix error --- libs/sdk-core/src/swap_out/reverseswap.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/sdk-core/src/swap_out/reverseswap.rs b/libs/sdk-core/src/swap_out/reverseswap.rs index b61120ca6..4d725e346 100644 --- a/libs/sdk-core/src/swap_out/reverseswap.rs +++ b/libs/sdk-core/src/swap_out/reverseswap.rs @@ -652,7 +652,7 @@ impl BTCSendSwap { if let Some(tx) = &claim_tx { info!( - "Cound claim tx for reverse swap {:?}: {:?}, status: {:?}", + "Found claim tx for reverse swap {:?}: {:?}, status: {:?}", rsi.id, tx.txid, claim_tx_status ); } From 0d952d2dd880896689cced9605f6accb5b786350 Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Wed, 9 Oct 2024 11:34:37 +0300 Subject: [PATCH 07/14] Merge pull request #1102 from breez/optimize-claim-fetch Optimize fetching claim tx by querying the lockup address From 75d6cf1fdfd6e84fca21f01c54e39d779cd97aab Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Fri, 27 Sep 2024 21:09:05 +0200 Subject: [PATCH 08/14] serialize diagnostic data as single json document --- libs/sdk-core/src/breez_services.rs | 63 ++++++++++++------------ libs/sdk-core/src/greenlight/node_api.rs | 29 +++++------ libs/sdk-core/src/models.rs | 10 ---- libs/sdk-core/src/node_api.rs | 5 +- libs/sdk-core/src/test_utils.rs | 7 +-- 5 files changed, 53 insertions(+), 61 deletions(-) diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 06f6d1d74..b27aa9006 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -17,7 +17,7 @@ use reqwest::{header::CONTENT_TYPE, Body, Url}; use sdk_common::grpc; use sdk_common::prelude::*; use serde::Serialize; -use serde_json::json; +use serde_json::{json, Value}; use strum_macros::EnumString; use tokio::sync::{mpsc, watch, Mutex}; use tokio::time::{sleep, MissedTickBehavior}; @@ -1149,7 +1149,9 @@ impl BreezServices { Ok(dev_cmd) => match dev_cmd { DevCommand::GenerateDiagnosticData => self.generate_diagnostic_data().await, }, - Err(_) => Ok(self.node_api.execute_command(command).await?), + Err(_) => Ok(serde_json::to_string_pretty( + &self.node_api.execute_command(command).await?, + )?), } } @@ -1164,14 +1166,17 @@ impl BreezServices { .node_api .generate_diagnostic_data() .await - .unwrap_or_else(|e| e.to_string()); + .unwrap_or_else(|e| json!({"error": e.to_string()})); let sdk_data = self .generate_sdk_diagnostic_data() .await - .unwrap_or_else(|e| e.to_string()); - Ok(format!( - "Diagnostic Timestamp: {now_sec}\nNode Data\n{node_data}\n\nSDK Data\n{sdk_data}" - )) + .unwrap_or_else(|e| json!({"error": e.to_string()})); + let result = json!({ + "timestamp": now_sec, + "node": node_data, + "sdk": sdk_data + }); + Ok(serde_json::to_string_pretty(&result)?) } /// This method sync the local state with the remote node state. @@ -2168,35 +2173,29 @@ impl BreezServices { }) } - async fn generate_sdk_diagnostic_data(&self) -> SdkResult { - let state: String = serde_json::to_string_pretty(&self.persister.get_node_state()?)?; - let payments = serde_json::to_string_pretty( + async fn generate_sdk_diagnostic_data(&self) -> SdkResult { + let state = serde_json::to_value(&self.persister.get_node_state()?)?; + let payments = serde_json::to_value( &self .persister .list_payments(ListPaymentsRequest::default())?, )?; - let channels = serde_json::to_string_pretty(&self.persister.list_channels()?)?; - let settings = serde_json::to_string_pretty(&self.persister.list_settings()?)?; - let reverse_swaps = self - .persister - .list_reverse_swaps() - .map(sanitize_vec_pretty_print)??; - let swaps = self - .persister - .list_swaps() - .map(sanitize_vec_pretty_print)??; - let lsp_id = serde_json::to_string_pretty(&self.persister.get_lsp_id()?)?; - - let res = format!( - "\ - ***Node State***\n{state}\n\n \ - ***Payments***\n{payments}\n\n \ - ***Channels***\n{channels}\n\n \ - ***Settings***\n{settings}\n\n \ - ***Reverse Swaps***\n{reverse_swaps}\n\n \ - ***LSP ID***\n{lsp_id}\n\n \ - ***Swaps***\n{swaps}\n\n" - ); + let channels = serde_json::to_value(&self.persister.list_channels()?)?; + let settings = serde_json::to_value(&self.persister.list_settings()?)?; + let reverse_swaps = + serde_json::to_value(self.persister.list_reverse_swaps().map(sanitize_vec)?)?; + let swaps = serde_json::to_value(self.persister.list_swaps().map(sanitize_vec)?)?; + let lsp_id = serde_json::to_value(&self.persister.get_lsp_id()?)?; + + let res = json!({ + "node_state": state, + "payments": payments, + "channels": channels, + "settings": settings, + "reverse_swaps": reverse_swaps, + "swaps": swaps, + "lsp_id": lsp_id, + }); Ok(res) } } diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index 31d8a60c5..50b3c3c4b 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -27,6 +27,7 @@ use gl_client::signer::model::greenlight::{amount, scheduler}; use gl_client::signer::Signer; use sdk_common::prelude::*; use serde::{Deserialize, Serialize}; +use serde_json::{json, Map, Value}; use strum_macros::{Display, EnumString}; use tokio::sync::{mpsc, watch, Mutex}; use tokio::time::{sleep, MissedTickBehavior}; @@ -1406,7 +1407,7 @@ impl NodeAPI for Greenlight { Ok(hex_vec) } - async fn generate_diagnostic_data(&self) -> NodeResult { + async fn generate_diagnostic_data(&self) -> NodeResult { let all_commands = vec![ NodeCommand::GetInfo.to_string(), NodeCommand::ListPeerChannels.to_string(), @@ -1415,19 +1416,19 @@ impl NodeAPI for Greenlight { NodeCommand::ListInvoices.to_string(), ]; - let mut result = String::new(); + let mut result = Map::new(); for command in all_commands { let command_name = command.clone(); let res = self .execute_command(command) .await - .unwrap_or_else(|e| e.to_string()); - result += &format!("***{command_name}:***\n\n {res}\n\n"); + .unwrap_or_else(|e| json!({ "error": e.to_string() })); + result.insert(command_name, res); } - Ok(result) + Ok(Value::Object(result)) } - async fn execute_command(&self, command: String) -> NodeResult { + async fn execute_command(&self, command: String) -> NodeResult { let node_cmd = NodeCommand::from_str(&command).map_err(|_| anyhow!("Command not found: {command}"))?; match node_cmd { @@ -1439,7 +1440,7 @@ impl NodeAPI for Greenlight { .await? .into_inner(); - Ok(serde_json::to_string_pretty(&resp)?) + Ok(serde_json::to_value(&resp)?) } NodeCommand::ListPeerChannels => { let resp = self @@ -1448,7 +1449,7 @@ impl NodeAPI for Greenlight { .list_peer_channels(cln::ListpeerchannelsRequest::default()) .await? .into_inner(); - Ok(serde_json::to_string_pretty(&resp)?) + Ok(serde_json::to_value(&resp)?) } NodeCommand::ListFunds => { let resp = self @@ -1457,7 +1458,7 @@ impl NodeAPI for Greenlight { .list_funds(cln::ListfundsRequest::default()) .await? .into_inner(); - Ok(serde_json::to_string_pretty(&resp)?) + Ok(serde_json::to_value(&resp)?) } NodeCommand::ListPayments => { let resp = self @@ -1466,7 +1467,7 @@ impl NodeAPI for Greenlight { .list_pays(cln::ListpaysRequest::default()) .await? .into_inner(); - Ok(serde_json::to_string_pretty(&resp)?) + Ok(serde_json::to_value(&resp)?) } NodeCommand::ListInvoices => { let resp = self @@ -1475,7 +1476,7 @@ impl NodeAPI for Greenlight { .list_invoices(cln::ListinvoicesRequest::default()) .await? .into_inner(); - Ok(serde_json::to_string_pretty(&resp)?) + Ok(serde_json::to_value(&resp)?) } NodeCommand::CloseAllChannels => { let peers_res = self @@ -1488,7 +1489,7 @@ impl NodeAPI for Greenlight { self.close_peer_channels(hex::encode(p.id)).await?; } - Ok("All channels were closed".to_string()) + Ok(Value::String("All channels were closed".to_string())) } NodeCommand::GetInfo => { let resp = self @@ -1497,7 +1498,7 @@ impl NodeAPI for Greenlight { .getinfo(cln::GetinfoRequest::default()) .await? .into_inner(); - Ok(serde_json::to_string_pretty(&resp)?) + Ok(serde_json::to_value(&resp)?) } NodeCommand::Stop => { let resp = self @@ -1506,7 +1507,7 @@ impl NodeAPI for Greenlight { .stop(cln::StopRequest::default()) .await? .into_inner(); - Ok(serde_json::to_string_pretty(&resp)?) + Ok(serde_json::to_value(&resp)?) } } } diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index 86b866c41..7375c2120 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -1575,9 +1575,6 @@ impl PaymentPathEdge { } pub(crate) mod sanitize { - use anyhow::Result; - use serde::Serialize; - use crate::{FullReverseSwapInfo, SwapInfo}; pub(crate) trait Sanitize { @@ -1594,13 +1591,6 @@ pub(crate) mod sanitize { .collect::>() } - pub(crate) fn sanitize_vec_pretty_print(vals: Vec) -> Result - where - T: Sanitize + Serialize, - { - serde_json::to_string_pretty(&sanitize_vec(vals)).map_err(anyhow::Error::new) - } - impl Sanitize for FullReverseSwapInfo { fn sanitize(self) -> FullReverseSwapInfo { FullReverseSwapInfo { diff --git a/libs/sdk-core/src/node_api.rs b/libs/sdk-core/src/node_api.rs index 171a935e1..583b9ca49 100644 --- a/libs/sdk-core/src/node_api.rs +++ b/libs/sdk-core/src/node_api.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::pin::Pin; use anyhow::Result; +use serde_json::Value; use tokio::sync::{mpsc, watch}; use tokio_stream::Stream; use tonic::Streaming; @@ -179,8 +180,8 @@ pub trait NodeAPI: Send + Sync { &self, ) -> NodeResult>; async fn static_backup(&self) -> NodeResult>; - async fn execute_command(&self, command: String) -> NodeResult; - async fn generate_diagnostic_data(&self) -> NodeResult; + async fn execute_command(&self, command: String) -> NodeResult; + async fn generate_diagnostic_data(&self) -> NodeResult; async fn sign_message(&self, message: &str) -> NodeResult; async fn check_message(&self, message: &str, pubkey: &str, signature: &str) -> NodeResult; diff --git a/libs/sdk-core/src/test_utils.rs b/libs/sdk-core/src/test_utils.rs index 28ed1f8cb..64483dd05 100644 --- a/libs/sdk-core/src/test_utils.rs +++ b/libs/sdk-core/src/test_utils.rs @@ -13,6 +13,7 @@ use rand::rngs::OsRng; use rand::{random, Rng}; use sdk_common::grpc; use sdk_common::prelude::{FiatAPI, FiatCurrency, Rate}; +use serde_json::{json, Value}; use tokio::sync::{mpsc, watch, Mutex}; use tokio::time::sleep; use tokio_stream::Stream; @@ -465,12 +466,12 @@ impl NodeAPI for MockNodeAPI { Ok(Vec::new()) } - async fn execute_command(&self, _command: String) -> NodeResult { + async fn execute_command(&self, _command: String) -> NodeResult { Err(NodeError::Generic("Not implemented".to_string())) } - async fn generate_diagnostic_data(&self) -> NodeResult { - Ok("".to_string()) + async fn generate_diagnostic_data(&self) -> NodeResult { + Ok(json!({})) } async fn max_sendable_amount( From f47e40a14617f6f877a4b3f7807735e5dce8b6fd Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Sat, 28 Sep 2024 14:24:41 +0200 Subject: [PATCH 09/14] JSON serialize Vec fields as hex serde_json has no simple way to serialize specific field types as hex, until specialization is merged. This commit contains a hack to serialize all Vec fields as hex anyway. It is impossible to check the type of a generic type parameter at runtime. So all values are first serialized, then attempted to deserialize into a Vec. If that works, the found vector will be serialized as a hex string, rather than an array of numbers. This is not a performant machanism, but it gets the job done. As long as it is only used for dev commands and diagnostic data it should be fine. At least all those number arrays in the diagnostic data are now gone, without having to duplicate every struct definition just for serialization. Note that the 'Value' serialization is a copy of the real one. --- libs/Cargo.lock | 5 +- libs/sdk-core/Cargo.toml | 1 + libs/sdk-core/src/breez_services.rs | 22 +- libs/sdk-core/src/greenlight/node_api.rs | 14 +- libs/sdk-core/src/lib.rs | 1 + libs/sdk-core/src/serializer.rs | 896 +++++++++++++++++++++++ tools/sdk-cli/Cargo.lock | 5 +- 7 files changed, 923 insertions(+), 21 deletions(-) create mode 100644 libs/sdk-core/src/serializer.rs diff --git a/libs/Cargo.lock b/libs/Cargo.lock index 3a866f66d..e53a3edb5 100644 --- a/libs/Cargo.lock +++ b/libs/Cargo.lock @@ -647,6 +647,7 @@ dependencies = [ "ripemd", "rusqlite", "rusqlite_migration", + "ryu", "sdk-common", "serde", "serde_json", @@ -3077,9 +3078,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" diff --git a/libs/sdk-core/Cargo.toml b/libs/sdk-core/Cargo.toml index 599d42231..e446f2d90 100644 --- a/libs/sdk-core/Cargo.toml +++ b/libs/sdk-core/Cargo.toml @@ -51,6 +51,7 @@ miniz_oxide = "0.7.1" tokio-stream = "0.1.14" serde_with = "3.3.0" regex = { workspace = true } +ryu = "1.0.18" [dev-dependencies] mockito = { workspace = true } diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index b27aa9006..27362d8a8 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -1149,7 +1149,7 @@ impl BreezServices { Ok(dev_cmd) => match dev_cmd { DevCommand::GenerateDiagnosticData => self.generate_diagnostic_data().await, }, - Err(_) => Ok(serde_json::to_string_pretty( + Err(_) => Ok(crate::serializer::to_string_pretty( &self.node_api.execute_command(command).await?, )?), } @@ -1176,7 +1176,7 @@ impl BreezServices { "node": node_data, "sdk": sdk_data }); - Ok(serde_json::to_string_pretty(&result)?) + Ok(crate::serializer::to_string_pretty(&result)?) } /// This method sync the local state with the remote node state. @@ -2174,18 +2174,20 @@ impl BreezServices { } async fn generate_sdk_diagnostic_data(&self) -> SdkResult { - let state = serde_json::to_value(&self.persister.get_node_state()?)?; - let payments = serde_json::to_value( + let state = crate::serializer::value::to_value(&self.persister.get_node_state()?)?; + let payments = crate::serializer::value::to_value( &self .persister .list_payments(ListPaymentsRequest::default())?, )?; - let channels = serde_json::to_value(&self.persister.list_channels()?)?; - let settings = serde_json::to_value(&self.persister.list_settings()?)?; - let reverse_swaps = - serde_json::to_value(self.persister.list_reverse_swaps().map(sanitize_vec)?)?; - let swaps = serde_json::to_value(self.persister.list_swaps().map(sanitize_vec)?)?; - let lsp_id = serde_json::to_value(&self.persister.get_lsp_id()?)?; + let channels = crate::serializer::value::to_value(&self.persister.list_channels()?)?; + let settings = crate::serializer::value::to_value(&self.persister.list_settings()?)?; + let reverse_swaps = crate::serializer::value::to_value( + self.persister.list_reverse_swaps().map(sanitize_vec)?, + )?; + let swaps = + crate::serializer::value::to_value(self.persister.list_swaps().map(sanitize_vec)?)?; + let lsp_id = crate::serializer::value::to_value(&self.persister.get_lsp_id()?)?; let res = json!({ "node_state": state, diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index 50b3c3c4b..f82e39639 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -1440,7 +1440,7 @@ impl NodeAPI for Greenlight { .await? .into_inner(); - Ok(serde_json::to_value(&resp)?) + Ok(crate::serializer::value::to_value(&resp)?) } NodeCommand::ListPeerChannels => { let resp = self @@ -1449,7 +1449,7 @@ impl NodeAPI for Greenlight { .list_peer_channels(cln::ListpeerchannelsRequest::default()) .await? .into_inner(); - Ok(serde_json::to_value(&resp)?) + Ok(crate::serializer::value::to_value(&resp)?) } NodeCommand::ListFunds => { let resp = self @@ -1458,7 +1458,7 @@ impl NodeAPI for Greenlight { .list_funds(cln::ListfundsRequest::default()) .await? .into_inner(); - Ok(serde_json::to_value(&resp)?) + Ok(crate::serializer::value::to_value(&resp)?) } NodeCommand::ListPayments => { let resp = self @@ -1467,7 +1467,7 @@ impl NodeAPI for Greenlight { .list_pays(cln::ListpaysRequest::default()) .await? .into_inner(); - Ok(serde_json::to_value(&resp)?) + Ok(crate::serializer::value::to_value(&resp)?) } NodeCommand::ListInvoices => { let resp = self @@ -1476,7 +1476,7 @@ impl NodeAPI for Greenlight { .list_invoices(cln::ListinvoicesRequest::default()) .await? .into_inner(); - Ok(serde_json::to_value(&resp)?) + Ok(crate::serializer::value::to_value(&resp)?) } NodeCommand::CloseAllChannels => { let peers_res = self @@ -1498,7 +1498,7 @@ impl NodeAPI for Greenlight { .getinfo(cln::GetinfoRequest::default()) .await? .into_inner(); - Ok(serde_json::to_value(&resp)?) + Ok(crate::serializer::value::to_value(&resp)?) } NodeCommand::Stop => { let resp = self @@ -1507,7 +1507,7 @@ impl NodeAPI for Greenlight { .stop(cln::StopRequest::default()) .await? .into_inner(); - Ok(serde_json::to_value(&resp)?) + Ok(crate::serializer::value::to_value(&resp)?) } } } diff --git a/libs/sdk-core/src/lib.rs b/libs/sdk-core/src/lib.rs index 4859002c0..ef4583bb0 100644 --- a/libs/sdk-core/src/lib.rs +++ b/libs/sdk-core/src/lib.rs @@ -178,6 +178,7 @@ mod lsps0; mod lsps2; mod models; mod persist; +mod serializer; mod support; mod swap_in; mod swap_out; diff --git a/libs/sdk-core/src/serializer.rs b/libs/sdk-core/src/serializer.rs new file mode 100644 index 000000000..59a6b74e5 --- /dev/null +++ b/libs/sdk-core/src/serializer.rs @@ -0,0 +1,896 @@ +use serde::{ser::SerializeStruct, Serialize}; + +pub fn to_string_pretty(value: &T) -> serde_json::Result +where + T: ?Sized + Serialize, +{ + let vec = to_vec_pretty(value)?; + let string = unsafe { + // We do not emit invalid UTF-8. + String::from_utf8_unchecked(vec) + }; + Ok(string) +} + +pub fn to_vec_pretty(value: &T) -> serde_json::Result> +where + T: ?Sized + Serialize, +{ + let mut writer = Vec::with_capacity(128); + to_writer_pretty(&mut writer, value)?; + Ok(writer) +} + +pub fn to_writer_pretty(writer: W, value: &T) -> serde_json::Result<()> +where + W: std::io::Write, + T: ?Sized + Serialize, +{ + let mut ser = serde_json::Serializer::pretty(writer); + let ser = HexSerializer::new(&mut ser); + value.serialize(ser) +} + +pub struct HexSerializer +where + S: serde::ser::Serializer, +{ + inner: S, +} + +impl HexSerializer +where + S: serde::ser::Serializer, +{ + pub fn new(inner: S) -> Self { + Self { inner } + } +} + +impl<'a, S: 'a> serde::ser::Serializer for HexSerializer +where + S: serde::ser::Serializer, +{ + type Ok = S::Ok; + type Error = S::Error; + + type SerializeSeq = S::SerializeSeq; + type SerializeTuple = S::SerializeTuple; + type SerializeTupleStruct = S::SerializeTupleStruct; + type SerializeTupleVariant = S::SerializeTupleVariant; + type SerializeMap = S::SerializeMap; + type SerializeStruct = HexSerializeStruct; + type SerializeStructVariant = S::SerializeStructVariant; + + fn serialize_bool(self, v: bool) -> Result { + self.inner.serialize_bool(v) + } + + fn serialize_i8(self, v: i8) -> Result { + self.inner.serialize_i8(v) + } + + fn serialize_i16(self, v: i16) -> Result { + self.inner.serialize_i16(v) + } + + fn serialize_i32(self, v: i32) -> Result { + self.inner.serialize_i32(v) + } + + fn serialize_i64(self, v: i64) -> Result { + self.inner.serialize_i64(v) + } + + fn serialize_u8(self, v: u8) -> Result { + self.inner.serialize_u8(v) + } + + fn serialize_u16(self, v: u16) -> Result { + self.inner.serialize_u16(v) + } + + fn serialize_u32(self, v: u32) -> Result { + self.inner.serialize_u32(v) + } + + fn serialize_u64(self, v: u64) -> Result { + self.inner.serialize_u64(v) + } + + fn serialize_f32(self, v: f32) -> Result { + self.inner.serialize_f32(v) + } + + fn serialize_f64(self, v: f64) -> Result { + self.inner.serialize_f64(v) + } + + fn serialize_char(self, v: char) -> Result { + self.inner.serialize_char(v) + } + + fn serialize_str(self, v: &str) -> Result { + self.inner.serialize_str(v) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + self.inner.serialize_bytes(v) + } + + fn serialize_none(self) -> Result { + self.inner.serialize_none() + } + + fn serialize_some(self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + self.inner.serialize_some(value) + } + + fn serialize_unit(self) -> Result { + self.inner.serialize_unit() + } + + fn serialize_unit_struct(self, name: &'static str) -> Result { + self.inner.serialize_unit_struct(name) + } + + fn serialize_unit_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + ) -> Result { + self.inner + .serialize_unit_variant(name, variant_index, variant) + } + + fn serialize_newtype_struct( + self, + name: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + self.inner.serialize_newtype_struct(name, value) + } + + fn serialize_newtype_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + self.inner + .serialize_newtype_variant(name, variant_index, variant, value) + } + + fn serialize_seq(self, len: Option) -> Result { + self.inner.serialize_seq(len) + } + + fn serialize_tuple(self, len: usize) -> Result { + self.inner.serialize_tuple(len) + } + + fn serialize_tuple_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + self.inner.serialize_tuple_struct(name, len) + } + + fn serialize_tuple_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + self.inner + .serialize_tuple_variant(name, variant_index, variant, len) + } + + fn serialize_map(self, len: Option) -> Result { + self.inner.serialize_map(len) + } + + fn serialize_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + match self.inner.serialize_struct(name, len) { + Ok(s) => Ok(Self::SerializeStruct::new(s)), + Err(e) => Err(e), + } + } + + fn serialize_struct_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + self.inner + .serialize_struct_variant(name, variant_index, variant, len) + } +} + +pub struct HexSerializeStruct +where + S: SerializeStruct, +{ + inner: S, +} + +impl HexSerializeStruct +where + S: SerializeStruct, +{ + pub fn new(inner: S) -> Self { + Self { inner } + } +} + +impl SerializeStruct for HexSerializeStruct +where + S: SerializeStruct, +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + // Finally, here's the hack. Serialize the value, and try to deserialize + // it as a Vec. If that works, hex encode the Vec, otherwise use + // the default serialization. + // Unfortunately there's no way to inspect the generic type parameter T + // here, otherwise this logic would work by simply checking whether the + // generic type parameter T is a Vec or not. + let as_vec = match serde_json::to_vec(value) { + Ok(as_vec) => as_vec, + Err(_) => return self.inner.serialize_field(key, value), + }; + let val: Vec = match serde_json::from_slice(&as_vec) { + Ok(val) => val, + Err(_) => return self.inner.serialize_field(key, value), + }; + self.inner.serialize_field(key, &hex::encode(&val)) + } + + fn end(self) -> Result { + self.inner.end() + } +} + +pub mod value { + use std::fmt::Display; + + use serde::{ser::Impossible, Serialize}; + use serde_json::{Error, Map, Result, Value}; + + pub fn to_value(value: T) -> Result + where + T: Serialize, + { + value.serialize(Serializer) + } + + pub struct Serializer; + + impl serde::Serializer for Serializer { + type Ok = Value; + type Error = Error; + + type SerializeSeq = SerializeVec; + type SerializeTuple = SerializeVec; + type SerializeTupleStruct = SerializeVec; + type SerializeTupleVariant = SerializeTupleVariant; + type SerializeMap = SerializeMap; + type SerializeStruct = SerializeMap; + type SerializeStructVariant = SerializeStructVariant; + + #[inline] + fn serialize_bool(self, value: bool) -> Result { + Ok(Value::Bool(value)) + } + + #[inline] + fn serialize_i8(self, value: i8) -> Result { + self.serialize_i64(value as i64) + } + + #[inline] + fn serialize_i16(self, value: i16) -> Result { + self.serialize_i64(value as i64) + } + + #[inline] + fn serialize_i32(self, value: i32) -> Result { + self.serialize_i64(value as i64) + } + + fn serialize_i64(self, value: i64) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_i128(self, value: i128) -> Result { + if let Ok(value) = i64::try_from(value) { + Ok(Value::Number(value.into())) + } else { + Err(serde::ser::Error::custom("number out of range")) + } + } + + #[inline] + fn serialize_u8(self, value: u8) -> Result { + self.serialize_u64(value as u64) + } + + #[inline] + fn serialize_u16(self, value: u16) -> Result { + self.serialize_u64(value as u64) + } + + #[inline] + fn serialize_u32(self, value: u32) -> Result { + self.serialize_u64(value as u64) + } + + #[inline] + fn serialize_u64(self, value: u64) -> Result { + Ok(Value::Number(value.into())) + } + + fn serialize_u128(self, value: u128) -> Result { + if let Ok(value) = u64::try_from(value) { + Ok(Value::Number(value.into())) + } else { + Err(serde::ser::Error::custom("number out of range")) + } + } + + #[inline] + fn serialize_f32(self, float: f32) -> Result { + Ok(Value::from(float)) + } + + #[inline] + fn serialize_f64(self, float: f64) -> Result { + Ok(Value::from(float)) + } + + #[inline] + fn serialize_char(self, value: char) -> Result { + let mut s = String::new(); + s.push(value); + Ok(Value::String(s)) + } + + #[inline] + fn serialize_str(self, value: &str) -> Result { + Ok(Value::String(value.to_owned())) + } + + fn serialize_bytes(self, value: &[u8]) -> Result { + Ok(Value::String(hex::encode(value))) + } + + #[inline] + fn serialize_unit(self) -> Result { + Ok(Value::Null) + } + + #[inline] + fn serialize_unit_struct(self, _name: &'static str) -> Result { + self.serialize_unit() + } + + #[inline] + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + self.serialize_str(variant) + } + + #[inline] + fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + let mut values = Map::new(); + values.insert(String::from(variant), to_value(value)?); + Ok(Value::Object(values)) + } + + #[inline] + fn serialize_none(self) -> Result { + self.serialize_unit() + } + + #[inline] + fn serialize_some(self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_seq(self, len: Option) -> Result { + Ok(SerializeVec { + vec: Vec::with_capacity(len.unwrap_or(0)), + }) + } + + fn serialize_tuple(self, len: usize) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + self.serialize_seq(Some(len)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(SerializeTupleVariant { + name: String::from(variant), + vec: Vec::with_capacity(len), + }) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(SerializeMap { + map: Map::new(), + next_key: None, + }) + } + + fn serialize_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + self.serialize_map(Some(len)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + _len: usize, + ) -> Result { + Ok(SerializeStructVariant { + name: String::from(variant), + map: Map::new(), + }) + } + + fn collect_str(self, value: &T) -> Result + where + T: ?Sized + Display, + { + Ok(Value::String(value.to_string())) + } + } + + pub struct SerializeVec { + vec: Vec, + } + + pub struct SerializeTupleVariant { + name: String, + vec: Vec, + } + + pub struct SerializeMap { + map: Map, + next_key: Option, + } + + pub struct SerializeStructVariant { + name: String, + map: Map, + } + + impl serde::ser::SerializeSeq for SerializeVec { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.vec.push(to_value(value)?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Array(self.vec)) + } + } + + impl serde::ser::SerializeTuple for SerializeVec { + type Ok = Value; + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } + } + + impl serde::ser::SerializeTupleStruct for SerializeVec { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + serde::ser::SerializeSeq::end(self) + } + } + + impl serde::ser::SerializeTupleVariant for SerializeTupleVariant { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.vec.push(to_value(value)?); + Ok(()) + } + + fn end(self) -> Result { + let mut object = Map::new(); + + object.insert(self.name, Value::Array(self.vec)); + + Ok(Value::Object(object)) + } + } + + impl serde::ser::SerializeMap for SerializeMap { + type Ok = Value; + type Error = Error; + + fn serialize_key(&mut self, key: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.next_key = Some(key.serialize(MapKeySerializer)?); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + let key = self.next_key.take(); + // Panic because this indicates a bug in the program rather than an + // expected failure. + let key = key.expect("serialize_value called before serialize_key"); + + // Finally, here's the hack. Serialize the value, and try to deserialize + // it as a Vec. If that works, hex encode the Vec, otherwise use + // the default serialization. + // Unfortunately there's no way to inspect the generic type parameter T + // here, otherwise this logic would work by simply checking whether the + // generic type parameter T is a Vec or not. + let as_vec = match serde_json::to_vec(value) { + Ok(as_vec) => as_vec, + Err(_) => { + self.map.insert(key, to_value(value)?); + return Ok(()); + } + }; + let val: Vec = match serde_json::from_slice(&as_vec) { + Ok(val) => val, + Err(_) => { + self.map.insert(key, to_value(value)?); + return Ok(()); + } + }; + + self.map.insert(key, to_value(hex::encode(&val))?); + Ok(()) + } + + fn end(self) -> Result { + Ok(Value::Object(self.map)) + } + } + + impl serde::ser::SerializeStruct for SerializeMap { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + serde::ser::SerializeMap::serialize_entry(self, key, value) + } + + fn end(self) -> Result { + serde::ser::SerializeMap::end(self) + } + } + + impl serde::ser::SerializeStructVariant for SerializeStructVariant { + type Ok = Value; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + self.map.insert(String::from(key), to_value(value)?); + Ok(()) + } + + fn end(self) -> Result { + let mut object = Map::new(); + + object.insert(self.name, Value::Object(self.map)); + + Ok(Value::Object(object)) + } + } + + struct MapKeySerializer; + + fn key_must_be_a_string() -> Error { + serde::ser::Error::custom("key must be a string") + } + + fn float_key_must_be_finite() -> Error { + serde::ser::Error::custom("float key must be finite") + } + + impl serde::Serializer for MapKeySerializer { + type Ok = String; + type Error = Error; + + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + #[inline] + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + Ok(variant.to_owned()) + } + + #[inline] + fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_bool(self, value: bool) -> Result { + Ok(value.to_string()) + } + + fn serialize_i8(self, value: i8) -> Result { + Ok(value.to_string()) + } + + fn serialize_i16(self, value: i16) -> Result { + Ok(value.to_string()) + } + + fn serialize_i32(self, value: i32) -> Result { + Ok(value.to_string()) + } + + fn serialize_i64(self, value: i64) -> Result { + Ok(value.to_string()) + } + + fn serialize_i128(self, value: i128) -> Result { + Ok(value.to_string()) + } + + fn serialize_u8(self, value: u8) -> Result { + Ok(value.to_string()) + } + + fn serialize_u16(self, value: u16) -> Result { + Ok(value.to_string()) + } + + fn serialize_u32(self, value: u32) -> Result { + Ok(value.to_string()) + } + + fn serialize_u64(self, value: u64) -> Result { + Ok(value.to_string()) + } + + fn serialize_u128(self, value: u128) -> Result { + Ok(value.to_string()) + } + + fn serialize_f32(self, value: f32) -> Result { + if value.is_finite() { + Ok(ryu::Buffer::new().format_finite(value).to_owned()) + } else { + Err(float_key_must_be_finite()) + } + } + + fn serialize_f64(self, value: f64) -> Result { + if value.is_finite() { + Ok(ryu::Buffer::new().format_finite(value).to_owned()) + } else { + Err(float_key_must_be_finite()) + } + } + + #[inline] + fn serialize_char(self, value: char) -> Result { + Ok({ + let mut s = String::new(); + s.push(value); + s + }) + } + + #[inline] + fn serialize_str(self, value: &str) -> Result { + Ok(value.to_owned()) + } + + fn serialize_bytes(self, _value: &[u8]) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_unit(self) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + Err(key_must_be_a_string()) + } + + fn serialize_none(self) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_some(self, _value: &T) -> Result + where + T: ?Sized + Serialize, + { + Err(key_must_be_a_string()) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn collect_str(self, value: &T) -> Result + where + T: ?Sized + Display, + { + Ok(value.to_string()) + } + } +} diff --git a/tools/sdk-cli/Cargo.lock b/tools/sdk-cli/Cargo.lock index 35f5b7eb4..3a00eb9f0 100644 --- a/tools/sdk-cli/Cargo.lock +++ b/tools/sdk-cli/Cargo.lock @@ -556,6 +556,7 @@ dependencies = [ "ripemd", "rusqlite", "rusqlite_migration", + "ryu", "sdk-common", "serde", "serde_json", @@ -2944,9 +2945,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" From a1b615076e3902ecf35529b2a3db84c6bd9430e1 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Fri, 11 Oct 2024 13:25:19 +0200 Subject: [PATCH 10/14] trampoline: set max fee percent --- libs/sdk-core/src/greenlight/node_api.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index f82e39639..951015127 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -53,6 +53,8 @@ use crate::{NodeConfig, PrepareRedeemOnchainFundsRequest, PrepareRedeemOnchainFu const MAX_PAYMENT_AMOUNT_MSAT: u64 = 4294967000; const MAX_INBOUND_LIQUIDITY_MSAT: u64 = 4000000000; +const TRAMPOLINE_BASE_FEE_MSAT: u64 = 4000; +const TRAMPOLINE_FEE_PPM: u64 = 5000; pub(crate) struct Greenlight { sdk_config: Config, @@ -1062,6 +1064,10 @@ impl NodeAPI for Greenlight { unix_nano: SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(), amount_msat, })?; + let fee_msat = + (amount_msat.saturating_mul(TRAMPOLINE_FEE_PPM) / 1_000_000) + TRAMPOLINE_BASE_FEE_MSAT; + let fee_percent = ((fee_msat as f64 / amount_msat as f64) * 100.) as f32; + debug!("using fee msat {} fee percent {}", fee_msat, fee_percent); let mut client = self.get_client().await?; let request = TrampolinePayRequest { bolt11, @@ -1070,7 +1076,7 @@ impl NodeAPI for Greenlight { label, maxdelay: u32::default(), description: String::default(), - maxfeepercent: f32::default(), + maxfeepercent: fee_percent, }; let result = self .with_keep_alive(client.trampoline_pay(request)) From 391260404c4801b74fa53f666ac5b15779c62cb2 Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Sun, 13 Oct 2024 19:58:48 +0000 Subject: [PATCH 11/14] Diagnostic data: include SDK version and git hash (#1101) --- libs/sdk-core/src/breez_services.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 27362d8a8..562041479 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -185,8 +185,7 @@ impl BreezServices { req: ConnectRequest, event_listener: Box, ) -> BreezServicesResult> { - let sdk_version = option_env!("CARGO_PKG_VERSION").unwrap_or_default(); - let sdk_git_hash = option_env!("SDK_GIT_HASH").unwrap_or_default(); + let (sdk_version, sdk_git_hash) = Self::get_sdk_version(); info!("SDK v{sdk_version} ({sdk_git_hash})"); let start = Instant::now(); let services = BreezServicesBuilder::new(req.config) @@ -199,6 +198,12 @@ impl BreezServices { Ok(services) } + fn get_sdk_version() -> (&'static str, &'static str) { + let sdk_version = option_env!("CARGO_PKG_VERSION").unwrap_or_default(); + let sdk_git_hash = option_env!("SDK_GIT_HASH").unwrap_or_default(); + (sdk_version, sdk_git_hash) + } + /// Internal utility method that starts the BreezServices background tasks for this instance. /// /// It should be called once right after creating [BreezServices], since it is essential for the @@ -2174,6 +2179,8 @@ impl BreezServices { } async fn generate_sdk_diagnostic_data(&self) -> SdkResult { + let (sdk_version, sdk_git_hash) = Self::get_sdk_version(); + let version = format!("SDK v{sdk_version} ({sdk_git_hash})"); let state = crate::serializer::value::to_value(&self.persister.get_node_state()?)?; let payments = crate::serializer::value::to_value( &self @@ -2190,6 +2197,7 @@ impl BreezServices { let lsp_id = crate::serializer::value::to_value(&self.persister.get_lsp_id()?)?; let res = json!({ + "version": version, "node_state": state, "payments": payments, "channels": channels, From f8d9f7d598c0b93546f64e794cfe1a282ac32620 Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Mon, 14 Oct 2024 08:56:28 +0200 Subject: [PATCH 12/14] Bump gl-client to latest commit (#1106) --- 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 e53a3edb5..1e3c9e114 100644 --- a/libs/Cargo.lock +++ b/libs/Cargo.lock @@ -1472,7 +1472,7 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gl-client" version = "0.3.0" -source = "git+https://github.com/Blockstream/greenlight.git?rev=a0e4faf389484e426fcc197f8726c72a27eef32e#a0e4faf389484e426fcc197f8726c72a27eef32e" +source = "git+https://github.com/Blockstream/greenlight.git?rev=c09c1be59994b35aadfe4747b78bcdc8fffbe45a#c09c1be59994b35aadfe4747b78bcdc8fffbe45a" dependencies = [ "anyhow", "async-stream", diff --git a/libs/sdk-core/Cargo.toml b/libs/sdk-core/Cargo.toml index e446f2d90..d350adb4b 100644 --- a/libs/sdk-core/Cargo.toml +++ b/libs/sdk-core/Cargo.toml @@ -14,7 +14,7 @@ anyhow = { workspace = true } hex = { workspace = true } gl-client = { git = "https://github.com/Blockstream/greenlight.git", features = [ "permissive", -], rev = "a0e4faf389484e426fcc197f8726c72a27eef32e" } +], rev = "c09c1be59994b35aadfe4747b78bcdc8fffbe45a" } 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 3a00eb9f0..36d4140ca 100644 --- a/tools/sdk-cli/Cargo.lock +++ b/tools/sdk-cli/Cargo.lock @@ -1333,7 +1333,7 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gl-client" version = "0.3.0" -source = "git+https://github.com/Blockstream/greenlight.git?rev=a0e4faf389484e426fcc197f8726c72a27eef32e#a0e4faf389484e426fcc197f8726c72a27eef32e" +source = "git+https://github.com/Blockstream/greenlight.git?rev=c09c1be59994b35aadfe4747b78bcdc8fffbe45a#c09c1be59994b35aadfe4747b78bcdc8fffbe45a" dependencies = [ "anyhow", "async-stream", From 7d712c4639f240ca5a360bc386b78b02cd0c3d36 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Mon, 14 Oct 2024 10:02:00 +0200 Subject: [PATCH 13/14] use invoice destination for trampoline --- libs/sdk-core/src/greenlight/node_api.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index 951015127..d68dec40b 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -2003,12 +2003,18 @@ impl TryFrom for Payment { fee_msat: payment_amount_sent.saturating_sub(payment_amount), status, error: None, - description: ln_invoice.map(|i| i.description).unwrap_or_default(), + description: ln_invoice + .as_ref() + .map(|i| i.description.clone()) + .unwrap_or_default(), details: PaymentDetails::Ln { data: LnPaymentDetails { payment_hash: hex::encode(payment.payment_hash), label: client_label.unwrap_or_default(), - destination_pubkey: payment.destination.map(hex::encode).unwrap_or_default(), + destination_pubkey: ln_invoice.as_ref().map_or( + payment.destination.map(hex::encode).unwrap_or_default(), + |i| i.payee_pubkey.clone(), + ), payment_preimage: payment.preimage.map(hex::encode).unwrap_or_default(), keysend: payment.bolt11.is_none(), bolt11: payment.bolt11.unwrap_or_default(), From df86eec7b0745edb1736e6fcbb39c12fff15e920 Mon Sep 17 00:00:00 2001 From: Roei Erez Date: Mon, 14 Oct 2024 21:54:16 +0300 Subject: [PATCH 14/14] update version to 0.6.2 --- libs/Cargo.lock | 6 +++--- libs/Cargo.toml | 2 +- libs/sdk-flutter/android/build.gradle.production | 2 +- libs/sdk-flutter/ios/breez_sdk.podspec | 2 +- libs/sdk-flutter/ios/breez_sdk.podspec.production | 2 +- libs/sdk-flutter/pubspec.yaml | 2 +- libs/sdk-react-native/example/package.json | 4 ++-- libs/sdk-react-native/package.json | 2 +- tools/sdk-cli/Cargo.lock | 6 +++--- tools/sdk-cli/Cargo.toml | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/libs/Cargo.lock b/libs/Cargo.lock index 1e3c9e114..aac2f2e60 100644 --- a/libs/Cargo.lock +++ b/libs/Cargo.lock @@ -621,7 +621,7 @@ dependencies = [ [[package]] name = "breez-sdk-core" -version = "0.5.2" +version = "0.6.2" dependencies = [ "aes 0.8.3", "anyhow", @@ -665,7 +665,7 @@ dependencies = [ [[package]] name = "breez_sdk" -version = "0.5.2" +version = "0.6.2" dependencies = [ "anyhow", "breez-sdk-core", @@ -3129,7 +3129,7 @@ dependencies = [ [[package]] name = "sdk-common" -version = "0.5.2" +version = "0.6.2" dependencies = [ "aes 0.8.3", "anyhow", diff --git a/libs/Cargo.toml b/libs/Cargo.toml index e4a485524..c27f40f3e 100644 --- a/libs/Cargo.toml +++ b/libs/Cargo.toml @@ -14,7 +14,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.5.2" +version = "0.6.2" [workspace.dependencies] aes = "0.8" diff --git a/libs/sdk-flutter/android/build.gradle.production b/libs/sdk-flutter/android/build.gradle.production index 0fa5e7fb4..9ed422ec1 100644 --- a/libs/sdk-flutter/android/build.gradle.production +++ b/libs/sdk-flutter/android/build.gradle.production @@ -1,5 +1,5 @@ group 'com.breez.breez_sdk' -version '0.5.2' +version '0.6.2' buildscript { ext.kotlin_version = '1.8.20' diff --git a/libs/sdk-flutter/ios/breez_sdk.podspec b/libs/sdk-flutter/ios/breez_sdk.podspec index cf0322b1c..e962fa695 100644 --- a/libs/sdk-flutter/ios/breez_sdk.podspec +++ b/libs/sdk-flutter/ios/breez_sdk.podspec @@ -2,7 +2,7 @@ # Run `pod lib lint breez_sdk.podspec` to validate before publishing. Pod::Spec.new do |s| s.name = 'breez_sdk' - s.version = '0.5.2' + s.version = '0.6.2' s.summary = 'BreezSDK flutter plugin.' s.description = <<-DESC BreezSDK flutter plugin. diff --git a/libs/sdk-flutter/ios/breez_sdk.podspec.production b/libs/sdk-flutter/ios/breez_sdk.podspec.production index b5ad14256..5f965f223 100644 --- a/libs/sdk-flutter/ios/breez_sdk.podspec.production +++ b/libs/sdk-flutter/ios/breez_sdk.podspec.production @@ -1,4 +1,4 @@ -tag_version = '0.5.2' +tag_version = '0.6.2' framework = 'breez_sdkFFI.xcframework' lib_name = "breez-sdkFFI.#{tag_version}" url = "https://github.com/breez/breez-sdk-swift/releases/download/#{tag_version}/#{framework}.zip" diff --git a/libs/sdk-flutter/pubspec.yaml b/libs/sdk-flutter/pubspec.yaml index 6df574276..b3e21f989 100644 --- a/libs/sdk-flutter/pubspec.yaml +++ b/libs/sdk-flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: breez_sdk description: Flutter bindings for the Breez SDK repository: https://github.com/breez/breez-sdk-flutter -version: 0.5.2 +version: 0.6.2 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/libs/sdk-react-native/example/package.json b/libs/sdk-react-native/example/package.json index f671a9bab..6d4b281b7 100644 --- a/libs/sdk-react-native/example/package.json +++ b/libs/sdk-react-native/example/package.json @@ -13,7 +13,7 @@ "rebuild": "rm -rf node_modules && yarn && yarn pods" }, "dependencies": { - "@breeztech/react-native-breez-sdk": "0.5.2", + "@breeztech/react-native-breez-sdk": "0.6.2", "@dreson4/react-native-quick-bip39": "^0.0.5", "@react-navigation/native": "6.1.1", "@react-navigation/native-stack": "6.9.7", @@ -41,4 +41,4 @@ "jest": { "preset": "react-native" } -} +} \ No newline at end of file diff --git a/libs/sdk-react-native/package.json b/libs/sdk-react-native/package.json index 1b026a3da..caa604a3b 100644 --- a/libs/sdk-react-native/package.json +++ b/libs/sdk-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@breeztech/react-native-breez-sdk", - "version": "0.5.2", + "version": "0.6.2", "description": "React Native Breez SDK", "main": "lib/commonjs/index", "module": "lib/module/index", diff --git a/tools/sdk-cli/Cargo.lock b/tools/sdk-cli/Cargo.lock index 36d4140ca..6a7a5abb4 100644 --- a/tools/sdk-cli/Cargo.lock +++ b/tools/sdk-cli/Cargo.lock @@ -514,7 +514,7 @@ dependencies = [ [[package]] name = "breez-sdk-cli" -version = "0.5.2" +version = "0.6.2" dependencies = [ "anyhow", "breez-sdk-core", @@ -531,7 +531,7 @@ dependencies = [ [[package]] name = "breez-sdk-core" -version = "0.5.2" +version = "0.6.2" dependencies = [ "aes 0.8.2", "anyhow", @@ -2976,7 +2976,7 @@ dependencies = [ [[package]] name = "sdk-common" -version = "0.5.2" +version = "0.6.2" dependencies = [ "aes 0.8.2", "anyhow", diff --git a/tools/sdk-cli/Cargo.toml b/tools/sdk-cli/Cargo.toml index d661e29b1..ace8aaa5c 100644 --- a/tools/sdk-cli/Cargo.toml +++ b/tools/sdk-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breez-sdk-cli" -version = "0.5.2" +version = "0.6.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html