Skip to content

Commit 7da6f8c

Browse files
committed
Expose node ids as UniFFI objects
Reject malformed binding inputs in a fallible constructor before they reach graph lookups. Also expose safe conversion to and from public keys while keeping Rust graph APIs on the upstream type. Co-Authored-By: HAL 9000
1 parent 4e1b743 commit 7da6f8c

5 files changed

Lines changed: 114 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
- The language bindings now expose `NodeAlias` as an object instead of a string alias. Node aliases
1515
must be passed through its fallible constructor, which returns `NodeError::InvalidNodeAlias` for
1616
invalid input.
17+
- The language bindings now expose `NodeId` as an object instead of a string alias. Node ids must
18+
be passed through its fallible constructor, which returns `NodeError::InvalidNodeId` for invalid
19+
input.
1720
- `generate_entropy_mnemonic` has been removed. Use `bip39::Mnemonic::generate` in Rust and
1821
`Mnemonic::generate` in the language bindings instead.
1922
- Migrating between storage backends does not preserve the relative creation order of

bindings/ldk_node.udl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,7 @@ typedef interface SocketAddress;
388388

389389
typedef interface PublicKey;
390390

391-
[Custom]
392-
typedef string NodeId;
391+
typedef interface NodeId;
393392

394393
[Custom]
395394
typedef string Address;

bindings/python/src/ldk_node/test_ldk_node.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,38 @@ def test_node_alias_object(self):
309309
self.assertIsInstance(error.exception, NodeError.InvalidNodeAlias)
310310

311311

312+
class TestNodeId(unittest.TestCase):
313+
def test_node_id_object(self):
314+
self.assertTrue(
315+
hasattr(bindings.NodeId, "from_str"),
316+
"NodeId should be exposed as an object",
317+
)
318+
319+
node_id_str = (
320+
"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"
321+
)
322+
node_id = bindings.NodeId.from_str(node_id_str)
323+
324+
self.assertIsInstance(node_id, bindings.NodeId)
325+
self.assertEqual(str(node_id), node_id_str)
326+
self.assertEqual(node_id.as_bytes(), bytes.fromhex(node_id_str))
327+
328+
public_key = bindings.PublicKey.from_str(node_id_str)
329+
self.assertEqual(bindings.NodeId.from_public_key(public_key), node_id)
330+
self.assertEqual(node_id.as_public_key(), public_key)
331+
332+
with self.assertRaises(NodeError) as error:
333+
bindings.NodeId.from_str("invalid")
334+
335+
self.assertIsInstance(error.exception, NodeError.InvalidNodeId)
336+
337+
invalid_public_key_node_id = bindings.NodeId.from_str("00" * 33)
338+
with self.assertRaises(NodeError) as error:
339+
invalid_public_key_node_id.as_public_key()
340+
341+
self.assertIsInstance(error.exception, NodeError.InvalidNodeId)
342+
343+
312344
class TestLdkNode(unittest.TestCase):
313345
def setUp(self):
314346
bitcoin_cli("createwallet ldk_node_test")

src/ffi/types.rs

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ use lightning::offers::payer_proof::{
3838
use lightning::offers::refund::Refund as LdkRefund;
3939
use lightning::offers::static_invoice::StaticInvoice as LdkStaticInvoice;
4040
use lightning::onion_message::dns_resolution::HumanReadableName as LdkHumanReadableName;
41-
use lightning::routing::gossip::NodeAlias as LdkNodeAlias;
42-
pub use lightning::routing::gossip::{NodeId, RoutingFees};
41+
pub use lightning::routing::gossip::RoutingFees;
42+
use lightning::routing::gossip::{NodeAlias as LdkNodeAlias, NodeId as LdkNodeId};
4343
pub use lightning::routing::router::RouteParametersConfig;
4444
use lightning::util::persist::PageToken as LdkPageToken;
4545
use lightning::util::ser::{Readable, RequiredWrapper, Writeable, Writer};
@@ -242,19 +242,71 @@ impl ReadablePublicKey for RequiredWrapper<Arc<PublicKey>> {
242242
}
243243
}
244244

245-
uniffi::custom_type!(NodeId, String, {
246-
remote,
247-
try_lift: |val| {
248-
if let Ok(key) = NodeId::from_str(&val) {
249-
return Ok(key);
250-
}
245+
/// A compressed public key identifying a node in the network graph.
246+
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)]
247+
#[uniffi::export(Debug, Display, Eq)]
248+
pub struct NodeId {
249+
pub(crate) inner: LdkNodeId,
250+
}
251251

252-
Err(Error::InvalidNodeId.into())
253-
},
254-
lower: |obj| {
255-
obj.to_string()
256-
},
257-
});
252+
#[uniffi::export]
253+
impl NodeId {
254+
/// Constructs a node id from its serialized representation.
255+
#[uniffi::constructor]
256+
pub fn from_str(node_id_str: &str) -> Result<Self, Error> {
257+
node_id_str.parse()
258+
}
259+
260+
/// Constructs a node id from a public key.
261+
#[uniffi::constructor]
262+
pub fn from_public_key(public_key: Arc<PublicKey>) -> Self {
263+
LdkNodeId::from_pubkey(public_key.as_ref().as_ref()).into()
264+
}
265+
266+
/// Returns the serialized node id.
267+
pub fn as_bytes(&self) -> Vec<u8> {
268+
self.inner.as_slice().to_vec()
269+
}
270+
271+
/// Returns the node id as a public key.
272+
pub fn as_public_key(&self) -> Result<Arc<PublicKey>, Error> {
273+
self.inner.as_pubkey().map(PublicKey::from).map(Arc::new).map_err(|_| Error::InvalidNodeId)
274+
}
275+
}
276+
277+
impl FromStr for NodeId {
278+
type Err = Error;
279+
280+
fn from_str(node_id_str: &str) -> Result<Self, Self::Err> {
281+
node_id_str.parse::<LdkNodeId>().map(Self::from).map_err(|_| Error::InvalidNodeId)
282+
}
283+
}
284+
285+
impl From<LdkNodeId> for NodeId {
286+
fn from(inner: LdkNodeId) -> Self {
287+
Self { inner }
288+
}
289+
}
290+
291+
impl Deref for NodeId {
292+
type Target = LdkNodeId;
293+
294+
fn deref(&self) -> &Self::Target {
295+
&self.inner
296+
}
297+
}
298+
299+
impl AsRef<LdkNodeId> for NodeId {
300+
fn as_ref(&self) -> &LdkNodeId {
301+
self.deref()
302+
}
303+
}
304+
305+
impl std::fmt::Display for NodeId {
306+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307+
write!(f, "{}", self.inner)
308+
}
309+
}
258310

259311
uniffi::custom_type!(Address, String, {
260312
remote,

src/graph.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,21 @@ use std::sync::Arc;
1111

1212
#[cfg(feature = "uniffi")]
1313
type SocketAddress = Arc<crate::ffi::SocketAddress>;
14-
use lightning::routing::gossip::NodeId;
14+
#[cfg(not(feature = "uniffi"))]
15+
use lightning::routing::gossip::NodeId as LdkNodeId;
1516
#[cfg(feature = "uniffi")]
1617
use lightning::routing::gossip::RoutingFees;
1718
#[cfg(not(feature = "uniffi"))]
1819
use lightning::routing::gossip::{ChannelInfo, NodeInfo};
1920

21+
use crate::ffi::{maybe_deref, maybe_wrap};
2022
use crate::types::Graph;
2123

24+
#[cfg(not(feature = "uniffi"))]
25+
type NodeId = LdkNodeId;
26+
#[cfg(feature = "uniffi")]
27+
type NodeId = Arc<crate::ffi::NodeId>;
28+
2229
/// Represents the network as nodes and channels between them.
2330
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
2431
pub struct NetworkGraph {
@@ -45,12 +52,12 @@ impl NetworkGraph {
4552

4653
/// Returns the list of nodes in the graph
4754
pub fn list_nodes(&self) -> Vec<NodeId> {
48-
self.inner.read_only().nodes().unordered_keys().map(|n| *n).collect()
55+
self.inner.read_only().nodes().unordered_keys().map(|n| maybe_wrap(*n)).collect()
4956
}
5057

5158
/// Returns information on a node with the given id.
5259
pub fn node(&self, node_id: &NodeId) -> Option<NodeInfo> {
53-
self.inner.read_only().nodes().get(node_id).cloned().map(|n| n.into())
60+
self.inner.read_only().nodes().get(maybe_deref(node_id)).cloned().map(|n| n.into())
5461
}
5562
}
5663

@@ -78,9 +85,9 @@ pub struct ChannelInfo {
7885
impl From<lightning::routing::gossip::ChannelInfo> for ChannelInfo {
7986
fn from(value: lightning::routing::gossip::ChannelInfo) -> Self {
8087
Self {
81-
node_one: value.node_one,
88+
node_one: maybe_wrap(value.node_one),
8289
one_to_two: value.one_to_two.map(|u| u.into()),
83-
node_two: value.node_two,
90+
node_two: maybe_wrap(value.node_two),
8491
two_to_one: value.two_to_one.map(|u| u.into()),
8592
capacity_sats: value.capacity_sats,
8693
}

0 commit comments

Comments
 (0)