diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl index 21d8d758e..07310ef7f 100644 --- a/libs/sdk-bindings/src/breez_sdk.udl +++ b/libs/sdk-bindings/src/breez_sdk.udl @@ -1,6 +1,6 @@ dictionary RouteHintHop { string src_node_id; - u64 short_channel_id; + string short_channel_id; u32 fees_base_msat; u32 fees_proportional_millionths; u64 cltv_expiry_delta; diff --git a/libs/sdk-common/src/invoice.rs b/libs/sdk-common/src/invoice.rs index 43fac1040..fe6b4c390 100644 --- a/libs/sdk-common/src/invoice.rs +++ b/libs/sdk-common/src/invoice.rs @@ -1,3 +1,4 @@ +use std::num::ParseIntError; use std::str::FromStr; use std::time::{SystemTimeError, UNIX_EPOCH}; @@ -57,6 +58,12 @@ impl From for InvoiceError { } } +impl From for InvoiceError { + fn from(err: ParseIntError) -> Self { + Self::Generic(err.to_string()) + } +} + impl From for InvoiceError { fn from(err: regex::Error) -> Self { Self::Generic(err.to_string()) @@ -75,6 +82,25 @@ impl From for InvoiceError { } } +fn parse_short_channel_id(id_str: &str) -> InvoiceResult { + let parts: Vec<&str> = id_str.split('x').collect(); + if parts.len() != 3 { + return Err(InvoiceError::generic("Invalid short channel id")); + } + let block_num = parts[0].parse::()?; + let tx_num = parts[1].parse::()?; + let tx_out = parts[2].parse::()?; + + Ok((block_num & 0xFFFFFF) << 40 | (tx_num & 0xFFFFFF) << 16 | (tx_out & 0xFFFF)) +} + +fn format_short_channel_id(id: u64) -> String { + let block_num = (id >> 40) as u32; + let tx_num = ((id >> 16) & 0xFFFFFF) as u32; + let tx_out = (id & 0xFFFF) as u16; + format!("{block_num}x{tx_num}x{tx_out}") +} + /// Wrapper for a BOLT11 LN invoice #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct LNInvoice { @@ -106,7 +132,7 @@ pub struct RouteHintHop { /// The node_id of the non-target end of the route pub src_node_id: String, /// The short_channel_id of this channel - pub short_channel_id: u64, + pub short_channel_id: String, /// The fees which must be paid to use this channel pub fees_base_msat: u32, pub fees_proportional_millionths: u32, @@ -133,7 +159,7 @@ impl RouteHint { let router_hop = router::RouteHintHop { src_node_id: pubkey_res, - short_channel_id: hop.short_channel_id, + short_channel_id: parse_short_channel_id(&hop.short_channel_id)?, fees: RoutingFees { base_msat: hop.fees_base_msat, proportional_millionths: hop.fees_proportional_millionths, @@ -154,7 +180,7 @@ impl RouteHint { let router_hop = RouteHintHop { src_node_id: pubkey_res, - short_channel_id: hop.short_channel_id, + short_channel_id: format_short_channel_id(hop.short_channel_id), fees_base_msat: hop.fees.base_msat, fees_proportional_millionths: hop.fees.proportional_millionths, cltv_expiry_delta: u64::from(hop.cltv_expiry_delta), @@ -315,7 +341,7 @@ mod tests { private_key.copy_from_slice(&private_key_vec[0..32]); let hint_hop = self::RouteHintHop { src_node_id: res.payee_pubkey, - short_channel_id: 1234, + short_channel_id: format_short_channel_id(1234), cltv_expiry_delta: 2000, htlc_minimum_msat: Some(3000), htlc_maximum_msat: Some(4000), @@ -342,7 +368,7 @@ mod tests { private_key.copy_from_slice(&private_key_vec[0..32]); let hint_hop = self::RouteHintHop { src_node_id: res.payee_pubkey, - short_channel_id: 1234, + short_channel_id: format_short_channel_id(1234), fees_base_msat: 1000, fees_proportional_millionths: 100, cltv_expiry_delta: 2000, @@ -373,4 +399,44 @@ mod tests { assert!(res.is_ok()); assert!(validate_network(res.unwrap(), Network::Bitcoin).is_err()); } + + #[test] + fn test_format_short_channel_id() { + let valid_short_channel_ids = vec![ + (0, "0x0x0"), + (936_502_917_475_117, "851x12489658x11053"), + (455_944_619_913_684, "414x11395355x29140"), + (u64::MAX, "16777215x16777215x65535"), + ]; + for (scid, scid_str) in valid_short_channel_ids { + let res = format_short_channel_id(scid); + assert_eq!(res, scid_str); + } + } + + #[test] + fn test_parse_short_channel_id() { + let valid_short_channel_ids = vec![ + ("0x0x0", 0), + ("16000000x0x3965", 17_592_186_044_416_003_965), + ("94838x10x3", 104_275_483_755_675_651), + ("16777215x16777215x65535", u64::MAX), + ]; + for (scid_str, scid) in valid_short_channel_ids { + let res = parse_short_channel_id(scid_str); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), scid); + } + + let invalid_short_channel_ids = vec![ + "0", + "16000000x0x-3965", + "18446744073709551615", + "16777215x65535", + ]; + for scid_str in invalid_short_channel_ids { + let res = parse_short_channel_id(scid_str); + assert!(res.is_err()); + } + } } diff --git a/libs/sdk-core/src/binding.rs b/libs/sdk-core/src/binding.rs index 4d401d1df..6d0f4d58a 100644 --- a/libs/sdk-core/src/binding.rs +++ b/libs/sdk-core/src/binding.rs @@ -111,7 +111,7 @@ pub struct _RouteHint { #[frb(mirror(RouteHintHop))] pub struct _RouteHintHop { pub src_node_id: String, - pub short_channel_id: u64, + pub short_channel_id: String, pub fees_base_msat: u32, pub fees_proportional_millionths: u32, pub cltv_expiry_delta: u64, diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 1818a7b43..06f6d1d74 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -36,9 +36,9 @@ use crate::greenlight::{GLBackupTransport, Greenlight}; use crate::lnurl::pay::*; use crate::lsp::LspInformation; use crate::models::{ - parse_short_channel_id, sanitize::*, ChannelState, ClosedChannelPaymentDetails, Config, - EnvironmentType, LspAPI, NodeState, Payment, PaymentDetails, PaymentType, ReverseSwapPairInfo, - ReverseSwapServiceAPI, SwapInfo, SwapperAPI, INVOICE_PAYMENT_FEE_EXPIRY_SECONDS, + sanitize::*, ChannelState, ClosedChannelPaymentDetails, Config, EnvironmentType, LspAPI, + NodeState, Payment, PaymentDetails, PaymentType, ReverseSwapPairInfo, ReverseSwapServiceAPI, + SwapInfo, SwapperAPI, INVOICE_PAYMENT_FEE_EXPIRY_SECONDS, }; use crate::node_api::{CreateInvoiceRequest, NodeAPI}; use crate::persist::db::SqliteStorage; @@ -2714,7 +2714,7 @@ impl PaymentReceiver { let open_channel_hint = RouteHint { hops: vec![RouteHintHop { src_node_id: lsp_info.pubkey.clone(), - short_channel_id: parse_short_channel_id("1x0x0")?, + short_channel_id: "1x0x0".to_string(), fees_base_msat: lsp_info.base_fee_msat as u32, fees_proportional_millionths: (lsp_info.fee_rate * 1000000.0) as u32, cltv_expiry_delta: lsp_info.time_lock_delta as u64, @@ -3252,10 +3252,7 @@ pub(crate) mod tests { assert_eq!(ln_invoice.routing_hints[0].hops.len(), 1); let lsp_hop = &ln_invoice.routing_hints[0].hops[0]; assert_eq!(lsp_hop.src_node_id, breez_server.clone().lsp_pub_key()); - assert_eq!( - lsp_hop.short_channel_id, - parse_short_channel_id("1x0x0").unwrap() - ); + assert_eq!(lsp_hop.short_channel_id, "1x0x0"); Ok(()) } diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs index fe54e5a91..5d6c32529 100644 --- a/libs/sdk-core/src/bridge_generated.rs +++ b/libs/sdk-core/src/bridge_generated.rs @@ -1149,7 +1149,7 @@ const _: fn() = || { { let RouteHintHop = None::.unwrap(); let _: String = RouteHintHop.src_node_id; - let _: u64 = RouteHintHop.short_channel_id; + let _: String = RouteHintHop.short_channel_id; let _: u32 = RouteHintHop.fees_base_msat; let _: u32 = RouteHintHop.fees_proportional_millionths; let _: u64 = RouteHintHop.cltv_expiry_delta; diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index df3b10945..38e98e1d7 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -742,7 +742,7 @@ impl Greenlight { base_fee_msat: hint.fees_base_msat as u64, fee_per_millionth: hint.fees_proportional_millionths as u64, node_id: payee_node_id.clone().unwrap_or_default(), - short_channel_id: format_short_channel_id(hint.short_channel_id), + short_channel_id: hint.short_channel_id.clone(), channel_delay: hint.cltv_expiry_delta, }]) } @@ -1790,11 +1790,10 @@ impl NodeAPI for Greenlight { "For peer {}: remote base {} proportional {} cltv_delta {}", peer_id_str, fees_base_msat, fees_proportional_millionths, cltv_delta, ); - let scid = parse_short_channel_id(&channel_id)?; let hint = RouteHint { hops: vec![RouteHintHop { src_node_id: peer_id_str, - short_channel_id: scid, + short_channel_id: channel_id, fees_base_msat, fees_proportional_millionths, cltv_expiry_delta: cltv_delta as u64, diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index 533d77a11..86b866c41 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -1472,25 +1472,6 @@ impl SwapInfo { } } -pub(crate) fn parse_short_channel_id(id_str: &str) -> Result { - let parts: Vec<&str> = id_str.split('x').collect(); - if parts.len() != 3 { - return Ok(0); - } - let block_num = parts[0].parse::()?; - let tx_num = parts[1].parse::()?; - let tx_out = parts[2].parse::()?; - - Ok((block_num & 0xFFFFFF) << 40 | (tx_num & 0xFFFFFF) << 16 | (tx_out & 0xFFFF)) -} - -pub(crate) fn format_short_channel_id(id: u64) -> String { - let block_num = (id >> 40) as u32; - let tx_num = ((id >> 16) & 0xFFFFFF) as u32; - let tx_out = (id & 0xFFFF) as u16; - format!("{block_num}x{tx_num}x{tx_out}") -} - /// UTXO known to the LN node #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] pub struct UnspentTransactionOutput { diff --git a/libs/sdk-core/src/swap_out/boltzswap.rs b/libs/sdk-core/src/swap_out/boltzswap.rs index 6821d1bfb..99c14d268 100644 --- a/libs/sdk-core/src/swap_out/boltzswap.rs +++ b/libs/sdk-core/src/swap_out/boltzswap.rs @@ -61,7 +61,7 @@ impl From for RouteHintHop { fn from(value: BoltzRouteHintHop) -> Self { RouteHintHop { src_node_id: value.node_id, - short_channel_id: 0_u64, + short_channel_id: "0x0x0".to_string(), fees_base_msat: value.fee_base_msat, fees_proportional_millionths: value.fee_proportional_millionths, cltv_expiry_delta: value.cltv_expiry_delta, diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart index 732ef3261..7ac7d374a 100644 --- a/libs/sdk-flutter/lib/bridge_generated.dart +++ b/libs/sdk-flutter/lib/bridge_generated.dart @@ -1661,7 +1661,7 @@ class RouteHint { class RouteHintHop { final String srcNodeId; - final int shortChannelId; + final String shortChannelId; final int feesBaseMsat; final int feesProportionalMillionths; final int cltvExpiryDelta; @@ -4063,7 +4063,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { if (arr.length != 7) throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); return RouteHintHop( srcNodeId: _wire2api_String(arr[0]), - shortChannelId: _wire2api_u64(arr[1]), + shortChannelId: _wire2api_String(arr[1]), feesBaseMsat: _wire2api_u32(arr[2]), feesProportionalMillionths: _wire2api_u32(arr[3]), cltvExpiryDelta: _wire2api_u64(arr[4]), 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 181899eb2..27f2a4945 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 @@ -3083,7 +3083,7 @@ fun asRouteHintHop(routeHintHop: ReadableMap): RouteHintHop? { return null } val srcNodeId = routeHintHop.getString("srcNodeId")!! - val shortChannelId = routeHintHop.getDouble("shortChannelId").toULong() + val shortChannelId = routeHintHop.getString("shortChannelId")!! val feesBaseMsat = routeHintHop.getInt("feesBaseMsat").toUInt() val feesProportionalMillionths = routeHintHop.getInt("feesProportionalMillionths").toUInt() val cltvExpiryDelta = routeHintHop.getDouble("cltvExpiryDelta").toULong() diff --git a/libs/sdk-react-native/ios/BreezSDKMapper.swift b/libs/sdk-react-native/ios/BreezSDKMapper.swift index e864f4e40..012eecd30 100644 --- a/libs/sdk-react-native/ios/BreezSDKMapper.swift +++ b/libs/sdk-react-native/ios/BreezSDKMapper.swift @@ -3455,7 +3455,7 @@ enum BreezSDKMapper { guard let srcNodeId = routeHintHop["srcNodeId"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "srcNodeId", typeName: "RouteHintHop")) } - guard let shortChannelId = routeHintHop["shortChannelId"] as? UInt64 else { + guard let shortChannelId = routeHintHop["shortChannelId"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "shortChannelId", typeName: "RouteHintHop")) } guard let feesBaseMsat = routeHintHop["feesBaseMsat"] as? UInt32 else { diff --git a/libs/sdk-react-native/src/index.ts b/libs/sdk-react-native/src/index.ts index d434d039d..5225c062e 100644 --- a/libs/sdk-react-native/src/index.ts +++ b/libs/sdk-react-native/src/index.ts @@ -478,7 +478,7 @@ export interface RouteHint { export interface RouteHintHop { srcNodeId: string - shortChannelId: number + shortChannelId: string feesBaseMsat: number feesProportionalMillionths: number cltvExpiryDelta: number