Skip to main content
Glama
closeChannel.tsโ€ข8.81 kB
import { Tool, ToolResult } from '../types/tool.js'; import { LightningService } from '../services/lightningService.js'; const lightningService = new LightningService(); export const closeChannelTool: Tool = { name: 'ldk_close_channel', description: 'Close a Lightning channel cooperatively or force close', inputSchema: { type: 'object', properties: { channelId: { type: 'string', description: 'Channel ID to close' }, force: { type: 'boolean', description: 'Force close the channel', default: false } }, required: ['channelId'] }, execute: async (args: any): Promise<ToolResult> => { try { const success = await lightningService.closeChannel(args.channelId); return { content: [{ type: 'text', text: JSON.stringify({ success, channelId: args.channelId, closeType: args.force ? 'force' : 'cooperative', message: 'Channel closing initiated', swiftExample: ` // Swift code for channel closing in your iOS app import LightningDevKit class ChannelCloser { let channelManager: ChannelManager func closeChannel( channelId: String, counterpartyNodeId: String, force: Bool = false ) throws { guard let channelIdData = Data(hexString: channelId), channelIdData.count == 32, let nodeIdData = Data(hexString: counterpartyNodeId), nodeIdData.count == 33 else { throw ChannelError.invalidParameters } let result: Result_NoneAPIErrorZ if force { // Force close the channel result = channelManager.forceCloseBroadcastingLatestTxn( channelId: channelIdData.bytes, counterpartyNodeId: nodeIdData.bytes ) } else { // Cooperative close result = channelManager.closeChannel( channelId: channelIdData.bytes, counterpartyNodeId: nodeIdData.bytes ) } guard result.isOk() else { throw ChannelError.closeFailed(result.getError()!) } } // Handle spendable outputs after channel close func handleSpendableOutputs(event: Event.SpendableOutputs) async throws { let outputs = event.getOutputs() // Get destination address from wallet let address = try wallet.getAddress(addressIndex: .new) let script = try Address( address: address.address.asString(), network: .testnet ).scriptPubkey().toBytes() // Create sweep transaction let result = keysManager.spendSpendableOutputs( descriptors: outputs, outputs: [], // No additional outputs changeDestinationScript: script, feerateSatPer1000Weight: 1000, locktime: nil // Current block height recommended ) guard result.isOk(), let tx = result.getValue() else { throw ChannelError.sweepFailed } // Broadcast sweep transaction try await broadcastTransaction(Data(tx)) } } // SwiftUI view for channel management struct ChannelDetailView: View { let channel: ChannelDetails @State private var showingCloseAlert = false @State private var isClosing = false @State private var closeError: String? @Environment(\\.dismiss) private var dismiss var body: some View { ScrollView { VStack(spacing: 20) { // Channel status card VStack(alignment: .leading, spacing: 12) { HStack { Circle() .fill(channel.isUsable ? Color.green : Color.orange) .frame(width: 12, height: 12) Text(channelStatus) .font(.headline) Spacer() if let scid = channel.getShortChannelId() { Text(scid.description) .font(.caption) .foregroundColor(.secondary) } } Divider() // Balances VStack(spacing: 8) { BalanceRow( label: "Local Balance", amount: localBalanceSats, color: .green ) BalanceRow( label: "Remote Balance", amount: remoteBalanceSats, color: .blue ) BalanceRow( label: "Channel Capacity", amount: Int(channel.channelValueSatoshis), color: .primary ) } } .padding() .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(12) // Actions VStack(spacing: 12) { Button(action: { showingCloseAlert = true }) { Label("Close Channel", systemImage: "xmark.circle") .foregroundColor(.red) .frame(maxWidth: .infinity) } .buttonStyle(.bordered) .disabled(isClosing) } } .padding() } .navigationTitle("Channel Details") .navigationBarTitleDisplayMode(.inline) .alert("Close Channel", isPresented: $showingCloseAlert) { Button("Cooperative Close", role: .destructive) { closeChannel(force: false) } Button("Force Close", role: .destructive) { closeChannel(force: true) } Button("Cancel", role: .cancel) {} } message: { Text("Cooperative close is cheaper but requires the peer to be online. Force close can be done immediately but costs more in fees.") } .alert("Error", isPresented: .constant(closeError != nil)) { Button("OK") { closeError = nil } } message: { Text(closeError ?? "") } .overlay { if isClosing { Color.black.opacity(0.3) .ignoresSafeArea() ProgressView("Closing channel...") .padding() .background(Color(UIColor.systemBackground)) .cornerRadius(10) .shadow(radius: 5) } } } var channelStatus: String { if channel.isUsable { return "Active" } else if channel.isChannelReady { return "Ready" } else { return "Pending" } } var localBalanceSats: Int { Int(channel.balanceMsat / 1000) } var remoteBalanceSats: Int { Int(channel.channelValueSatoshis) - localBalanceSats } func closeChannel(force: Bool) { Task { isClosing = true defer { isClosing = false } do { try LDKManager.shared.closeChannel( channelId: channel.getChannelId().toHex(), counterpartyNodeId: channel.getCounterpartyNodeId().toHex(), force: force ) dismiss() } catch { closeError = error.localizedDescription } } } } struct BalanceRow: View { let label: String let amount: Int let color: Color var body: some View { HStack { Text(label) .foregroundColor(.secondary) Spacer() Text("\\(amount.formatted()) sats") .fontWeight(.medium) .foregroundColor(color) } } }`.trim() }, 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