Skip to main content
Glama
chainSync.ts55.5 kB
import { Tool, ToolResult } from '../types/tool.js'; export const chainSyncTool: Tool = { name: 'ldk_chain_sync', description: 'Get chain synchronization implementations for LDK with Electrum/Esplora', inputSchema: { type: 'object', properties: { syncMethod: { type: 'string', enum: [ 'electrum_sync', 'esplora_sync', 'bitcoin_core_rpc', 'compact_filters', 'sync_management', 'reorg_handling' ], description: 'Chain synchronization method to get implementation for' } }, required: ['syncMethod'] }, execute: async (args: any): Promise<ToolResult> => { const syncExamples: Record<string, string> = { electrum_sync: ` // Electrum-based chain synchronization for LDK import LightningDevKit import Foundation class ElectrumChainSync { private let channelManager: Bindings.ChannelManager private let chainMonitor: Bindings.ChainMonitor private let logger: Bindings.Logger private var electrumClient: ElectrumClient! private var lastSyncHeight: UInt32 = 0 init( channelManager: Bindings.ChannelManager, chainMonitor: Bindings.ChainMonitor, logger: Bindings.Logger ) { self.channelManager = channelManager self.chainMonitor = chainMonitor self.logger = logger } // Connect to Electrum server func connect(to server: String, port: UInt16 = 50001, useTLS: Bool = true) async throws { electrumClient = ElectrumClient( server: server, port: port, useTLS: useTLS, logger: logger ) try await electrumClient.connect() logger.log(message: "Connected to Electrum server: \\(server):\\(port)") } // Perform full synchronization func performSync() async throws { // Get current chain tip let chainTip = try await electrumClient.getChainTip() let currentHeight = chainTip.height let tipHash = chainTip.hash logger.log(message: "Syncing to block \\(currentHeight)") // Get transactions to monitor let relevantTxs = getRelevantTransactions() // Check for reorgs if lastSyncHeight > 0 { try await checkForReorgs(from: lastSyncHeight, to: currentHeight) } // Sync confirmed transactions for txid in relevantTxs { if let txInfo = try await electrumClient.getTransaction(txid: txid) { if txInfo.confirmations > 0 { try await confirmTransaction( txid: txid, blockHeight: txInfo.blockHeight, blockHash: txInfo.blockHash, txData: txInfo.rawTx ) } } } // Update to chain tip channelManager.asConfirm().bestBlockUpdated( header: tipHash, height: currentHeight ) chainMonitor.asConfirm().bestBlockUpdated( header: tipHash, height: currentHeight ) lastSyncHeight = currentHeight // Persist sync state UserDefaults.standard.set(currentHeight, forKey: "lastSyncHeight") } // Get relevant transactions from LDK private func getRelevantTransactions() -> Set<[UInt8]> { var txids = Set<[UInt8]>() // From ChannelManager let channelTxs = channelManager.asConfirm().getRelevantTxids() for (txid, _) in channelTxs { txids.insert(txid) } // From ChainMonitor let monitorTxs = chainMonitor.asConfirm().getRelevantTxids() for (txid, _) in monitorTxs { txids.insert(txid) } return txids } // Check for blockchain reorganizations private func checkForReorgs(from startHeight: UInt32, to endHeight: UInt32) async throws { logger.log(message: "Checking for reorgs from \\(startHeight) to \\(endHeight)") // Get block headers to verify chain for height in startHeight...min(endHeight, startHeight + 100) { let header = try await electrumClient.getBlockHeader(height: height) // Check if this block is still in the main chain let currentHeader = try await electrumClient.getBlockHeaderAt( height: height, checkMainChain: true ) if header.hash != currentHeader.hash { // Reorg detected! logger.log(message: "Reorg detected at height \\(height)") try await handleReorg(fromHeight: height) break } } } // Handle blockchain reorganization private func handleReorg(fromHeight: UInt32) async throws { // Get all transactions that need to be re-checked let relevantTxs = getRelevantTransactions() for txid in relevantTxs { // Check if transaction is still confirmed if let txInfo = try await electrumClient.getTransaction(txid: txid) { if txInfo.confirmations == 0 || txInfo.blockHeight >= fromHeight { // Transaction was reorged out channelManager.asConfirm().transactionUnconfirmed(txid: txid) chainMonitor.asConfirm().transactionUnconfirmed(txid: txid) logger.log(message: "Transaction unconfirmed due to reorg: \\(Data(txid).hexString)") } } } // Re-sync from the reorg point lastSyncHeight = fromHeight - 1 try await performSync() } // Confirm a transaction private func confirmTransaction( txid: [UInt8], blockHeight: UInt32, blockHash: [UInt8], txData: [UInt8] ) async throws { // Get block header let header = try await electrumClient.getBlockHeader(height: blockHeight) // Get transaction position in block let txPos = try await electrumClient.getTransactionPosition( txid: txid, blockHeight: blockHeight ) // Notify LDK of confirmation let txdata = [(txPos, txData)] channelManager.asConfirm().transactionsConfirmed( header: header.serialize(), txdata: txdata, height: blockHeight ) chainMonitor.asConfirm().transactionsConfirmed( header: header.serialize(), txdata: txdata, height: blockHeight ) logger.log(message: "Transaction confirmed at height \\(blockHeight): \\(Data(txid).hexString)") } } // Electrum Client Implementation class ElectrumClient { private let server: String private let port: UInt16 private let useTLS: Bool private let logger: Bindings.Logger private var connection: NWConnection? struct ChainTip { let height: UInt32 let hash: [UInt8] } struct TransactionInfo { let txid: [UInt8] let rawTx: [UInt8] let confirmations: Int let blockHeight: UInt32 let blockHash: [UInt8] } init(server: String, port: UInt16, useTLS: Bool, logger: Bindings.Logger) { self.server = server self.port = port self.useTLS = useTLS self.logger = logger } func connect() async throws { // Implementation of Electrum protocol connection // This would involve TCP/TLS connection and JSON-RPC communication } func getChainTip() async throws -> ChainTip { // Call blockchain.headers.subscribe let response = try await sendRequest( method: "blockchain.headers.subscribe", params: [] ) // Parse response let height = response["height"] as? UInt32 ?? 0 let hashHex = response["hex"] as? String ?? "" return ChainTip( height: height, hash: Data(hexString: hashHex)?.bytes ?? [] ) } func getTransaction(txid: [UInt8]) async throws -> TransactionInfo? { // Call blockchain.transaction.get let txidHex = Data(txid).hexString do { let rawTxHex = try await sendRequest( method: "blockchain.transaction.get", params: [txidHex, true] ) as? String ?? "" // Get confirmation status let txInfo = try await sendRequest( method: "blockchain.transaction.get_merkle", params: [txidHex] ) let blockHeight = txInfo["block_height"] as? UInt32 ?? 0 let confirmations = blockHeight > 0 ? (try await getChainTip().height - blockHeight + 1) : 0 return TransactionInfo( txid: txid, rawTx: Data(hexString: rawTxHex)?.bytes ?? [], confirmations: Int(confirmations), blockHeight: blockHeight, blockHash: [] // Would need to fetch block hash separately ) } catch { return nil } } private func sendRequest(method: String, params: [Any]) async throws -> Any { // Implementation of Electrum JSON-RPC protocol // This is a simplified version - real implementation would handle // proper JSON-RPC formatting, response parsing, error handling, etc. fatalError("Implement Electrum protocol communication") } }`.trim(), esplora_sync: ` // Esplora-based chain synchronization for LDK import LightningDevKit import Foundation class EsploraChainSync { private let channelManager: Bindings.ChannelManager private let chainMonitor: Bindings.ChainMonitor private let logger: Bindings.Logger private let esploraClient: EsploraClient private var lastSyncTimestamp: Date = Date.distantPast init( channelManager: Bindings.ChannelManager, chainMonitor: Bindings.ChainMonitor, logger: Bindings.Logger, esploraUrl: String ) { self.channelManager = channelManager self.chainMonitor = chainMonitor self.logger = logger self.esploraClient = EsploraClient(baseUrl: esploraUrl) } // Perform synchronization func sync() async throws { logger.log(message: "Starting Esplora sync...") // Get current tip let tip = try await esploraClient.getTip() // Get all outputs to watch let outputsToWatch = getWatchedOutputs() // Check each output for output in outputsToWatch { try await syncOutput(output, currentTip: tip) } // Update best block let tipHash = try await esploraClient.getBlockHash(height: tip.height) channelManager.asConfirm().bestBlockUpdated( header: tipHash, height: tip.height ) chainMonitor.asConfirm().bestBlockUpdated( header: tipHash, height: tip.height ) lastSyncTimestamp = Date() logger.log(message: "Esplora sync completed at height \\(tip.height)") } // Get outputs we need to watch private func getWatchedOutputs() -> [WatchedOutput] { var outputs: [WatchedOutput] = [] // Get from Filter interface if let filter = LDKManager.shared.filter { outputs.append(contentsOf: filter.getWatchedOutputs()) } // Get relevant transactions let relevantTxs = channelManager.asConfirm().getRelevantTxids() for (txid, outputIndices) in relevantTxs { for index in outputIndices { outputs.append(WatchedOutput( txid: txid, index: index, script: nil )) } } return outputs } // Sync a specific output private func syncOutput(_ output: WatchedOutput, currentTip: BlockTip) async throws { // Get spending status let outpoint = "\\(Data(output.txid).hexString):\\(output.index)" let status = try await esploraClient.getOutputStatus(outpoint: outpoint) if let txid = status.txid { // Get transaction details let tx = try await esploraClient.getTransaction(txid: txid) if let confirmedHeight = status.status?.confirmedHeight { // Transaction is confirmed let header = try await esploraClient.getBlockHeader(height: confirmedHeight) let txPos = tx.status?.blockTxPosition ?? 0 channelManager.asConfirm().transactionsConfirmed( header: header, txdata: [(UInt(txPos), tx.raw)], height: confirmedHeight ) chainMonitor.asConfirm().transactionsConfirmed( header: header, txdata: [(UInt(txPos), tx.raw)], height: confirmedHeight ) logger.log(message: "Output confirmed at height \\(confirmedHeight): \\(outpoint)") } else if tx.status?.confirmed == false { // Transaction is unconfirmed channelManager.asConfirm().transactionUnconfirmed(txid: output.txid) chainMonitor.asConfirm().transactionUnconfirmed(txid: output.txid) logger.log(message: "Output unconfirmed: \\(outpoint)") } } // Check if output is spent if let spendingTxid = status.spendingTxid { let spendingTx = try await esploraClient.getTransaction(txid: spendingTxid) if let confirmedHeight = spendingTx.status?.confirmedHeight { // Spending transaction is confirmed let header = try await esploraClient.getBlockHeader(height: confirmedHeight) let txPos = spendingTx.status?.blockTxPosition ?? 0 channelManager.asConfirm().transactionsConfirmed( header: header, txdata: [(UInt(txPos), spendingTx.raw)], height: confirmedHeight ) chainMonitor.asConfirm().transactionsConfirmed( header: header, txdata: [(UInt(txPos), spendingTx.raw)], height: confirmedHeight ) logger.log(message: "Output spent at height \\(confirmedHeight): \\(outpoint)") } } } } // Esplora API Client class EsploraClient { private let baseUrl: String private let session: URLSession init(baseUrl: String) { self.baseUrl = baseUrl.trimmingCharacters(in: .init(charactersIn: "/")) let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 30 config.timeoutIntervalForResource = 60 self.session = URLSession(configuration: config) } struct BlockTip: Codable { let height: UInt32 let hash: String } struct Transaction: Codable { let txid: String let raw: [UInt8] let status: TransactionStatus? } struct TransactionStatus: Codable { let confirmed: Bool let confirmedHeight: UInt32? let blockHash: String? let blockTime: UInt64? let blockTxPosition: UInt? } struct OutputStatus: Codable { let txid: String? let status: TransactionStatus? let spendingTxid: String? } // Get current chain tip func getTip() async throws -> BlockTip { let url = URL(string: "\\(baseUrl)/blocks/tip/height")! let (data, _) = try await session.data(from: url) let height = try JSONDecoder().decode(UInt32.self, from: data) let hashUrl = URL(string: "\\(baseUrl)/blocks/tip/hash")! let (hashData, _) = try await session.data(from: hashUrl) let hash = String(data: hashData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return BlockTip(height: height, hash: hash) } // Get block hash at height func getBlockHash(height: UInt32) async throws -> [UInt8] { let url = URL(string: "\\(baseUrl)/block-height/\\(height)")! let (data, _) = try await session.data(from: url) let hashHex = String(data: data, encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return Data(hexString: hashHex)?.bytes ?? [] } // Get block header func getBlockHeader(height: UInt32) async throws -> [UInt8] { let hash = try await getBlockHash(height: height) let hashHex = Data(hash).hexString let url = URL(string: "\\(baseUrl)/block/\\(hashHex)/header")! let (data, _) = try await session.data(from: url) let headerHex = String(data: data, encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return Data(hexString: headerHex)?.bytes ?? [] } // Get transaction func getTransaction(txid: String) async throws -> Transaction { // Get raw transaction let rawUrl = URL(string: "\\(baseUrl)/tx/\\(txid)/raw")! let (rawData, _) = try await session.data(from: rawUrl) // Get transaction status let statusUrl = URL(string: "\\(baseUrl)/tx/\\(txid)/status")! let (statusData, _) = try await session.data(from: statusUrl) let status = try? JSONDecoder().decode(TransactionStatus.self, from: statusData) return Transaction( txid: txid, raw: [UInt8](rawData), status: status ) } // Get output status func getOutputStatus(outpoint: String) async throws -> OutputStatus { let url = URL(string: "\\(baseUrl)/tx/\\(outpoint)/outspend")! let (data, response) = try await session.data(from: url) if (response as? HTTPURLResponse)?.statusCode == 404 { // Output not found or not spent return OutputStatus(txid: nil, status: nil, spendingTxid: nil) } return try JSONDecoder().decode(OutputStatus.self, from: data) } } // Watched output structure struct WatchedOutput { let txid: [UInt8] let index: UInt32 let script: [UInt8]? }`.trim(), bitcoin_core_rpc: ` // Bitcoin Core RPC chain synchronization import LightningDevKit import Foundation class BitcoinCoreSync { private let channelManager: Bindings.ChannelManager private let chainMonitor: Bindings.ChainMonitor private let logger: Bindings.Logger private let rpcClient: BitcoinCoreRPC private var lastProcessedBlock: String? init( channelManager: Bindings.ChannelManager, chainMonitor: Bindings.ChainMonitor, logger: Bindings.Logger, rpcUrl: String, rpcUser: String, rpcPassword: String ) { self.channelManager = channelManager self.chainMonitor = chainMonitor self.logger = logger self.rpcClient = BitcoinCoreRPC( url: rpcUrl, user: rpcUser, password: rpcPassword ) } // Start block monitoring func startBlockMonitoring() async { // Load last processed block lastProcessedBlock = UserDefaults.standard.string(forKey: "lastProcessedBlock") // Start monitoring loop while true { do { try await checkForNewBlocks() try await Task.sleep(nanoseconds: 10_000_000_000) // 10 seconds } catch { logger.log(message: "Block monitoring error: \\(error)") try? await Task.sleep(nanoseconds: 30_000_000_000) // 30 seconds retry } } } // Check for new blocks private func checkForNewBlocks() async throws { let currentTip = try await rpcClient.getBestBlockHash() if currentTip != lastProcessedBlock { logger.log(message: "New block detected: \\(currentTip)") // Get block info let blockInfo = try await rpcClient.getBlock(hash: currentTip) // Process all blocks from last known to current var blocksToProcess: [BlockInfo] = [blockInfo] var prevHash = blockInfo.previousblockhash // Walk back to find common ancestor while let prev = prevHash, prev != lastProcessedBlock { if let prevBlock = try? await rpcClient.getBlock(hash: prev) { blocksToProcess.insert(prevBlock, at: 0) prevHash = prevBlock.previousblockhash } else { break } } // Process blocks in order for block in blocksToProcess { try await processBlock(block) } lastProcessedBlock = currentTip UserDefaults.standard.set(currentTip, forKey: "lastProcessedBlock") } } // Process a single block private func processBlock(_ block: BlockInfo) async throws { logger.log(message: "Processing block \\(block.height): \\(block.hash)") // Get block header let header = try await rpcClient.getBlockHeader(hash: block.hash) // Get relevant transactions let relevantTxids = getRelevantTransactionIds() var confirmedTxs: [(UInt, [UInt8])] = [] // Check each transaction in the block for (index, txid) in block.tx.enumerated() { let txidBytes = Data(hexString: txid)?.bytes ?? [] if relevantTxids.contains(txidBytes) { // Get raw transaction let rawTx = try await rpcClient.getRawTransaction(txid: txid) confirmedTxs.append((UInt(index), rawTx)) logger.log(message: "Found relevant transaction: \\(txid)") } } // Notify LDK of confirmations if !confirmedTxs.isEmpty { channelManager.asConfirm().transactionsConfirmed( header: header, txdata: confirmedTxs, height: block.height ) chainMonitor.asConfirm().transactionsConfirmed( header: header, txdata: confirmedTxs, height: block.height ) } // Update best block channelManager.asConfirm().bestBlockUpdated( header: header, height: block.height ) chainMonitor.asConfirm().bestBlockUpdated( header: header, height: block.height ) } // Get relevant transaction IDs private func getRelevantTransactionIds() -> Set<[UInt8]> { var txids = Set<[UInt8]>() // From ChannelManager let channelTxs = channelManager.asConfirm().getRelevantTxids() for (txid, _) in channelTxs { txids.insert(txid) } // From ChainMonitor let monitorTxs = chainMonitor.asConfirm().getRelevantTxids() for (txid, _) in monitorTxs { txids.insert(txid) } return txids } } // Bitcoin Core RPC Client class BitcoinCoreRPC { private let url: String private let user: String private let password: String private let session: URLSession init(url: String, user: String, password: String) { self.url = url self.user = user self.password = password let config = URLSessionConfiguration.default let authString = "\\(user):\\(password)" let authData = authString.data(using: .utf8)!.base64EncodedString() config.httpAdditionalHeaders = ["Authorization": "Basic \\(authData)"] self.session = URLSession(configuration: config) } struct BlockInfo: Codable { let hash: String let height: UInt32 let previousblockhash: String? let tx: [String] let time: UInt64 } // RPC call helper private func rpcCall<T: Decodable>(method: String, params: [Any] = []) async throws -> T { let requestBody: [String: Any] = [ "jsonrpc": "1.0", "id": UUID().uuidString, "method": method, "params": params ] var request = URLRequest(url: URL(string: url)!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: requestBody) let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw RPCError.requestFailed } let rpcResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any] if let error = rpcResponse?["error"] as? [String: Any] { throw RPCError.rpcError( code: error["code"] as? Int ?? -1, message: error["message"] as? String ?? "Unknown error" ) } guard let result = rpcResponse?["result"] else { throw RPCError.noResult } let resultData = try JSONSerialization.data(withJSONObject: result) return try JSONDecoder().decode(T.self, from: resultData) } // Get best block hash func getBestBlockHash() async throws -> String { return try await rpcCall(method: "getbestblockhash") } // Get block func getBlock(hash: String) async throws -> BlockInfo { return try await rpcCall(method: "getblock", params: [hash, 1]) } // Get block header func getBlockHeader(hash: String) async throws -> [UInt8] { let headerHex: String = try await rpcCall( method: "getblockheader", params: [hash, false] ) return Data(hexString: headerHex)?.bytes ?? [] } // Get raw transaction func getRawTransaction(txid: String) async throws -> [UInt8] { let rawHex: String = try await rpcCall( method: "getrawtransaction", params: [txid] ) return Data(hexString: rawHex)?.bytes ?? [] } } enum RPCError: LocalizedError { case requestFailed case noResult case rpcError(code: Int, message: String) var errorDescription: String? { switch self { case .requestFailed: return "RPC request failed" case .noResult: return "No result in RPC response" case .rpcError(let code, let message): return "RPC error \\(code): \\(message)" } } }`.trim(), compact_filters: ` // Compact block filters (BIP157/158) synchronization import LightningDevKit import Foundation class CompactFilterSync { private let channelManager: Bindings.ChannelManager private let chainMonitor: Bindings.ChainMonitor private let logger: Bindings.Logger private let filterClient: CompactFilterClient private var lastSyncHeight: UInt32 = 0 private var cachedFilters: [UInt32: CompactFilter] = [:] init( channelManager: Bindings.ChannelManager, chainMonitor: Bindings.ChainMonitor, logger: Bindings.Logger, peers: [String] ) { self.channelManager = channelManager self.chainMonitor = chainMonitor self.logger = logger self.filterClient = CompactFilterClient(peers: peers) } // Start synchronization func startSync() async throws { // Connect to peers try await filterClient.connectToPeers() // Get filter headers let filterHeaders = try await filterClient.downloadFilterHeaders( startHeight: lastSyncHeight ) // Download and process filters for (height, filterHeader) in filterHeaders { try await processBlock(at: height, filterHeader: filterHeader) } logger.log(message: "Compact filter sync completed") } // Process a block using compact filters private func processBlock(at height: UInt32, filterHeader: [UInt8]) async throws { // Get watched scripts let watchedScripts = getWatchedScripts() // Download filter for this block let filter = try await filterClient.downloadFilter( height: height, filterHeader: filterHeader ) // Check if any of our scripts match let matches = filter.match(scripts: watchedScripts) if !matches.isEmpty { logger.log(message: "Filter match at height \\(height), downloading block") // Download full block let block = try await filterClient.downloadBlock(height: height) // Process relevant transactions try await processBlockTransactions( block: block, height: height, matchingScripts: matches ) } // Update sync progress lastSyncHeight = height // Update best block let blockHash = try await filterClient.getBlockHash(height: height) channelManager.asConfirm().bestBlockUpdated( header: blockHash, height: height ) chainMonitor.asConfirm().bestBlockUpdated( header: blockHash, height: height ) } // Get scripts we're watching private func getWatchedScripts() -> Set<[UInt8]> { var scripts = Set<[UInt8]>() // Get from Filter interface if let filter = LDKManager.shared.filter { scripts.formUnion(filter.getWatchedScripts()) } // Add funding output scripts let channels = channelManager.listChannels() for channel in channels { if let fundingTxo = channel.getFundingTxo() { // Would need to derive script from funding output // This is simplified } } return scripts } // Process transactions in a block private func processBlockTransactions( block: Block, height: UInt32, matchingScripts: Set<[UInt8]> ) async throws { var confirmedTxs: [(UInt, [UInt8])] = [] let relevantTxids = getRelevantTransactionIds() for (index, tx) in block.transactions.enumerated() { let txid = tx.txid() // Check if this is a relevant transaction if relevantTxids.contains(txid) || transactionMatchesScripts(tx: tx, scripts: matchingScripts) { confirmedTxs.append((UInt(index), tx.serialize())) logger.log(message: "Found relevant transaction at height \\(height)") } } // Notify LDK if !confirmedTxs.isEmpty { channelManager.asConfirm().transactionsConfirmed( header: block.header, txdata: confirmedTxs, height: height ) chainMonitor.asConfirm().transactionsConfirmed( header: block.header, txdata: confirmedTxs, height: height ) } } private func transactionMatchesScripts( tx: Transaction, scripts: Set<[UInt8]> ) -> Bool { // Check if any output scripts match our watched scripts for output in tx.outputs { if scripts.contains(output.scriptPubkey) { return true } } return false } } // Compact Filter Client class CompactFilterClient { private let peers: [String] private var connections: [PeerConnection] = [] init(peers: [String]) { self.peers = peers } // Connect to peers func connectToPeers() async throws { for peer in peers { if let connection = try? await PeerConnection.connect(to: peer) { connections.append(connection) } } guard !connections.isEmpty else { throw FilterError.noPeersAvailable } } // Download filter headers func downloadFilterHeaders( startHeight: UInt32 ) async throws -> [(height: UInt32, header: [UInt8])] { guard let peer = connections.first else { throw FilterError.noPeersAvailable } return try await peer.getFilterHeaders(startHeight: startHeight) } // Download specific filter func downloadFilter( height: UInt32, filterHeader: [UInt8] ) async throws -> CompactFilter { guard let peer = connections.first else { throw FilterError.noPeersAvailable } let filterData = try await peer.getFilter(height: height) return CompactFilter(data: filterData, header: filterHeader) } // Download full block func downloadBlock(height: UInt32) async throws -> Block { guard let peer = connections.first else { throw FilterError.noPeersAvailable } return try await peer.getBlock(height: height) } // Get block hash func getBlockHash(height: UInt32) async throws -> [UInt8] { guard let peer = connections.first else { throw FilterError.noPeersAvailable } return try await peer.getBlockHash(height: height) } } // Compact Filter implementation struct CompactFilter { private let data: [UInt8] private let header: [UInt8] private let p: UInt8 = 19 // BIP158 P value private let m: UInt64 = 784931 // BIP158 M value init(data: [UInt8], header: [UInt8]) { self.data = data self.header = header } // Check if filter matches any of the provided scripts func match(scripts: Set<[UInt8]>) -> Set<[UInt8]> { var matches = Set<[UInt8]>() // Decode filter using Golomb-Rice coding // This is a simplified version - real implementation would need // proper Golomb-Rice decoding for script in scripts { if filterContains(element: script) { matches.insert(script) } } return matches } private func filterContains(element: [UInt8]) -> Bool { // Simplified - would need actual Golomb-Rice filter checking // using SipHash and proper decoding return false } } // Block structure struct Block { let header: [UInt8] let transactions: [Transaction] } struct Transaction { let version: Int32 let inputs: [TransactionInput] let outputs: [TransactionOutput] let lockTime: UInt32 func txid() -> [UInt8] { // Calculate transaction ID let serialized = serialize() return dsha256(serialized) } func serialize() -> [UInt8] { // Serialize transaction return [] } } struct TransactionInput { let previousOutput: OutPoint let scriptSig: [UInt8] let sequence: UInt32 } struct TransactionOutput { let value: Int64 let scriptPubkey: [UInt8] } struct OutPoint { let txid: [UInt8] let vout: UInt32 } // Peer connection for compact filters class PeerConnection { private let address: String private init(address: String) { self.address = address } static func connect(to address: String) async throws -> PeerConnection { // Implement P2P connection return PeerConnection(address: address) } func getFilterHeaders(startHeight: UInt32) async throws -> [(UInt32, [UInt8])] { // Implement getcfheaders message return [] } func getFilter(height: UInt32) async throws -> [UInt8] { // Implement getcfilters message return [] } func getBlock(height: UInt32) async throws -> Block { // Implement getdata message for block return Block(header: [], transactions: []) } func getBlockHash(height: UInt32) async throws -> [UInt8] { // Get block hash at height return [] } } enum FilterError: LocalizedError { case noPeersAvailable case downloadFailed case invalidFilter var errorDescription: String? { switch self { case .noPeersAvailable: return "No peers available for compact filter sync" case .downloadFailed: return "Failed to download filter data" case .invalidFilter: return "Invalid compact filter data" } } }`.trim(), sync_management: ` // Chain synchronization management and coordination import LightningDevKit import Foundation class ChainSyncManager { private let channelManager: Bindings.ChannelManager private let chainMonitor: Bindings.ChainMonitor private let logger: Bindings.Logger private var syncMethod: SyncMethod private var syncTask: Task<Void, Error>? private var lastSyncTime: Date = Date.distantPast private var syncInterval: TimeInterval = 30 // 30 seconds enum SyncMethod { case electrum(ElectrumChainSync) case esplora(EsploraChainSync) case bitcoinCore(BitcoinCoreSync) case compactFilters(CompactFilterSync) } init( channelManager: Bindings.ChannelManager, chainMonitor: Bindings.ChainMonitor, logger: Bindings.Logger, syncMethod: SyncMethod ) { self.channelManager = channelManager self.chainMonitor = chainMonitor self.logger = logger self.syncMethod = syncMethod } // Start automatic synchronization func startSync() { syncTask = Task { while !Task.isCancelled { do { try await performSync() lastSyncTime = Date() // Wait for next sync interval try await Task.sleep(nanoseconds: UInt64(syncInterval * 1_000_000_000)) } catch { logger.log(message: "Sync error: \\(error)") // Exponential backoff on error let backoffInterval = min(syncInterval * 2, 300) // Max 5 minutes try? await Task.sleep(nanoseconds: UInt64(backoffInterval * 1_000_000_000)) } } } } // Stop synchronization func stopSync() { syncTask?.cancel() syncTask = nil } // Perform synchronization based on method private func performSync() async throws { logger.log(message: "Starting chain sync...") switch syncMethod { case .electrum(let electrumSync): try await electrumSync.performSync() case .esplora(let esploraSync): try await esploraSync.sync() case .bitcoinCore(let bitcoinCoreSync): try await bitcoinCoreSync.checkForNewBlocks() case .compactFilters(let compactFilterSync): try await compactFilterSync.startSync() } // After sync, process any pending events channelManager.processPendingHtlcForwards() // Persist state LDKManager.shared.persistState() logger.log(message: "Chain sync completed") } // Force immediate sync func syncNow() async throws { try await performSync() } // Get sync status func getSyncStatus() -> SyncStatus { let currentTime = Date() let timeSinceLastSync = currentTime.timeIntervalSince(lastSyncTime) return SyncStatus( lastSyncTime: lastSyncTime, isSyncing: syncTask != nil && !syncTask!.isCancelled, syncMethod: syncMethodName(), isUpToDate: timeSinceLastSync < syncInterval * 2 ) } private func syncMethodName() -> String { switch syncMethod { case .electrum: return "Electrum" case .esplora: return "Esplora" case .bitcoinCore: return "Bitcoin Core" case .compactFilters: return "Compact Filters" } } } // Sync status struct SyncStatus { let lastSyncTime: Date let isSyncing: Bool let syncMethod: String let isUpToDate: Bool } // SwiftUI Sync Status View struct SyncStatusView: View { @StateObject private var viewModel = SyncStatusViewModel() var body: some View { VStack(spacing: 16) { // Sync indicator HStack { if viewModel.isSyncing { ProgressView() .scaleEffect(0.8) } else { Image(systemName: viewModel.isUpToDate ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") .foregroundColor(viewModel.isUpToDate ? .green : .orange) } Text(viewModel.statusText) .font(.headline) Spacer() } // Sync details VStack(alignment: .leading, spacing: 8) { HStack { Text("Method:") Spacer() Text(viewModel.syncMethod) .foregroundColor(.secondary) } HStack { Text("Last Sync:") Spacer() Text(viewModel.lastSyncTime, style: .relative) .foregroundColor(.secondary) } HStack { Text("Block Height:") Spacer() Text("\\(viewModel.blockHeight)") .foregroundColor(.secondary) } } .font(.subheadline) // Manual sync button Button(action: { Task { await viewModel.syncNow() } }) { Label("Sync Now", systemImage: "arrow.clockwise") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .disabled(viewModel.isSyncing) } .padding() .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(12) .onAppear { viewModel.startMonitoring() } .onDisappear { viewModel.stopMonitoring() } } } // View Model for sync status @MainActor class SyncStatusViewModel: ObservableObject { @Published var isSyncing = false @Published var isUpToDate = true @Published var syncMethod = "Unknown" @Published var lastSyncTime = Date() @Published var blockHeight: UInt32 = 0 private var timer: Timer? var statusText: String { if isSyncing { return "Syncing..." } else if isUpToDate { return "Synchronized" } else { return "Out of Sync" } } func startMonitoring() { updateStatus() timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self.updateStatus() } } func stopMonitoring() { timer?.invalidate() timer = nil } func syncNow() async { do { try await LDKManager.shared.chainSyncManager.syncNow() } catch { print("Manual sync failed: \\(error)") } } private func updateStatus() { let status = LDKManager.shared.chainSyncManager.getSyncStatus() isSyncing = status.isSyncing isUpToDate = status.isUpToDate syncMethod = status.syncMethod lastSyncTime = status.lastSyncTime // Get current block height if let nodeInfo = try? LDKManager.shared.channelManager.getNodeInfo() { blockHeight = nodeInfo.blockHeight } } }`.trim(), reorg_handling: ` // Blockchain reorganization handling import LightningDevKit import Foundation class ReorgHandler { private let channelManager: Bindings.ChannelManager private let chainMonitor: Bindings.ChainMonitor private let logger: Bindings.Logger private var blockHistory: [UInt32: BlockRecord] = [:] private let maxHistoryDepth: UInt32 = 100 struct BlockRecord { let height: UInt32 let hash: [UInt8] let previousHash: [UInt8] let timestamp: Date let confirmedTransactions: Set<[UInt8]> } init( channelManager: Bindings.ChannelManager, chainMonitor: Bindings.ChainMonitor, logger: Bindings.Logger ) { self.channelManager = channelManager self.chainMonitor = chainMonitor self.logger = logger loadBlockHistory() } // Check for and handle reorganization func checkForReorg(newBlock: BlockInfo) async throws -> ReorgResult { let newHeight = newBlock.height let newHash = newBlock.hash let previousHash = newBlock.previousHash // Check if this connects to our known chain if let knownBlock = blockHistory[newHeight - 1] { if knownBlock.hash == previousHash { // Normal case - new block extends our chain addBlockToHistory(newBlock) return .noReorg } } // Potential reorg - find common ancestor let reorgDepth = findReorgDepth( startHeight: newHeight - 1, targetHash: previousHash ) if reorgDepth > 0 { logger.log(message: "Reorg detected! Depth: \\(reorgDepth) blocks") // Handle the reorg try await handleReorg( fromHeight: newHeight - reorgDepth, newChainTip: newBlock ) return .reorgDetected(depth: reorgDepth) } // Add to history addBlockToHistory(newBlock) return .noReorg } // Find how deep the reorg is private func findReorgDepth(startHeight: UInt32, targetHash: [UInt8]) -> UInt32 { var depth: UInt32 = 0 var currentHeight = startHeight while currentHeight > 0 && depth < maxHistoryDepth { if let block = blockHistory[currentHeight] { if block.hash == targetHash { return depth } } depth += 1 currentHeight -= 1 } // Reorg deeper than our history return depth } // Handle reorganization private func handleReorg( fromHeight: UInt32, newChainTip: BlockInfo ) async throws { logger.log(message: "Handling reorg from height \\(fromHeight)") // 1. Collect all transactions that were confirmed after reorg point var reorgedTransactions = Set<[UInt8]>() for height in fromHeight...newChainTip.height { if let block = blockHistory[height] { reorgedTransactions.formUnion(block.confirmedTransactions) } } // 2. Mark transactions as unconfirmed for txid in reorgedTransactions { channelManager.asConfirm().transactionUnconfirmed(txid: txid) chainMonitor.asConfirm().transactionUnconfirmed(txid: txid) logger.log(message: "Transaction unconfirmed: \\(Data(txid).hexString)") } // 3. Remove reorged blocks from history for height in fromHeight...newChainTip.height { blockHistory.removeValue(forKey: height) } // 4. Re-scan from reorg point // This would involve re-downloading blocks and checking transactions try await rescanFromHeight(fromHeight) // 5. Update to new chain tip channelManager.asConfirm().bestBlockUpdated( header: newChainTip.hash, height: newChainTip.height ) chainMonitor.asConfirm().bestBlockUpdated( header: newChainTip.hash, height: newChainTip.height ) // 6. Check if any channels need to be force closed checkChannelsAfterReorg() } // Rescan blockchain from given height private func rescanFromHeight(_ height: UInt32) async throws { logger.log(message: "Rescanning from height \\(height)") // This would involve: // 1. Re-downloading blocks from the reorg point // 2. Re-checking all relevant transactions // 3. Re-confirming transactions that are still in the chain // Implementation depends on the chain source (Electrum/Esplora/etc) } // Check channels after reorg private func checkChannelsAfterReorg() { let channels = channelManager.listChannels() for channel in channels { // Check if funding transaction is still confirmed if let fundingTxo = channel.getFundingTxo() { // Would need to check if funding tx is still confirmed // If not, the channel may need to be closed } // Check if any commitment transactions were broadcast // This is handled automatically by LDK's chain monitoring } } // Add block to history private func addBlockToHistory(_ block: BlockInfo) { let record = BlockRecord( height: block.height, hash: block.hash, previousHash: block.previousHash, timestamp: Date(), confirmedTransactions: Set(block.confirmedTransactions) ) blockHistory[block.height] = record // Prune old history let pruneHeight = block.height.saturatingSubtraction(maxHistoryDepth) blockHistory = blockHistory.filter { $0.key >= pruneHeight } // Persist history saveBlockHistory() } // Persistence private func saveBlockHistory() { // Save to UserDefaults or file // Only save recent blocks to limit storage let recentBlocks = blockHistory.filter { $0.key > (blockHistory.keys.max() ?? 0).saturatingSubtraction(20) } if let encoded = try? JSONEncoder().encode(recentBlocks) { UserDefaults.standard.set(encoded, forKey: "blockHistory") } } private func loadBlockHistory() { // Load from UserDefaults or file if let data = UserDefaults.standard.data(forKey: "blockHistory"), let decoded = try? JSONDecoder().decode([UInt32: BlockRecord].self, from: data) { blockHistory = decoded } } } // Block info for reorg detection struct BlockInfo { let height: UInt32 let hash: [UInt8] let previousHash: [UInt8] let confirmedTransactions: [[UInt8]] } // Reorg result enum ReorgResult { case noReorg case reorgDetected(depth: UInt32) } // Reorg monitoring service class ReorgMonitor { private let reorgHandler: ReorgHandler private let logger: Bindings.Logger private var monitoringTask: Task<Void, Never>? init(reorgHandler: ReorgHandler, logger: Bindings.Logger) { self.reorgHandler = reorgHandler self.logger = logger } // Start monitoring for reorgs func startMonitoring() { monitoringTask = Task { while !Task.isCancelled { await checkForReorgs() // Check every 30 seconds try? await Task.sleep(nanoseconds: 30_000_000_000) } } } // Stop monitoring func stopMonitoring() { monitoringTask?.cancel() monitoringTask = nil } private func checkForReorgs() async { // This would get the latest block and check for reorgs // Implementation depends on chain source } } // SwiftUI Reorg Alert struct ReorgAlertView: View { let reorgDepth: UInt32 let onDismiss: () -> Void var body: some View { VStack(spacing: 20) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 50)) .foregroundColor(.orange) Text("Blockchain Reorganization") .font(.title2) .fontWeight(.bold) Text("A \\(reorgDepth)-block reorganization has been detected.") .multilineTextAlignment(.center) Text("Your wallet is automatically handling this. Some recent transactions may have changed confirmation status.") .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.center) Button("Understood") { onDismiss() } .buttonStyle(.borderedProminent) } .padding() .background(Color(UIColor.systemBackground)) .cornerRadius(16) .shadow(radius: 10) .padding() } }`.trim() }; try { const code = syncExamples[args.syncMethod]; if (!code) { throw new Error(`Unknown sync method: ${args.syncMethod}`); } return { content: [{ type: 'text', text: JSON.stringify({ success: true, syncMethod: args.syncMethod, swiftCode: code, description: `Chain synchronization implementation for ${args.syncMethod}` }, null, 2) }] }; } catch (error) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }, null, 2) }], isError: true }; } } };

Latest Blog Posts

MCP directory API

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

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

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