eventHandling.ts•65.6 kB
import { Tool, ToolResult } from '../types/tool.js';
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
};
}
}
};