Skip to main content
Glama
networkGraph.tsโ€ข23 kB
import { Tool, ToolResult } from '../types/tool.js'; export const networkGraphTool: Tool = { name: 'ldk_network_graph', description: 'Get network graph operations and RapidGossipSync implementation', inputSchema: { type: 'object', properties: { operation: { type: 'string', enum: [ 'setup', 'rapid_gossip_sync', 'query_routes', 'node_info', 'channel_info', 'update_handling' ], description: 'Network graph operation to perform' }, nodeId: { type: 'string', description: 'Node ID for node_info operation (optional)' }, channelId: { type: 'string', description: 'Channel ID for channel_info operation (optional)' } }, required: ['operation'] }, execute: async (args: any): Promise<ToolResult> => { const swiftExamples: Record<string, string> = { setup: ` // Network Graph and RapidGossipSync setup in Swift import LightningDevKit class NetworkGraphManager { private let networkGraph: Bindings.NetworkGraph private let rapidGossipSync: Bindings.RapidGossipSync private let logger: Bindings.Logger private let network: Bindings.Network private var lastSyncTimestamp: UInt64 = 0 init(network: Bindings.Network, logger: Bindings.Logger) { self.network = network self.logger = logger // Initialize or load network graph if let savedGraph = loadNetworkGraphFromDisk() { let readResult = Bindings.NetworkGraph.read(ser: savedGraph, arg: logger) if readResult.isOk() { self.networkGraph = readResult.getValue()! } else { self.networkGraph = Bindings.NetworkGraph(network: network, logger: logger) } } else { self.networkGraph = Bindings.NetworkGraph(network: network, logger: logger) } // Initialize RapidGossipSync self.rapidGossipSync = Bindings.RapidGossipSync(networkGraph: networkGraph, logger: logger) // Load last sync timestamp self.lastSyncTimestamp = UserDefaults.standard.object(forKey: "lastGossipSync") as? UInt64 ?? 0 } // Persist network graph to disk func saveNetworkGraph() throws { let serialized = networkGraph.write() let documentsPath = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask ).first! let graphPath = documentsPath.appendingPathComponent("network_graph.bin") try Data(serialized).write(to: graphPath) } private func loadNetworkGraphFromDisk() -> [UInt8]? { let documentsPath = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask ).first! let graphPath = documentsPath.appendingPathComponent("network_graph.bin") guard let data = try? Data(contentsOf: graphPath) else { return nil } return [UInt8](data) } }`.trim(), rapid_gossip_sync: ` // RapidGossipSync implementation for efficient network updates import LightningDevKit extension NetworkGraphManager { // Sync network graph using RapidGossipSync func syncNetworkGraph() async throws { let currentTime = UInt64(Date().timeIntervalSince1970) // Build RGS URL with last sync timestamp let baseUrl = network == .bitcoin ? "https://rapidsync.lightningdevkit.org/snapshot" : "https://rapidsync.lightningdevkit.org/testnet/snapshot" let url = URL(string: "\\(baseUrl)/\\(lastSyncTimestamp)")! // Download snapshot let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw NetworkError.syncFailed } // Apply update to network graph let result = rapidGossipSync.updateNetworkGraphNoStd( updateData: [UInt8](data), currentTimeUnix: currentTime ) if result.isOk() { // Update last sync timestamp lastSyncTimestamp = currentTime UserDefaults.standard.set(lastSyncTimestamp, forKey: "lastGossipSync") // Persist updated graph try saveNetworkGraph() print("RapidGossipSync successful - updated to timestamp: \\(currentTime)") } else if let error = result.getError() { throw NetworkError.graphUpdateFailed(error.getValueAsDecodeError()?.getDescription() ?? "Unknown error") } } // Schedule automatic sync func startAutomaticSync(interval: TimeInterval = 3600) { Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in Task { do { try await self.syncNetworkGraph() } catch { print("Automatic sync failed: \\(error)") } } } // Perform initial sync Task { try? await syncNetworkGraph() } } // Manual incremental update func applyGossipUpdate(_ update: [UInt8]) -> Bool { let currentTime = UInt64(Date().timeIntervalSince1970) let result = rapidGossipSync.updateNetworkGraphNoStd( updateData: update, currentTimeUnix: currentTime ) return result.isOk() } } // Background task for iOS extension NetworkGraphManager { func performBackgroundSync() async -> Bool { do { try await syncNetworkGraph() return true } catch { print("Background sync failed: \\(error)") return false } } }`.trim(), query_routes: ` // Query routes using the network graph import LightningDevKit extension NetworkGraphManager { // Find routes for a payment func findRoute( to destination: [UInt8], amountMsat: UInt64, paymentParams: Bindings.PaymentParameters ) -> Result<[RouteInfo], RoutingError> { let channelManager = LDKManager.shared.channelManager let ourNodeId = channelManager.getOurNodeId() let channels = channelManager.listUsableChannels() // Create route parameters let routeParams = Bindings.RouteParameters( paymentParamsArg: paymentParams, finalValueMsatArg: amountMsat, maxTotalRoutingFeeMsatArg: amountMsat / 100 // 1% max fee ) // Use router to find route let router = LDKManager.shared.router let inflightHtlcs = Bindings.InFlightHtlcs() let routeResult = router.findRoute( payer: ourNodeId, routeParams: routeParams, firstHops: channels, inflightHtlcs: inflightHtlcs ) if routeResult.isOk(), let route = routeResult.getValue() { return .success(analyzeRoute(route)) } else if let error = routeResult.getError() { return .failure(.routingFailed(error.getDescription())) } return .failure(.noRoute) } // Analyze route details private func analyzeRoute(_ route: Bindings.Route) -> [RouteInfo] { return route.getPaths().map { path in let hops = path.getHops().map { hop in HopInfo( pubkey: Data(hop.getPubkey()).hexString, shortChannelId: hop.getShortChannelId(), feeMsat: hop.getFeeMsat(), cltvExpiryDelta: hop.getCltvExpiryDelta() ) } let totalFees = path.getFeeMsat() let finalValueMsat = path.getFinalValueMsat() return RouteInfo( hops: hops, totalFeeMsat: totalFees, totalAmountMsat: finalValueMsat ) } } } struct RouteInfo { let hops: [HopInfo] let totalFeeMsat: UInt64 let totalAmountMsat: UInt64 } struct HopInfo { let pubkey: String let shortChannelId: UInt64 let feeMsat: UInt64 let cltvExpiryDelta: UInt32 } enum RoutingError: Error { case noRoute case routingFailed(String) case insufficientBalance }`.trim(), node_info: ` // Query node information from network graph import LightningDevKit extension NetworkGraphManager { // Get information about a specific node func getNodeInfo(nodeId: String) -> NodeDetails? { guard let pubkeyData = Data(hexString: nodeId), pubkeyData.count == 33 else { return nil } let nodeIdObj = Bindings.NodeId(pubkey: pubkeyData.bytes) guard let nodeInfo = networkGraph.readOnly().node(nodeId: nodeIdObj) else { return nil } // Extract node details let channels = nodeInfo.getChannels() let announcement = nodeInfo.getAnnouncementMessage() var nodeDetails = NodeDetails( nodeId: nodeId, alias: "", color: "", addresses: [], features: [], channelCount: channels.count, totalCapacityMsat: 0 ) // Parse announcement if available if let announcement = announcement { let contents = announcement.getContents() nodeDetails.alias = String(data: Data(contents.getAlias()), encoding: .utf8) ?? "" nodeDetails.color = Data(contents.getRgb()).hexString // Parse addresses nodeDetails.addresses = contents.getAddresses().compactMap { address in parseNetworkAddress(address) } // Features if let features = contents.getFeatures() { nodeDetails.features = parseNodeFeatures(features) } } // Calculate total capacity for channelId in channels { if let channelInfo = networkGraph.readOnly().channel(shortChannelId: channelId) { nodeDetails.totalCapacityMsat += channelInfo.getCapacityMsat() ?? 0 } } return nodeDetails } // Get all known nodes func getAllNodes() -> [NodeSummary] { let readOnly = networkGraph.readOnly() let nodeIds = readOnly.listNodes() return nodeIds.compactMap { nodeId in guard let nodeInfo = readOnly.node(nodeId: nodeId) else { return nil } let pubkey = Data(nodeId.asSlice()).hexString let channels = nodeInfo.getChannels() var alias = "" if let announcement = nodeInfo.getAnnouncementMessage() { alias = String(data: Data(announcement.getContents().getAlias()), encoding: .utf8) ?? "" } return NodeSummary( nodeId: pubkey, alias: alias, channelCount: channels.count, isReachable: !channels.isEmpty ) } } private func parseNetworkAddress(_ address: Bindings.SocketAddress) -> String? { switch address.getValueType() { case .TcpIpV4: if let ipv4 = address.getValueAsTcpIpV4() { let addr = ipv4.getAddr() return "\\(addr.0).\\(addr.1).\\(addr.2).\\(addr.3):\\(ipv4.getPort())" } case .TcpIpV6: if let ipv6 = address.getValueAsTcpIpV6() { return "[\\(ipv6.getAddr().map{String($0)}.joined(separator:":"))]:\\(ipv6.getPort())" } case .OnionV3: if let onion = address.getValueAsOnionV3() { return "\\(Data(onion.getEd25519Pubkey()).hexString).onion:\\(onion.getPort())" } default: break } return nil } private func parseNodeFeatures(_ features: Bindings.NodeFeatures) -> [String] { var featureList: [String] = [] if features.supportsVariableLengthOnion() { featureList.append("Variable Length Onion") } if features.supportsStaticRemoteKey() { featureList.append("Static Remote Key") } if features.supportsAnchors() { featureList.append("Anchor Outputs") } return featureList } } struct NodeDetails { var nodeId: String var alias: String var color: String var addresses: [String] var features: [String] var channelCount: Int var totalCapacityMsat: UInt64 } struct NodeSummary { let nodeId: String let alias: String let channelCount: Int let isReachable: Bool }`.trim(), channel_info: ` // Query channel information from network graph import LightningDevKit extension NetworkGraphManager { // Get information about a specific channel func getChannelInfo(shortChannelId: UInt64) -> ChannelDetails? { guard let channelInfo = networkGraph.readOnly().channel(shortChannelId: shortChannelId) else { return nil } let node1 = Data(channelInfo.getNodeOne().asSlice()).hexString let node2 = Data(channelInfo.getNodeTwo().asSlice()).hexString var details = ChannelDetails( shortChannelId: shortChannelId, node1: node1, node2: node2, capacityMsat: channelInfo.getCapacityMsat(), node1Policy: nil, node2Policy: nil, lastUpdate: nil ) // Get directional policies if let node1Policy = channelInfo.getOneToTwo() { details.node1Policy = parseChannelPolicy(node1Policy) } if let node2Policy = channelInfo.getTwoToOne() { details.node2Policy = parseChannelPolicy(node2Policy) } // Get announcement details if let announcement = channelInfo.getAnnouncementMessage() { let contents = announcement.getContents() details.lastUpdate = Date(timeIntervalSince1970: TimeInterval(contents.getTimestamp())) } return details } // Get all channels for a node func getNodeChannels(nodeId: String) -> [ChannelSummary] { guard let pubkeyData = Data(hexString: nodeId), pubkeyData.count == 33 else { return [] } let nodeIdObj = Bindings.NodeId(pubkey: pubkeyData.bytes) guard let nodeInfo = networkGraph.readOnly().node(nodeId: nodeIdObj) else { return [] } return nodeInfo.getChannels().compactMap { channelId in guard let channelInfo = networkGraph.readOnly().channel(shortChannelId: channelId) else { return nil } let node1 = Data(channelInfo.getNodeOne().asSlice()).hexString let node2 = Data(channelInfo.getNodeTwo().asSlice()).hexString let isNode1 = node1 == nodeId let remoteNode = isNode1 ? node2 : node1 // Get our policy let ourPolicy = isNode1 ? channelInfo.getOneToTwo() : channelInfo.getTwoToOne() let theirPolicy = isNode1 ? channelInfo.getTwoToOne() : channelInfo.getOneToTwo() return ChannelSummary( shortChannelId: channelId, remoteNode: remoteNode, capacityMsat: channelInfo.getCapacityMsat(), isEnabled: ourPolicy?.getEnabled() ?? false, baseFee: ourPolicy?.getFeeBaseMsat() ?? 0, feeRate: ourPolicy?.getFeeProportionalMillionths() ?? 0, remoteBaseFee: theirPolicy?.getFeeBaseMsat() ?? 0, remoteFeeRate: theirPolicy?.getFeeProportionalMillionths() ?? 0 ) } } private func parseChannelPolicy(_ update: Bindings.ChannelUpdateInfo) -> ChannelPolicy { return ChannelPolicy( enabled: update.getEnabled(), cltvExpiryDelta: update.getCltvExpiryDelta(), htlcMinimumMsat: update.getHtlcMinimumMsat(), htlcMaximumMsat: update.getHtlcMaximumMsat(), feeBaseMsat: update.getFeeBaseMsat(), feeProportionalMillionths: update.getFeeProportionalMillionths(), lastUpdate: Date(timeIntervalSince1970: TimeInterval(update.getLastUpdate())) ) } } struct ChannelDetails { let shortChannelId: UInt64 let node1: String let node2: String let capacityMsat: UInt64? var node1Policy: ChannelPolicy? var node2Policy: ChannelPolicy? var lastUpdate: Date? } struct ChannelPolicy { let enabled: Bool let cltvExpiryDelta: UInt16 let htlcMinimumMsat: UInt64 let htlcMaximumMsat: UInt64? let feeBaseMsat: UInt32 let feeProportionalMillionths: UInt32 let lastUpdate: Date } struct ChannelSummary { let shortChannelId: UInt64 let remoteNode: String let capacityMsat: UInt64? let isEnabled: Bool let baseFee: UInt32 let feeRate: UInt32 let remoteBaseFee: UInt32 let remoteFeeRate: UInt32 }`.trim(), update_handling: ` // Handle network graph updates import LightningDevKit extension NetworkGraphManager { // Process channel announcement func handleChannelAnnouncement(_ announcement: Bindings.ChannelAnnouncement) -> Bool { let result = networkGraph.updateChannelFromAnnouncement( msg: announcement, utxoLookup: nil ) if result.isOk() { // Persist updated graph try? saveNetworkGraph() return true } return false } // Process channel update func handleChannelUpdate(_ update: Bindings.ChannelUpdate) -> Bool { let result = networkGraph.updateChannelFromUnsignedAnnouncement( msg: update.getContents(), utxoLookup: nil ) if result.isOk() { try? saveNetworkGraph() return true } return false } // Process node announcement func handleNodeAnnouncement(_ announcement: Bindings.NodeAnnouncement) -> Bool { let result = networkGraph.updateNodeFromAnnouncement(msg: announcement) if result.isOk() { try? saveNetworkGraph() return true } return false } // Prune old channels func pruneStaleChannels() { let currentTime = UInt64(Date().timeIntervalSince1970) let twoWeeksAgo = currentTime - (14 * 24 * 60 * 60) networkGraph.removeStaleChannelsAndTrackedNodes(currentTimeUnix: twoWeeksAgo) // Persist pruned graph try? saveNetworkGraph() } // Monitor graph statistics func getGraphStatistics() -> GraphStatistics { let readOnly = networkGraph.readOnly() let nodeCount = readOnly.listNodes().count var channelCount = 0 var totalCapacityMsat: UInt64 = 0 for node in readOnly.listNodes() { if let nodeInfo = readOnly.node(nodeId: node) { let channels = nodeInfo.getChannels() channelCount += channels.count for channelId in channels { if let channelInfo = readOnly.channel(shortChannelId: channelId) { totalCapacityMsat += channelInfo.getCapacityMsat() ?? 0 } } } } // Channels are counted twice (once per node) channelCount /= 2 return GraphStatistics( nodeCount: nodeCount, channelCount: channelCount, totalCapacityMsat: totalCapacityMsat, lastSync: Date(timeIntervalSince1970: TimeInterval(lastSyncTimestamp)) ) } } struct GraphStatistics { let nodeCount: Int let channelCount: Int let totalCapacityMsat: UInt64 let lastSync: Date } // SwiftUI View for Network Graph Stats struct NetworkGraphView: View { @StateObject private var viewModel = NetworkGraphViewModel() var body: some View { List { Section("Network Statistics") { StatRow(label: "Nodes", value: "\\(viewModel.stats.nodeCount)") StatRow(label: "Channels", value: "\\(viewModel.stats.channelCount)") StatRow(label: "Total Capacity", value: "\\(viewModel.stats.totalCapacityMsat / 1_000_000_000) BTC") } Section("Sync Status") { HStack { Text("Last Sync") Spacer() Text(viewModel.stats.lastSync, style: .relative) .foregroundColor(.secondary) } Button("Sync Now") { Task { await viewModel.syncNetworkGraph() } } .disabled(viewModel.isSyncing) } if viewModel.isSyncing { Section { HStack { ProgressView() Text("Syncing network graph...") .foregroundColor(.secondary) } } } } .navigationTitle("Network Graph") .onAppear { viewModel.loadStatistics() } } } struct StatRow: View { let label: String let value: String var body: some View { HStack { Text(label) Spacer() Text(value) .fontWeight(.medium) .foregroundColor(.secondary) } } }`.trim() }; try { const code = swiftExamples[args.operation]; if (!code) { throw new Error(`Unknown operation: ${args.operation}`); } return { content: [{ type: 'text', text: JSON.stringify({ success: true, operation: args.operation, swiftCode: code, description: `NetworkGraph and RapidGossipSync implementation for ${args.operation}` }, null, 2) }] }; } catch (error) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }, null, 2) }], isError: true }; } } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/StevenGeller/ldk-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server