chainSync.ts•55.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
};
}
}
};