Skip to main content
Glama

ldk_chain_sync

Retrieve chain synchronization implementations for LDK using Electrum, Esplora, or other methods to ensure accurate blockchain data for Lightning wallet development.

Instructions

Get chain synchronization implementations for LDK with Electrum/Esplora

Input Schema

NameRequiredDescriptionDefault
syncMethodYesChain synchronization method to get implementation for

Input Schema (JSON Schema)

{ "properties": { "syncMethod": { "description": "Chain synchronization method to get implementation for", "enum": [ "electrum_sync", "esplora_sync", "bitcoin_core_rpc", "compact_filters", "sync_management", "reorg_handling" ], "type": "string" } }, "required": [ "syncMethod" ], "type": "object" }

Implementation Reference

  • The complete tool implementation including handler (execute function) that generates and returns Swift code examples for LDK chain synchronization methods based on the input syncMethod.
    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 }; } }
  • Input schema defining the required 'syncMethod' parameter with enum values for different chain sync implementations.
    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'] },
  • src/index.ts:35-62 (registration)
    Import of chainSyncTool and addition to the tools array used for MCP server registration. The server then handles listTools and callTool requests using this array.
    import { chainSyncTool } from './tools/chainSync.js'; // Aggregate all tools 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, ];

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