Skip to main content
Glama
StevenGeller

LDK MCP Server

by StevenGeller

ldk_event_handling

Handle Lightning Network events in iOS wallets by implementing LDK patterns for payments, channels, funding, and synchronization.

Instructions

Get comprehensive LDK event handling patterns and implementations

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
eventTypeYesType of event handling to get code for

Implementation Reference

  • src/index.ts:38-62 (registration)
    Registration of the eventHandlingTool in the MCP server's tools array.
    const tools = [
      generateInvoiceTool,
      payInvoiceTool,
      getChannelStatusTool,
      getNodeInfoTool,
      backupStateTool,
      keychainTestTool,
      backgroundTestTool,
      pushNotificationTool,
      biometricAuthTool,
      createChannelTool,
      closeChannelTool,
      getBalanceTool,
      decodeInvoiceTool,
      listPaymentsTool,
      estimateFeeTool,
      generateMnemonicTool,
      deriveAddressTool,
      getSwiftCodeTool,
      getArchitectureTool,
      testScenarioTool,
      networkGraphTool,
      eventHandlingTool,
      chainSyncTool,
    ];
  • The main tool handler. Defines the tool with name 'ldk_event_handling', input schema, and execute function that returns comprehensive Swift code examples for various LDK event handling scenarios based on the eventType parameter.
    export const eventHandlingTool: Tool = {
      name: 'ldk_event_handling',
      description: 'Get comprehensive LDK event handling patterns and implementations',
      inputSchema: {
        type: 'object',
        properties: {
          eventType: {
            type: 'string',
            enum: [
              'all_events',
              'payment_events',
              'channel_events',
              'funding_events',
              'spendable_outputs',
              'forwarding_events',
              'event_persistence'
            ],
            description: 'Type of event handling to get code for'
          }
        },
        required: ['eventType']
      },
      execute: async (args: any): Promise<ToolResult> => {
        const eventExamples: Record<string, string> = {
          all_events: `
    // Comprehensive LDK Event Handler
    import LightningDevKit
    import BitcoinDevKit
    
    class LDKEventHandler: Bindings.EventHandler {
        private let channelManager: Bindings.ChannelManager
        private let wallet: BitcoinDevKit.Wallet
        private let keysManager: Bindings.KeysManager
        private let networkGraph: Bindings.NetworkGraph
        private let logger: Bindings.Logger
        private let broadcaster: Bindings.BroadcasterInterface
        
        init(
            channelManager: Bindings.ChannelManager,
            wallet: BitcoinDevKit.Wallet,
            keysManager: Bindings.KeysManager,
            networkGraph: Bindings.NetworkGraph,
            logger: Bindings.Logger,
            broadcaster: Bindings.BroadcasterInterface
        ) {
            self.channelManager = channelManager
            self.wallet = wallet
            self.keysManager = keysManager
            self.networkGraph = networkGraph
            self.logger = logger
            self.broadcaster = broadcaster
            super.init()
        }
        
        override func handleEvent(event: Bindings.Event) {
            // Log all events
            logger.log(message: "Handling event: \\(event.getValueType())")
            
            switch event.getValueType() {
            // Payment Events
            case .PaymentClaimable:
                handlePaymentClaimable(event.getValueAsPaymentClaimable()!)
            case .PaymentClaimed:
                handlePaymentClaimed(event.getValueAsPaymentClaimed()!)
            case .PaymentSent:
                handlePaymentSent(event.getValueAsPaymentSent()!)
            case .PaymentFailed:
                handlePaymentFailed(event.getValueAsPaymentFailed()!)
            case .PaymentPathSuccessful:
                handlePaymentPathSuccessful(event.getValueAsPaymentPathSuccessful()!)
            case .PaymentPathFailed:
                handlePaymentPathFailed(event.getValueAsPaymentPathFailed()!)
            case .PaymentForwarded:
                handlePaymentForwarded(event.getValueAsPaymentForwarded()!)
                
            // Channel Events
            case .FundingGenerationReady:
                handleFundingGenerationReady(event.getValueAsFundingGenerationReady()!)
            case .ChannelReady:
                handleChannelReady(event.getValueAsChannelReady()!)
            case .ChannelClosed:
                handleChannelClosed(event.getValueAsChannelClosed()!)
            case .DiscardFunding:
                handleDiscardFunding(event.getValueAsDiscardFunding()!)
            case .OpenChannelRequest:
                handleOpenChannelRequest(event.getValueAsOpenChannelRequest()!)
                
            // HTLC Events
            case .HTLCIntercepted:
                handleHTLCIntercepted(event.getValueAsHTLCIntercepted()!)
            case .HTLCHandlingFailed:
                handleHTLCHandlingFailed(event.getValueAsHTLCHandlingFailed()!)
                
            // Output Events
            case .SpendableOutputs:
                handleSpendableOutputs(event.getValueAsSpendableOutputs()!)
                
            // Probe Events
            case .ProbeFailed:
                handleProbeFailed(event.getValueAsProbeFailed()!)
            case .ProbeSuccessful:
                handleProbeSuccessful(event.getValueAsProbeSuccessful()!)
                
            // Other Events
            case .PendingHTLCsForwardable:
                handlePendingHTLCsForwardable(event.getValueAsPendingHTLCsForwardable()!)
            case .BumpTransaction:
                handleBumpTransaction(event.getValueAsBumpTransaction()!)
            case .InvoiceRequestFailed:
                handleInvoiceRequestFailed(event.getValueAsInvoiceRequestFailed()!)
            case .ConnectionNeeded:
                handleConnectionNeeded(event.getValueAsConnectionNeeded()!)
                
            default:
                logger.log(message: "Unhandled event type: \\(event.getValueType())")
            }
            
            // Persist event for recovery
            persistEvent(event)
        }
        
        private func persistEvent(_ event: Bindings.Event) {
            // Store event for potential replay during recovery
            EventPersistence.shared.store(event: event)
        }
    }`.trim(),
    
          payment_events: `
    // Payment Event Handling
    import LightningDevKit
    import UserNotifications
    
    extension LDKEventHandler {
        // Payment Claimable - Incoming payment detected
        func handlePaymentClaimable(_ event: Bindings.Event.PaymentClaimable) {
            let paymentHash = event.getPaymentHash()
            let amountMsat = event.getClaimableAmountMsat()
            let purpose = event.getPurpose()
            let claimDeadline = event.getClaimDeadline()
            
            // Auto-claim the payment
            if let preimage = extractPaymentPreimage(purpose: purpose) {
                channelManager.claimFunds(paymentPreimage: preimage)
                
                // Log successful claim attempt
                logger.log(message: "Claiming payment: \\(Data(paymentHash).hexString) for \\(amountMsat) msat")
            }
            
            // Store pending payment
            PaymentStore.shared.storePendingPayment(
                paymentHash: Data(paymentHash).hexString,
                amountMsat: amountMsat,
                purpose: purpose,
                claimDeadline: claimDeadline
            )
            
            // Notify UI
            DispatchQueue.main.async {
                NotificationCenter.default.post(
                    name: .paymentClaimable,
                    object: nil,
                    userInfo: [
                        "paymentHash": Data(paymentHash).hexString,
                        "amountMsat": amountMsat,
                        "description": self.extractDescription(purpose: purpose)
                    ]
                )
            }
        }
        
        // Payment Claimed - Successfully received payment
        func handlePaymentClaimed(_ event: Bindings.Event.PaymentClaimed) {
            let paymentHash = event.getPaymentHash()
            let amountMsat = event.getAmountMsat()
            let purpose = event.getPurpose()
            let receivers = event.getReceivers()
            let htlcs = event.getHtlcs()
            
            // Update payment record
            PaymentStore.shared.markPaymentClaimed(
                paymentHash: Data(paymentHash).hexString,
                amountMsat: amountMsat,
                receivers: receivers,
                htlcs: htlcs
            )
            
            // Show notification
            showPaymentNotification(
                title: "Payment Received",
                body: "Received \\(amountMsat / 1000) sats",
                identifier: Data(paymentHash).hexString
            )
            
            // Update balance
            BalanceManager.shared.updateBalance()
            
            // Log for analytics
            Analytics.logPaymentReceived(
                amountMsat: amountMsat,
                paymentType: getPaymentType(purpose: purpose)
            )
        }
        
        // Payment Sent - Outgoing payment succeeded
        func handlePaymentSent(_ event: Bindings.Event.PaymentSent) {
            let paymentId = event.getPaymentId()
            let paymentPreimage = event.getPaymentPreimage()
            let paymentHash = event.getPaymentHash()
            let feePaidMsat = event.getFeePaidMsat()?.getValue() ?? 0
            
            // Update payment record
            PaymentStore.shared.markPaymentSent(
                paymentId: paymentId?.data.hexString,
                paymentHash: Data(paymentHash).hexString,
                preimage: Data(paymentPreimage).hexString,
                feeMsat: feePaidMsat
            )
            
            // Update UI
            DispatchQueue.main.async {
                NotificationCenter.default.post(
                    name: .paymentSent,
                    object: nil,
                    userInfo: [
                        "paymentHash": Data(paymentHash).hexString,
                        "feeMsat": feePaidMsat
                    ]
                )
            }
            
            // Log success
            logger.log(message: "Payment sent successfully. Fee: \\(feePaidMsat) msat")
        }
        
        // Payment Failed - Outgoing payment failed
        func handlePaymentFailed(_ event: Bindings.Event.PaymentFailed) {
            let paymentId = event.getPaymentId()
            let paymentHash = event.getPaymentHash()
            let reason = event.getReason()
            
            // Parse failure reason
            let failureReason = parsePaymentFailureReason(reason)
            
            // Update payment record
            PaymentStore.shared.markPaymentFailed(
                paymentId: paymentId.data.hexString,
                paymentHash: Data(paymentHash).hexString,
                reason: failureReason
            )
            
            // Notify UI with detailed error
            DispatchQueue.main.async {
                NotificationCenter.default.post(
                    name: .paymentFailed,
                    object: nil,
                    userInfo: [
                        "paymentHash": Data(paymentHash).hexString,
                        "reason": failureReason.description,
                        "isRetryable": failureReason.isRetryable
                    ]
                )
            }
            
            // Log failure for debugging
            logger.log(message: "Payment failed: \\(failureReason.description)")
        }
        
        // Helper methods
        private func extractPaymentPreimage(purpose: Bindings.PaymentPurpose) -> [UInt8]? {
            switch purpose.getValueType() {
            case .Bolt11InvoicePayment:
                if let bolt11 = purpose.getValueAsBolt11InvoicePayment() {
                    return bolt11.getPaymentPreimage()?.getValue()
                }
            case .Bolt12OfferPayment:
                if let bolt12 = purpose.getValueAsBolt12OfferPayment() {
                    return bolt12.getPaymentPreimage()?.getValue()
                }
            case .Bolt12RefundPayment:
                if let refund = purpose.getValueAsBolt12RefundPayment() {
                    return refund.getPaymentPreimage()?.getValue()
                }
            case .SpontaneousPayment:
                if let spontaneous = purpose.getValueAsSpontaneousPayment() {
                    return spontaneous.getSpontaneousPayment()
                }
            default:
                break
            }
            return nil
        }
        
        private func extractDescription(purpose: Bindings.PaymentPurpose) -> String {
            switch purpose.getValueType() {
            case .Bolt11InvoicePayment:
                return "Lightning Invoice"
            case .Bolt12OfferPayment:
                return "BOLT12 Offer"
            case .Bolt12RefundPayment:
                return "BOLT12 Refund"
            case .SpontaneousPayment:
                return "Spontaneous Payment"
            default:
                return "Unknown Payment"
            }
        }
        
        private func parsePaymentFailureReason(_ reason: Bindings.PaymentFailureReason?) -> PaymentFailure {
            guard let reason = reason else {
                return PaymentFailure(type: .unknown, description: "Unknown failure", isRetryable: true)
            }
            
            switch reason.getValueType() {
            case .RecipientRejected:
                return PaymentFailure(type: .recipientRejected, description: "Recipient rejected payment", isRetryable: false)
            case .UserAbandoned:
                return PaymentFailure(type: .userAbandoned, description: "Payment abandoned by user", isRetryable: false)
            case .RetriesExhausted:
                return PaymentFailure(type: .retriesExhausted, description: "All payment retries exhausted", isRetryable: false)
            case .PaymentExpired:
                return PaymentFailure(type: .expired, description: "Payment expired", isRetryable: false)
            case .RouteNotFound:
                return PaymentFailure(type: .noRoute, description: "No route found", isRetryable: true)
            case .UnexpectedError:
                return PaymentFailure(type: .unexpected, description: "Unexpected error", isRetryable: true)
            default:
                return PaymentFailure(type: .unknown, description: "Unknown failure", isRetryable: true)
            }
        }
    }
    
    struct PaymentFailure {
        enum FailureType {
            case recipientRejected
            case userAbandoned
            case retriesExhausted
            case expired
            case noRoute
            case unexpected
            case unknown
        }
        
        let type: FailureType
        let description: String
        let isRetryable: Bool
    }
    
    // Notification Names
    extension Notification.Name {
        static let paymentClaimable = Notification.Name("ldk.payment.claimable")
        static let paymentClaimed = Notification.Name("ldk.payment.claimed")
        static let paymentSent = Notification.Name("ldk.payment.sent")
        static let paymentFailed = Notification.Name("ldk.payment.failed")
    }`.trim(),
    
          channel_events: `
    // Channel Event Handling
    import LightningDevKit
    import BitcoinDevKit
    
    extension LDKEventHandler {
        // Funding Generation Ready - Need to create funding transaction
        func handleFundingGenerationReady(_ event: Bindings.Event.FundingGenerationReady) {
            Task {
                do {
                    let channelValue = event.getChannelValueSatoshis()
                    let outputScript = event.getOutputScript()
                    let userChannelId = event.getUserChannelId()
                    
                    // Build funding transaction with BDK
                    let address = try BitcoinDevKit.Address(
                        scriptPubkey: BitcoinDevKit.Script(rawOutputScript: outputScript)
                    )
                    
                    let txBuilder = try BitcoinDevKit.TxBuilder()
                        .addRecipient(script: address.scriptPubkey(), amount: channelValue)
                        .feeRate(satPerVbyte: Float(await getFeeRate()))
                        .enableRbf()
                    
                    let psbt = try txBuilder.finish(wallet: wallet)
                    let signResult = try wallet.sign(psbt: psbt, signOptions: nil)
                    
                    guard signResult.isFinalized else {
                        throw ChannelError.fundingNotFinalized
                    }
                    
                    let fundingTx = signResult.psbt.extractTx()
                    
                    // Provide funding transaction to LDK
                    let fundingResult = channelManager.fundingTransactionGenerated(
                        temporaryChannelId: event.getTemporaryChannelId(),
                        counterpartyNodeId: event.getCounterpartyNodeId(),
                        fundingTransaction: fundingTx.serialize()
                    )
                    
                    guard fundingResult.isOk() else {
                        throw ChannelError.fundingFailed(fundingResult.getError()?.getDescription() ?? "Unknown error")
                    }
                    
                    // Broadcast transaction
                    broadcaster.broadcastTransactions(txs: [fundingTx.serialize()])
                    
                    // Store channel info
                    ChannelStore.shared.storePendingChannel(
                        temporaryChannelId: event.getTemporaryChannelId(),
                        userChannelId: Data(userChannelId).hexString,
                        counterpartyNodeId: Data(event.getCounterpartyNodeId()).hexString,
                        fundingTxid: fundingTx.txid(),
                        channelValueSats: channelValue
                    )
                    
                    // Notify UI
                    DispatchQueue.main.async {
                        NotificationCenter.default.post(
                            name: .channelPendingFunding,
                            object: nil,
                            userInfo: [
                                "temporaryChannelId": event.getTemporaryChannelId().data.hexString,
                                "fundingTxid": fundingTx.txid()
                            ]
                        )
                    }
                    
                } catch {
                    logger.log(message: "Funding generation failed: \\(error)")
                    
                    // Notify failure
                    DispatchQueue.main.async {
                        NotificationCenter.default.post(
                            name: .channelFundingFailed,
                            object: nil,
                            userInfo: ["error": error.localizedDescription]
                        )
                    }
                }
            }
        }
        
        // Channel Ready - Channel is now usable
        func handleChannelReady(_ event: Bindings.Event.ChannelReady) {
            let channelId = event.getChannelId()
            let userChannelId = event.getUserChannelId()
            let counterpartyNodeId = event.getCounterpartyNodeId()
            let channelType = event.getChannelType()
            
            // Update channel status
            ChannelStore.shared.markChannelReady(
                channelId: channelId,
                userChannelId: Data(userChannelId).hexString,
                counterpartyNodeId: Data(counterpartyNodeId).hexString,
                channelType: channelType
            )
            
            // Update routing hints for invoices
            updateRoutingHints()
            
            // Notify UI
            DispatchQueue.main.async {
                NotificationCenter.default.post(
                    name: .channelReady,
                    object: nil,
                    userInfo: [
                        "channelId": channelId.data.hexString,
                        "counterpartyNodeId": Data(counterpartyNodeId).hexString
                    ]
                )
            }
            
            // Show notification
            showChannelNotification(
                title: "Channel Ready",
                body: "Channel is now active and ready for payments",
                identifier: channelId.data.hexString
            )
            
            logger.log(message: "Channel ready: \\(channelId.data.hexString)")
        }
        
        // Channel Closed - Channel has been closed
        func handleChannelClosed(_ event: Bindings.Event.ChannelClosed) {
            let channelId = event.getChannelId()
            let userChannelId = event.getUserChannelId()
            let reason = event.getReason()
            let counterpartyNodeId = event.getCounterpartyNodeId()
            let channelCapacityMsat = event.getChannelCapacitySats()?.getValue()
            let channelFundingTxo = event.getChannelFundingTxo()
            
            // Parse closure reason
            let closureReason = parseClosureReason(reason)
            
            // Update channel status
            ChannelStore.shared.markChannelClosed(
                channelId: channelId,
                userChannelId: Data(userChannelId).hexString,
                reason: closureReason,
                capacityMsat: channelCapacityMsat
            )
            
            // Handle based on closure type
            switch closureReason.type {
            case .cooperativeClosure:
                logger.log(message: "Channel closed cooperatively")
            case .commitmentTxConfirmed:
                logger.log(message: "Channel force closed - commitment tx confirmed")
                // Monitor for penalty transactions if needed
            case .counterpartyForceClosed:
                logger.log(message: "Counterparty force closed channel")
                // Ensure we claim our outputs
            case .holderForceClosed:
                logger.log(message: "We force closed the channel")
            default:
                logger.log(message: "Channel closed: \\(closureReason.description)")
            }
            
            // Update routing hints
            updateRoutingHints()
            
            // Notify UI
            DispatchQueue.main.async {
                NotificationCenter.default.post(
                    name: .channelClosed,
                    object: nil,
                    userInfo: [
                        "channelId": channelId.data.hexString,
                        "reason": closureReason.description,
                        "isForceClose": closureReason.isForceClose
                    ]
                )
            }
        }
        
        // Open Channel Request - Counterparty wants to open channel
        func handleOpenChannelRequest(_ event: Bindings.Event.OpenChannelRequest) {
            let temporaryChannelId = event.getTemporaryChannelId()
            let counterpartyNodeId = event.getCounterpartyNodeId()
            let fundingSatoshis = event.getFundingSatoshis()
            let pushMsat = event.getPushMsat()
            let channelType = event.getChannelType()
            
            // Check if we want to accept this channel
            let shouldAccept = evaluateChannelRequest(
                counterpartyNodeId: Data(counterpartyNodeId).hexString,
                fundingSatoshis: fundingSatoshis,
                pushMsat: pushMsat,
                channelType: channelType
            )
            
            if shouldAccept {
                // Accept the channel
                let userChannelId = generateUserChannelId()
                let acceptResult = channelManager.acceptInboundChannel(
                    temporaryChannelId: temporaryChannelId,
                    counterpartyNodeId: counterpartyNodeId,
                    userChannelId: userChannelId
                )
                
                if acceptResult.isOk() {
                    logger.log(message: "Accepted inbound channel from \\(Data(counterpartyNodeId).hexString)")
                    
                    // Store pending channel
                    ChannelStore.shared.storeInboundPendingChannel(
                        temporaryChannelId: temporaryChannelId,
                        userChannelId: Data(userChannelId).hexString,
                        counterpartyNodeId: Data(counterpartyNodeId).hexString,
                        fundingSatoshis: fundingSatoshis,
                        pushMsat: pushMsat
                    )
                } else {
                    logger.log(message: "Failed to accept channel: \\(acceptResult.getError()?.getDescription() ?? "Unknown")")
                }
            } else {
                // Reject the channel
                let rejectResult = channelManager.forceCloseWithoutBroadcastingTxn(
                    channelId: temporaryChannelId,
                    counterpartyNodeId: counterpartyNodeId
                )
                
                logger.log(message: "Rejected inbound channel from \\(Data(counterpartyNodeId).hexString)")
            }
        }
        
        // Helper methods
        private func parseClosureReason(_ reason: Bindings.ClosureReason) -> ChannelClosureInfo {
            switch reason.getValueType() {
            case .CounterpartyForceClosed:
                let details = reason.getValueAsCounterpartyForceClosed()!
                return ChannelClosureInfo(
                    type: .counterpartyForceClosed,
                    description: "Counterparty force closed: \\(details.getPeerMessage())",
                    isForceClose: true
                )
            case .HolderForceClosed:
                return ChannelClosureInfo(
                    type: .holderForceClosed,
                    description: "We force closed the channel",
                    isForceClose: true
                )
            case .CooperativeClosure:
                return ChannelClosureInfo(
                    type: .cooperativeClosure,
                    description: "Channel closed cooperatively",
                    isForceClose: false
                )
            case .CommitmentTxConfirmed:
                return ChannelClosureInfo(
                    type: .commitmentTxConfirmed,
                    description: "Commitment transaction confirmed on-chain",
                    isForceClose: true
                )
            case .FundingTimedOut:
                return ChannelClosureInfo(
                    type: .fundingTimeout,
                    description: "Channel funding timed out",
                    isForceClose: false
                )
            case .ProcessingError:
                let error = reason.getValueAsProcessingError()!
                return ChannelClosureInfo(
                    type: .processingError,
                    description: "Processing error: \\(error.getErr())",
                    isForceClose: false
                )
            case .DisconnectedPeer:
                return ChannelClosureInfo(
                    type: .disconnectedPeer,
                    description: "Peer disconnected",
                    isForceClose: false
                )
            case .OutdatedChannelManager:
                return ChannelClosureInfo(
                    type: .outdatedChannelManager,
                    description: "Channel manager outdated",
                    isForceClose: false
                )
            case .CounterpartyCoopClosedUnfundedChannel:
                return ChannelClosureInfo(
                    type: .coopClosedUnfunded,
                    description: "Counterparty cooperatively closed unfunded channel",
                    isForceClose: false
                )
            case .FundingBatchClosure:
                return ChannelClosureInfo(
                    type: .fundingBatchClosure,
                    description: "Closed due to funding batch failure",
                    isForceClose: false
                )
            case .HTLCsTimedOut:
                return ChannelClosureInfo(
                    type: .htlcsTimedOut,
                    description: "Channel closed due to HTLC timeout",
                    isForceClose: false
                )
            default:
                return ChannelClosureInfo(
                    type: .unknown,
                    description: "Unknown closure reason",
                    isForceClose: false
                )
            }
        }
        
        private func evaluateChannelRequest(
            counterpartyNodeId: String,
            fundingSatoshis: UInt64,
            pushMsat: UInt64,
            channelType: Bindings.ChannelTypeFeatures
        ) -> Bool {
            // Check minimum channel size
            guard fundingSatoshis >= 20000 else { return false }
            
            // Check if we know this peer
            guard PeerStore.shared.isTrustedPeer(nodeId: counterpartyNodeId) else { return false }
            
            // Check channel type compatibility
            if channelType.requiresAnchorsZeroFeeHtlcTx() && !supportsAnchors() {
                return false
            }
            
            // Check total channel count
            let currentChannels = channelManager.listChannels().count
            guard currentChannels < maxChannelCount() else { return false }
            
            return true
        }
        
        private func generateUserChannelId() -> [UInt8] {
            var userChannelId = [UInt8](repeating: 0, count: 16)
            arc4random_buf(&userChannelId, 16)
            return userChannelId
        }
        
        private func updateRoutingHints() {
            // Update routing hints for invoice generation
            RoutingHintManager.shared.updateFromChannels(channelManager.listUsableChannels())
        }
    }
    
    struct ChannelClosureInfo {
        enum ClosureType {
            case counterpartyForceClosed
            case holderForceClosed
            case cooperativeClosure
            case commitmentTxConfirmed
            case fundingTimeout
            case processingError
            case disconnectedPeer
            case outdatedChannelManager
            case coopClosedUnfunded
            case fundingBatchClosure
            case htlcsTimedOut
            case unknown
        }
        
        let type: ClosureType
        let description: String
        let isForceClose: Bool
    }
    
    // Channel notification names
    extension Notification.Name {
        static let channelPendingFunding = Notification.Name("ldk.channel.pendingFunding")
        static let channelFundingFailed = Notification.Name("ldk.channel.fundingFailed")
        static let channelReady = Notification.Name("ldk.channel.ready")
        static let channelClosed = Notification.Name("ldk.channel.closed")
    }`.trim(),
    
          funding_events: `
    // Funding and Transaction Events
    import LightningDevKit
    import BitcoinDevKit
    
    extension LDKEventHandler {
        // Discard Funding - Funding transaction can be discarded
        func handleDiscardFunding(_ event: Bindings.Event.DiscardFunding) {
            let channelId = event.getChannelId()
            let fundingTx = event.getTransaction()
            
            logger.log(message: "Discarding funding transaction for channel: \\(channelId.data.hexString)")
            
            // Remove from our transaction store
            TransactionStore.shared.removePendingTransaction(
                txid: BitcoinDevKit.Transaction(transactionBytes: fundingTx).txid()
            )
            
            // Update channel status
            ChannelStore.shared.removeChannel(channelId: channelId)
            
            // Notify UI
            DispatchQueue.main.async {
                NotificationCenter.default.post(
                    name: .fundingDiscarded,
                    object: nil,
                    userInfo: ["channelId": channelId.data.hexString]
                )
            }
        }
        
        // Bump Transaction - Need to bump fee for transaction
        func handleBumpTransaction(_ event: Bindings.Event.BumpTransaction) {
            switch event.getValueType() {
            case .ChannelClose:
                handleBumpChannelClose(event.getValueAsChannelClose()!)
            case .HTLCResolution:
                handleBumpHTLCResolution(event.getValueAsHTLCResolution()!)
            default:
                logger.log(message: "Unknown bump transaction type")
            }
        }
        
        private func handleBumpChannelClose(_ bumpEvent: Bindings.Event.BumpTransaction.ChannelClose) {
            let channelId = bumpEvent.getChannelId()
            let counterpartyNodeId = bumpEvent.getCounterpartyNodeId()
            let claimId = bumpEvent.getClaimId()
            let packageTargetFeerateSatPer1000Weight = bumpEvent.getPackageTargetFeerateSatPer1000Weight()
            let commitmentTx = bumpEvent.getCommitmentTx()
            let commitmentTxFeeSatoshis = bumpEvent.getCommitmentTxFeeSatoshis()
            let anchorDescriptor = bumpEvent.getAnchorDescriptor()
            let pendingHtlcs = bumpEvent.getPendingHtlcs()
            
            Task {
                do {
                    // Build anchor spending transaction
                    let anchorTx = try await buildAnchorSpendingTransaction(
                        anchorDescriptor: anchorDescriptor,
                        targetFeeRate: packageTargetFeerateSatPer1000Weight,
                        commitmentTx: commitmentTx,
                        commitmentTxFee: commitmentTxFeeSatoshis
                    )
                    
                    // Broadcast the transaction
                    broadcaster.broadcastTransactions(txs: [anchorTx])
                    
                    // Handle any pending HTLCs
                    for htlc in pendingHtlcs {
                        handlePendingHTLC(htlc)
                    }
                    
                    logger.log(message: "Bumped channel close fee for channel: \\(channelId.data.hexString)")
                    
                } catch {
                    logger.log(message: "Failed to bump channel close fee: \\(error)")
                }
            }
        }
        
        private func handleBumpHTLCResolution(_ bumpEvent: Bindings.Event.BumpTransaction.HTLCResolution) {
            let channelId = bumpEvent.getChannelId()
            let counterpartyNodeId = bumpEvent.getCounterpartyNodeId()
            let claimId = bumpEvent.getClaimId()
            let targetFeerateSatPer1000Weight = bumpEvent.getTargetFeerateSatPer1000Weight()
            let htlcDescriptors = bumpEvent.getHtlcDescriptors()
            let txLockTime = bumpEvent.getTxLockTime()
            
            Task {
                do {
                    // Build HTLC claim transaction
                    let htlcTx = try await buildHTLCTransaction(
                        htlcDescriptors: htlcDescriptors,
                        targetFeeRate: targetFeerateSatPer1000Weight,
                        lockTime: txLockTime
                    )
                    
                    // Broadcast the transaction
                    broadcaster.broadcastTransactions(txs: [htlcTx])
                    
                    logger.log(message: "Bumped HTLC resolution fee for channel: \\(channelId.data.hexString)")
                    
                } catch {
                    logger.log(message: "Failed to bump HTLC resolution fee: \\(error)")
                }
            }
        }
        
        // Build anchor spending transaction
        private func buildAnchorSpendingTransaction(
            anchorDescriptor: Bindings.AnchorDescriptor,
            targetFeeRate: UInt32,
            commitmentTx: [UInt8],
            commitmentTxFee: UInt64
        ) async throws -> [UInt8] {
            // Get anchor output
            let anchorOutpoint = anchorDescriptor.getOutpoint()
            let anchorOutput = anchorDescriptor.getPreviousUtxo()
            
            // Calculate required fee
            let anchorSpendWeight: UInt64 = 321 // Approximate weight for anchor spend
            let requiredFee = (anchorSpendWeight * UInt64(targetFeeRate)) / 1000
            
            // Build transaction with BDK
            let txBuilder = BitcoinDevKit.TxBuilder()
            
            // Add anchor input
            txBuilder.addUtxo(outpoint: anchorOutpoint, satisfaction: nil)
            
            // Add wallet inputs if needed for fees
            if requiredFee > anchorOutput.getValue() {
                let additionalFee = requiredFee - anchorOutput.getValue()
                txBuilder.fundTransaction(wallet: wallet, requiredAmount: additionalFee)
            }
            
            // Set fee rate
            txBuilder.feeRate(satPerVbyte: Float(targetFeeRate / 250)) // Convert to sat/vbyte
            
            // Set change address
            let changeAddress = try wallet.getAddress(addressIndex: .new)
            txBuilder.drainTo(address: changeAddress.address.asString())
            
            // Finish and sign
            let psbt = try txBuilder.finish(wallet: wallet)
            let signed = try wallet.sign(psbt: psbt, signOptions: nil)
            
            return signed.psbt.extractTx().serialize()
        }
        
        // Build HTLC transaction
        private func buildHTLCTransaction(
            htlcDescriptors: [Bindings.HTLCDescriptor],
            targetFeeRate: UInt32,
            lockTime: UInt32
        ) async throws -> [UInt8] {
            var inputs: [Bindings.TxIn] = []
            var outputs: [Bindings.TxOut] = []
            var totalValue: UInt64 = 0
            
            // Add HTLC inputs
            for descriptor in htlcDescriptors {
                let input = Bindings.TxIn(
                    previousOutput: descriptor.getOutpoint(),
                    scriptSig: Bindings.CVecU8Z(),
                    sequence: descriptor.getPerCommitmentNumber(),
                    witness: Bindings.Witness(elements: [])
                )
                inputs.append(input)
                
                totalValue += descriptor.getHtlc().getAmountMsat() / 1000
            }
            
            // Calculate fee
            let txWeight = estimateHTLCTxWeight(htlcCount: htlcDescriptors.count)
            let fee = (txWeight * UInt64(targetFeeRate)) / 1000
            
            // Create output to our wallet
            let outputValue = totalValue.saturatingSubtraction(fee)
            if outputValue > 546 { // Dust limit
                let address = try wallet.getAddress(addressIndex: .new)
                let script = try address.address.scriptPubkey()
                
                let output = Bindings.TxOut(
                    value: outputValue,
                    scriptPubkey: script.toBytes()
                )
                outputs.append(output)
            }
            
            // Build transaction
            let htlcTx = Bindings.Transaction(
                version: 2,
                lockTime: lockTime,
                input: inputs,
                output: outputs
            )
            
            // Sign inputs
            for (index, descriptor) in htlcDescriptors.enumerated() {
                let signature = keysManager.asSignerProvider().signCounterpartyHtlcTransaction(
                    htlcTx: htlcTx.write(),
                    inputIdx: UInt(index),
                    amount: descriptor.getHtlc().getAmountMsat() / 1000,
                    perCommitmentPoint: descriptor.getPerCommitmentPoint(),
                    htlc: descriptor.getHtlc()
                )
                
                // Add witness
                htlcTx.input[index].witness = createHTLCWitness(
                    signature: signature,
                    htlcDescriptor: descriptor
                )
            }
            
            return htlcTx.write()
        }
        
        private func estimateHTLCTxWeight(htlcCount: Int) -> UInt64 {
            // Base transaction weight + per-HTLC input weight
            let baseTxWeight: UInt64 = 42 * 4 // Version, locktime, counts
            let perInputWeight: UInt64 = 413 // Approximate HTLC input weight
            let outputWeight: UInt64 = 31 * 4 // P2WPKH output
            
            return baseTxWeight + (UInt64(htlcCount) * perInputWeight) + outputWeight
        }
    }
    
    // Transaction notification names
    extension Notification.Name {
        static let fundingDiscarded = Notification.Name("ldk.funding.discarded")
        static let transactionBumped = Notification.Name("ldk.transaction.bumped")
    }`.trim(),
    
          spendable_outputs: `
    // Spendable Outputs Event Handling
    import LightningDevKit
    import BitcoinDevKit
    
    extension LDKEventHandler {
        // Handle spendable outputs that can be swept
        func handleSpendableOutputs(_ event: Bindings.Event.SpendableOutputs) {
            let outputs = event.getOutputs()
            let channelId = event.getChannelId()
            
            Task {
                do {
                    // Group outputs by type for efficient handling
                    let groupedOutputs = groupOutputsByType(outputs)
                    
                    // Process each group
                    for (outputType, descriptors) in groupedOutputs {
                        try await sweepOutputs(
                            descriptors: descriptors,
                            outputType: outputType,
                            channelId: channelId
                        )
                    }
                    
                    logger.log(message: "Successfully swept \\(outputs.count) spendable outputs")
                    
                } catch {
                    logger.log(message: "Failed to sweep outputs: \\(error)")
                    
                    // Store for retry
                    SpendableOutputStore.shared.storePendingOutputs(
                        outputs: outputs,
                        channelId: channelId
                    )
                }
            }
        }
        
        // Sweep outputs to wallet
        private func sweepOutputs(
            descriptors: [Bindings.SpendableOutputDescriptor],
            outputType: OutputType,
            channelId: Bindings.ChannelId?
        ) async throws {
            // Get fee rate based on output type
            let feeRate = await getFeeRateForOutputType(outputType)
            
            // Get destination address
            let destinationAddress = try wallet.getAddress(addressIndex: .new)
            let destinationScript = try destinationAddress.address.scriptPubkey().toBytes()
            
            // Build spending transaction
            let spendResult = keysManager.spendSpendableOutputs(
                descriptors: descriptors,
                outputs: [], // No additional outputs
                changeDestinationScript: destinationScript,
                feerateSatPer1000Weight: feeRate,
                locktime: nil
            )
            
            guard spendResult.isOk(), let transaction = spendResult.getValue() else {
                throw OutputSweepError.failedToBuildTransaction(
                    spendResult.getError()?.getDescription() ?? "Unknown error"
                )
            }
            
            // Broadcast transaction
            broadcaster.broadcastTransactions(txs: [transaction])
            
            // Store sweep transaction
            let txid = BitcoinDevKit.Transaction(transactionBytes: transaction).txid()
            SweepTransactionStore.shared.store(
                txid: txid,
                outputs: descriptors,
                channelId: channelId,
                outputType: outputType,
                timestamp: Date()
            )
            
            // Update balance after sweep
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                BalanceManager.shared.updateBalance()
            }
            
            // Notify UI
            DispatchQueue.main.async {
                NotificationCenter.default.post(
                    name: .outputsSwept,
                    object: nil,
                    userInfo: [
                        "txid": txid,
                        "outputCount": descriptors.count,
                        "outputType": outputType.rawValue
                    ]
                )
            }
        }
        
        // Group outputs by type for batching
        private func groupOutputsByType(_ outputs: [Bindings.SpendableOutputDescriptor]) -> [OutputType: [Bindings.SpendableOutputDescriptor]] {
            var grouped: [OutputType: [Bindings.SpendableOutputDescriptor]] = [:]
            
            for output in outputs {
                let outputType = getOutputType(output)
                if grouped[outputType] == nil {
                    grouped[outputType] = []
                }
                grouped[outputType]?.append(output)
            }
            
            return grouped
        }
        
        // Determine output type
        private func getOutputType(_ output: Bindings.SpendableOutputDescriptor) -> OutputType {
            switch output.getValueType() {
            case .StaticPaymentOutput:
                return .staticPayment
            case .DelayedPaymentOutput:
                return .delayedPayment
            case .StaticOutput:
                return .staticOutput
            default:
                return .unknown
            }
        }
        
        // Get appropriate fee rate for output type
        private func getFeeRateForOutputType(_ outputType: OutputType) async -> UInt32 {
            let feeEstimator = LDKManager.shared.feeEstimator
            
            switch outputType {
            case .staticPayment:
                // Higher priority for static payment outputs
                return feeEstimator.getEstSatPer1000Weight(
                    confirmationTarget: .onChainSweep
                )
            case .delayedPayment:
                // Medium priority for delayed payments
                return feeEstimator.getEstSatPer1000Weight(
                    confirmationTarget: .nonAnchorChannelFee
                )
            case .staticOutput:
                // Lower priority for static outputs
                return feeEstimator.getEstSatPer1000Weight(
                    confirmationTarget: .channelCloseMinimum
                )
            case .unknown:
                // Default fee rate
                return feeEstimator.getEstSatPer1000Weight(
                    confirmationTarget: .outputSpendingFee
                )
            }
        }
    }
    
    // Output type enumeration
    enum OutputType: String {
        case staticPayment = "static_payment"
        case delayedPayment = "delayed_payment"
        case staticOutput = "static_output"
        case unknown = "unknown"
    }
    
    // Output sweep error
    enum OutputSweepError: LocalizedError {
        case failedToBuildTransaction(String)
        case broadcastFailed(String)
        case insufficientFunds
        
        var errorDescription: String? {
            switch self {
            case .failedToBuildTransaction(let reason):
                return "Failed to build sweep transaction: \\(reason)"
            case .broadcastFailed(let reason):
                return "Failed to broadcast sweep transaction: \\(reason)"
            case .insufficientFunds:
                return "Insufficient funds to sweep outputs"
            }
        }
    }
    
    // Spendable Output Store for persistence
    class SpendableOutputStore {
        static let shared = SpendableOutputStore()
        
        private let queue = DispatchQueue(label: "spendable.output.store", attributes: .concurrent)
        private var pendingOutputs: [PendingOutput] = []
        
        struct PendingOutput: Codable {
            let id: String
            let outputData: Data
            let channelId: String?
            let addedAt: Date
            var retryCount: Int
        }
        
        func storePendingOutputs(outputs: [Bindings.SpendableOutputDescriptor], channelId: Bindings.ChannelId?) {
            queue.async(flags: .barrier) {
                for output in outputs {
                    let pending = PendingOutput(
                        id: UUID().uuidString,
                        outputData: Data(output.write()),
                        channelId: channelId?.data.hexString,
                        addedAt: Date(),
                        retryCount: 0
                    )
                    self.pendingOutputs.append(pending)
                }
                self.persist()
            }
        }
        
        func getPendingOutputs() -> [PendingOutput] {
            queue.sync { pendingOutputs }
        }
        
        func removePendingOutput(id: String) {
            queue.async(flags: .barrier) {
                self.pendingOutputs.removeAll { $0.id == id }
                self.persist()
            }
        }
        
        private func persist() {
            // Persist to UserDefaults or file
            if let encoded = try? JSONEncoder().encode(pendingOutputs) {
                UserDefaults.standard.set(encoded, forKey: "pending_spendable_outputs")
            }
        }
    }
    
    // Retry sweeping pending outputs
    extension LDKEventHandler {
        func retryPendingOutputSweeps() {
            let pendingOutputs = SpendableOutputStore.shared.getPendingOutputs()
            
            for pending in pendingOutputs where pending.retryCount < 3 {
                // Deserialize output
                if let outputResult = Bindings.SpendableOutputDescriptor.read(
                    ser: [UInt8](pending.outputData),
                    arg: keysManager
                ), outputResult.isOk(), let output = outputResult.getValue() {
                    
                    Task {
                        do {
                            try await sweepOutputs(
                                descriptors: [output],
                                outputType: getOutputType(output),
                                channelId: nil
                            )
                            
                            // Remove from pending if successful
                            SpendableOutputStore.shared.removePendingOutput(id: pending.id)
                            
                        } catch {
                            logger.log(message: "Retry sweep failed: \\(error)")
                            // Increment retry count
                            pending.retryCount += 1
                        }
                    }
                }
            }
        }
    }
    
    // Notification name for swept outputs
    extension Notification.Name {
        static let outputsSwept = Notification.Name("ldk.outputs.swept")
    }`.trim(),
    
          forwarding_events: `
    // Payment Forwarding Event Handling
    import LightningDevKit
    
    extension LDKEventHandler {
        // Payment Forwarded - Successfully forwarded a payment
        func handlePaymentForwarded(_ event: Bindings.Event.PaymentForwarded) {
            let prevChannelId = event.getPrevChannelId()
            let nextChannelId = event.getNextChannelId()
            let prevUserChannelId = event.getPrevUserChannelId()
            let nextUserChannelId = event.getNextUserChannelId()
            let totalFeeMsat = event.getTotalFeeMsat()
            let skimmedFeeMsat = event.getSkimmedFeeMsat()
            let claimFromOnchain = event.getClaimFromOnchainTx()
            let outboundAmountMsat = event.getOutboundAmountForwardedMsat()
            
            // Calculate forwarding fee earned
            let feeEarnedMsat = totalFeeMsat?.getValue() ?? 0
            
            // Store forwarding record
            ForwardingStore.shared.recordForwarding(
                prevChannelId: prevChannelId?.data.hexString,
                nextChannelId: nextChannelId?.data.hexString,
                feeEarnedMsat: feeEarnedMsat,
                amountMsat: outboundAmountMsat?.getValue() ?? 0,
                timestamp: Date()
            )
            
            // Update routing statistics
            RoutingStats.shared.recordSuccessfulForward(
                fromChannel: prevChannelId,
                toChannel: nextChannelId,
                feeMsat: feeEarnedMsat
            )
            
            // Notify UI
            DispatchQueue.main.async {
                NotificationCenter.default.post(
                    name: .paymentForwarded,
                    object: nil,
                    userInfo: [
                        "feeEarnedMsat": feeEarnedMsat,
                        "prevChannelId": prevChannelId?.data.hexString ?? "unknown",
                        "nextChannelId": nextChannelId?.data.hexString ?? "unknown"
                    ]
                )
            }
            
            logger.log(message: "Payment forwarded. Fee earned: \\(feeEarnedMsat) msat")
        }
        
        // Pending HTLCs Forwardable - Need to process pending forwards
        func handlePendingHTLCsForwardable(_ event: Bindings.Event.PendingHTLCsForwardable) {
            let timeForwardable = event.getTimeForwardableNanos()
            
            // Calculate delay in milliseconds
            let delayMs = timeForwardable / 1_000_000
            
            if delayMs > 0 {
                // Schedule forward processing
                DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(Int(delayMs))) {
                    self.channelManager.processPendingHtlcForwards()
                    self.logger.log(message: "Processed pending HTLC forwards after \\(delayMs)ms delay")
                }
            } else {
                // Process immediately
                channelManager.processPendingHtlcForwards()
                logger.log(message: "Processed pending HTLC forwards immediately")
            }
        }
        
        // HTLC Intercepted - Intercept HTLC for custom handling
        func handleHTLCIntercepted(_ event: Bindings.Event.HTLCIntercepted) {
            let interceptId = event.getInterceptId()
            let requestedNextHopScid = event.getRequestedNextHopScid()
            let paymentHash = event.getPaymentHash()
            let inboundAmount = event.getInboundAmountMsat()
            let outboundAmount = event.getExpectedOutboundAmountMsat()
            
            // Evaluate HTLC for acceptance
            let evaluation = evaluateHTLC(
                paymentHash: paymentHash,
                inboundAmount: inboundAmount,
                outboundAmount: outboundAmount,
                nextHopScid: requestedNextHopScid
            )
            
            switch evaluation {
            case .accept:
                // Forward the HTLC normally
                let result = channelManager.forwardInterceptedHtlc(
                    interceptId: interceptId,
                    nextHopChannelId: getChannelId(fromScid: requestedNextHopScid),
                    nextHopNodeId: getNodeId(fromScid: requestedNextHopScid),
                    amtToForwardMsat: outboundAmount
                )
                
                if result.isOk() {
                    logger.log(message: "Forwarded intercepted HTLC")
                } else {
                    logger.log(message: "Failed to forward intercepted HTLC")
                }
                
            case .reject(let reason):
                // Fail the HTLC
                channelManager.failInterceptedHtlc(interceptId: interceptId)
                logger.log(message: "Rejected intercepted HTLC: \\(reason)")
                
            case .customHandle(let customData):
                // Handle with custom logic (e.g., hold invoice)
                handleCustomHTLC(
                    interceptId: interceptId,
                    paymentHash: paymentHash,
                    customData: customData
                )
            }
        }
        
        // HTLC Handling Failed
        func handleHTLCHandlingFailed(_ event: Bindings.Event.HTLCHandlingFailed) {
            let prevChannelId = event.getPrevChannelId()
            let failedNextDestination = event.getFailedNextDestination()
            
            // Update routing statistics
            RoutingStats.shared.recordFailedForward(
                fromChannel: prevChannelId,
                toDestination: failedNextDestination
            )
            
            logger.log(message: "HTLC handling failed from channel: \\(prevChannelId.data.hexString)")
        }
    }
    
    // HTLC evaluation logic
    extension LDKEventHandler {
        enum HTLCEvaluation {
            case accept
            case reject(String)
            case customHandle(Data)
        }
        
        private func evaluateHTLC(
            paymentHash: [UInt8],
            inboundAmount: UInt64,
            outboundAmount: UInt64,
            nextHopScid: UInt64
        ) -> HTLCEvaluation {
            // Check if this is a hold invoice
            if let holdInvoice = HoldInvoiceStore.shared.getHoldInvoice(
                paymentHash: Data(paymentHash).hexString
            ) {
                return .customHandle(holdInvoice.metadata)
            }
            
            // Check channel capacity
            if let channel = getChannel(fromScid: nextHopScid) {
                if channel.getOutboundCapacityMsat() < outboundAmount {
                    return .reject("Insufficient outbound capacity")
                }
            }
            
            // Check fee policy
            let impliedFee = inboundAmount.saturatingSubtraction(outboundAmount)
            if impliedFee < minimumForwardingFee() {
                return .reject("Fee too low")
            }
            
            // Check rate limiting
            if !RateLimiter.shared.allowForward(
                amount: outboundAmount,
                channelScid: nextHopScid
            ) {
                return .reject("Rate limit exceeded")
            }
            
            return .accept
        }
        
        private func handleCustomHTLC(
            interceptId: Bindings.InterceptId,
            paymentHash: [UInt8],
            customData: Data
        ) {
            // Example: Hold invoice handling
            Task {
                // Wait for external approval
                let approved = await waitForApproval(
                    paymentHash: Data(paymentHash).hexString,
                    timeout: 30
                )
                
                if approved {
                    // Forward the HTLC
                    let result = channelManager.forwardInterceptedHtlc(
                        interceptId: interceptId,
                        nextHopChannelId: nil, // Use default
                        nextHopNodeId: nil, // Use default
                        amtToForwardMsat: 0 // Use default
                    )
                    
                    if result.isOk() {
                        logger.log(message: "Hold invoice approved and forwarded")
                    }
                } else {
                    // Fail the HTLC
                    channelManager.failInterceptedHtlc(interceptId: interceptId)
                    logger.log(message: "Hold invoice rejected")
                }
            }
        }
    }
    
    // Forwarding Statistics
    class RoutingStats {
        static let shared = RoutingStats()
        
        private var successfulForwards: [ForwardRecord] = []
        private var failedForwards: [FailedForwardRecord] = []
        private let queue = DispatchQueue(label: "routing.stats", attributes: .concurrent)
        
        struct ForwardRecord {
            let fromChannel: Bindings.ChannelId?
            let toChannel: Bindings.ChannelId?
            let feeMsat: UInt64
            let timestamp: Date
        }
        
        struct FailedForwardRecord {
            let fromChannel: Bindings.ChannelId
            let toDestination: Bindings.HTLCDestination
            let timestamp: Date
        }
        
        func recordSuccessfulForward(
            fromChannel: Bindings.ChannelId?,
            toChannel: Bindings.ChannelId?,
            feeMsat: UInt64
        ) {
            queue.async(flags: .barrier) {
                let record = ForwardRecord(
                    fromChannel: fromChannel,
                    toChannel: toChannel,
                    feeMsat: feeMsat,
                    timestamp: Date()
                )
                self.successfulForwards.append(record)
                
                // Keep only last 1000 records
                if self.successfulForwards.count > 1000 {
                    self.successfulForwards.removeFirst()
                }
            }
        }
        
        func recordFailedForward(
            fromChannel: Bindings.ChannelId,
            toDestination: Bindings.HTLCDestination
        ) {
            queue.async(flags: .barrier) {
                let record = FailedForwardRecord(
                    fromChannel: fromChannel,
                    toDestination: toDestination,
                    timestamp: Date()
                )
                self.failedForwards.append(record)
                
                // Keep only last 1000 records
                if self.failedForwards.count > 1000 {
                    self.failedForwards.removeFirst()
                }
            }
        }
        
        func getStatistics(since: Date) -> (
            totalForwards: Int,
            successfulForwards: Int,
            failedForwards: Int,
            totalFeesEarned: UInt64,
            averageFee: UInt64
        ) {
            queue.sync {
                let recentSuccessful = successfulForwards.filter { $0.timestamp >= since }
                let recentFailed = failedForwards.filter { $0.timestamp >= since }
                
                let totalFees = recentSuccessful.reduce(0) { $0 + $1.feeMsat }
                let avgFee = recentSuccessful.isEmpty ? 0 : totalFees / UInt64(recentSuccessful.count)
                
                return (
                    totalForwards: recentSuccessful.count + recentFailed.count,
                    successfulForwards: recentSuccessful.count,
                    failedForwards: recentFailed.count,
                    totalFeesEarned: totalFees,
                    averageFee: avgFee
                )
            }
        }
    }
    
    // Forwarding notification names
    extension Notification.Name {
        static let paymentForwarded = Notification.Name("ldk.payment.forwarded")
        static let htlcIntercepted = Notification.Name("ldk.htlc.intercepted")
    }`.trim(),
    
          event_persistence: `
    // Event Persistence and Recovery
    import LightningDevKit
    import Foundation
    
    class EventPersistence {
        static let shared = EventPersistence()
        
        private let queue = DispatchQueue(label: "event.persistence", attributes: .concurrent)
        private let documentsDirectory: URL
        private let eventsDirectory: URL
        private let maxStoredEvents = 1000
        
        init() {
            documentsDirectory = FileManager.default.urls(
                for: .documentDirectory,
                in: .userDomainMask
            ).first!
            
            eventsDirectory = documentsDirectory.appendingPathComponent("ldk_events")
            
            // Create directory if needed
            try? FileManager.default.createDirectory(
                at: eventsDirectory,
                withIntermediateDirectories: true
            )
        }
        
        // Store event for potential replay
        func store(event: Bindings.Event) {
            queue.async(flags: .barrier) {
                let eventId = UUID().uuidString
                let timestamp = Date()
                
                // Serialize event
                let eventData = event.write()
                
                // Create event record
                let record = StoredEvent(
                    id: eventId,
                    timestamp: timestamp,
                    eventType: event.getValueType().rawValue,
                    eventData: Data(eventData),
                    processed: false
                )
                
                // Save to disk
                self.saveEvent(record)
                
                // Clean up old events
                self.pruneOldEvents()
            }
        }
        
        // Mark event as processed
        func markProcessed(eventId: String) {
            queue.async(flags: .barrier) {
                let eventPath = self.eventsDirectory.appendingPathComponent("\\(eventId).json")
                
                if var record = self.loadEvent(from: eventPath) {
                    record.processed = true
                    self.saveEvent(record)
                }
            }
        }
        
        // Load unprocessed events for recovery
        func loadUnprocessedEvents() -> [StoredEvent] {
            queue.sync {
                var unprocessedEvents: [StoredEvent] = []
                
                do {
                    let files = try FileManager.default.contentsOfDirectory(
                        at: eventsDirectory,
                        includingPropertiesForKeys: [.creationDateKey]
                    )
                    
                    for file in files where file.pathExtension == "json" {
                        if let event = loadEvent(from: file), !event.processed {
                            unprocessedEvents.append(event)
                        }
                    }
                    
                    // Sort by timestamp
                    unprocessedEvents.sort { $0.timestamp < $1.timestamp }
                    
                } catch {
                    logger.log(message: "Failed to load events: \\(error)")
                }
                
                return unprocessedEvents
            }
        }
        
        // Replay unprocessed events during recovery
        func replayUnprocessedEvents(handler: LDKEventHandler) async {
            let unprocessedEvents = loadUnprocessedEvents()
            
            logger.log(message: "Found \\(unprocessedEvents.count) unprocessed events to replay")
            
            for storedEvent in unprocessedEvents {
                // Deserialize event
                if let event = deserializeEvent(storedEvent.eventData) {
                    // Check if event is still valid
                    if isEventStillValid(event, storedAt: storedEvent.timestamp) {
                        // Replay event
                        handler.handleEvent(event: event)
                        
                        // Mark as processed
                        markProcessed(eventId: storedEvent.id)
                        
                        logger.log(message: "Replayed event: \\(storedEvent.eventType)")
                    } else {
                        // Skip expired events
                        logger.log(message: "Skipped expired event: \\(storedEvent.eventType)")
                        markProcessed(eventId: storedEvent.id)
                    }
                }
            }
        }
        
        // Check if event is still valid for replay
        private func isEventStillValid(_ event: Bindings.Event, storedAt: Date) -> Bool {
            let age = Date().timeIntervalSince(storedAt)
            
            switch event.getValueType() {
            case .PaymentClaimable:
                // Check claim deadline
                if let claimable = event.getValueAsPaymentClaimable(),
                   let deadline = claimable.getClaimDeadline() {
                    return UInt64(Date().timeIntervalSince1970) < deadline
                }
                return age < 86400 // 24 hours
                
            case .PendingHTLCsForwardable:
                // Always process pending forwards
                return true
                
            case .SpendableOutputs:
                // Always process spendable outputs
                return true
                
            case .FundingGenerationReady:
                // Funding should be processed within reasonable time
                return age < 3600 // 1 hour
                
            default:
                // Default: events valid for 24 hours
                return age < 86400
            }
        }
        
        // Serialize/deserialize helpers
        private func deserializeEvent(_ data: Data) -> Bindings.Event? {
            let result = Bindings.Event.read(ser: [UInt8](data))
            return result.isOk() ? result.getValue() : nil
        }
        
        private func saveEvent(_ event: StoredEvent) {
            let eventPath = eventsDirectory.appendingPathComponent("\\(event.id).json")
            
            do {
                let encoded = try JSONEncoder().encode(event)
                try encoded.write(to: eventPath)
            } catch {
                logger.log(message: "Failed to save event: \\(error)")
            }
        }
        
        private func loadEvent(from url: URL) -> StoredEvent? {
            do {
                let data = try Data(contentsOf: url)
                return try JSONDecoder().decode(StoredEvent.self, from: data)
            } catch {
                return nil
            }
        }
        
        private func pruneOldEvents() {
            do {
                let files = try FileManager.default.contentsOfDirectory(
                    at: eventsDirectory,
                    includingPropertiesForKeys: [.creationDateKey]
                ).sorted { file1, file2 in
                    let date1 = try? file1.resourceValues(forKeys: [.creationDateKey]).creationDate
                    let date2 = try? file2.resourceValues(forKeys: [.creationDateKey]).creationDate
                    return (date1 ?? Date.distantPast) < (date2 ?? Date.distantPast)
                }
                
                // Remove old events if we exceed limit
                if files.count > maxStoredEvents {
                    let toRemove = files.prefix(files.count - maxStoredEvents)
                    for file in toRemove {
                        try FileManager.default.removeItem(at: file)
                    }
                }
                
                // Remove processed events older than 7 days
                let cutoffDate = Date().addingTimeInterval(-7 * 24 * 60 * 60)
                for file in files {
                    if let event = loadEvent(from: file),
                       event.processed && event.timestamp < cutoffDate {
                        try FileManager.default.removeItem(at: file)
                    }
                }
                
            } catch {
                logger.log(message: "Failed to prune events: \\(error)")
            }
        }
    }
    
    // Stored event structure
    struct StoredEvent: Codable {
        let id: String
        let timestamp: Date
        let eventType: Int
        let eventData: Data
        var processed: Bool
    }
    
    // Event recovery during startup
    extension LDKEventHandler {
        func performEventRecovery() async {
            logger.log(message: "Starting event recovery...")
            
            await EventPersistence.shared.replayUnprocessedEvents(handler: self)
            
            logger.log(message: "Event recovery completed")
        }
    }
    
    // Graceful shutdown
    extension LDKEventHandler {
        func prepareForShutdown() {
            // Process any pending events
            channelManager.processPendingHtlcForwards()
            
            // Wait for event processing to complete
            Thread.sleep(forTimeInterval: 0.5)
            
            logger.log(message: "Event handler prepared for shutdown")
        }
    }`.trim()
        };
    
        try {
          const code = eventExamples[args.eventType];
          if (!code) {
            throw new Error(`Unknown event type: ${args.eventType}`);
          }
    
          return {
            content: [{
              type: 'text',
              text: JSON.stringify({
                success: true,
                eventType: args.eventType,
                swiftCode: code,
                description: `LDK event handling implementation for ${args.eventType}`
              }, 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
          };
        }
      }
    };
  • Input schema defining the eventType parameter with specific enum values for different LDK event handling categories.
    inputSchema: {
      type: 'object',
      properties: {
        eventType: {
          type: 'string',
          enum: [
            'all_events',
            'payment_events',
            'channel_events',
            'funding_events',
            'spendable_outputs',
            'forwarding_events',
            'event_persistence'
          ],
          description: 'Type of event handling to get code for'
        }
      },
      required: ['eventType']
    },
  • src/index.ts:34-34 (registration)
    Import statement for the eventHandlingTool.
    import { eventHandlingTool } from './tools/eventHandling.js';

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