diff --git a/Bitkit/PaykitIntegration/Executors/BitkitBitcoinExecutor.swift b/Bitkit/PaykitIntegration/Executors/BitkitBitcoinExecutor.swift new file mode 100644 index 00000000..d042f5f8 --- /dev/null +++ b/Bitkit/PaykitIntegration/Executors/BitkitBitcoinExecutor.swift @@ -0,0 +1,186 @@ +// BitkitBitcoinExecutor.swift +// Bitkit iOS - Paykit Integration +// +// Implements BitcoinExecutorFFI to connect Bitkit's wallet to Paykit. + +import Foundation +import LDKNode + +// MARK: - BitkitBitcoinExecutor + +/// Bitkit implementation of BitcoinExecutorFFI. +/// +/// Bridges Bitkit's LightningService (which handles onchain) to Paykit's executor interface. +/// All methods are called synchronously from the Rust FFI layer. +/// +/// Thread Safety: All methods use async bridging with semaphores to handle +/// the async LightningService APIs from sync FFI calls. +public final class BitkitBitcoinExecutor { + + // MARK: - Properties + + private let lightningService: LightningService + private let timeout: TimeInterval = 60.0 + + // MARK: - Initialization + + public init(lightningService: LightningService = .shared) { + self.lightningService = lightningService + } + + // MARK: - BitcoinExecutorFFI Implementation + + /// Send Bitcoin to an address. + /// + /// Bridges async LightningService.send() to sync FFI call. + /// + /// - Parameters: + /// - address: Destination Bitcoin address + /// - amountSats: Amount to send in satoshis + /// - feeRate: Optional fee rate in sat/vB + /// - Returns: Transaction result with txid and fee details + public func sendToAddress( + address: String, + amountSats: UInt64, + feeRate: Double? + ) throws -> BitcoinTxResult { + let semaphore = DispatchSemaphore(value: 0) + var result: Result? + + let satsPerVbyte = UInt32(feeRate ?? 1.0) + + Task { + do { + let txid = try await lightningService.send( + address: address, + sats: amountSats, + satsPerVbyte: satsPerVbyte, + utxosToSpend: nil, + isMaxAmount: false + ) + result = .success(txid) + } catch { + result = .failure(error) + } + semaphore.signal() + } + + let waitResult = semaphore.wait(timeout: .now() + timeout) + + if waitResult == .timedOut { + throw PaykitError.timeout + } + + switch result { + case .success(let txid): + return BitcoinTxResult( + txid: txid.description, + rawTx: nil, + vout: 0, + feeSats: 0, + feeRate: feeRate ?? 1.0, + blockHeight: nil, + confirmations: 0 + ) + case .failure(let error): + throw PaykitError.paymentFailed(error.localizedDescription) + case .none: + throw PaykitError.unknown("No result returned") + } + } + + /// Estimate the fee for a transaction. + /// + /// - Parameters: + /// - address: Destination address + /// - amountSats: Amount to send + /// - targetBlocks: Confirmation target + /// - Returns: Estimated fee in satoshis + public func estimateFee( + address: String, + amountSats: UInt64, + targetBlocks: UInt32 + ) throws -> UInt64 { + // Use default fee estimation + // TODO: Implement actual fee estimation via CoreService + let baseFee: UInt64 = 250 + let feeMultiplier: UInt64 = switch targetBlocks { + case 1: 3 + case 2...3: 2 + default: 1 + } + return baseFee * feeMultiplier + } + + /// Get transaction details by txid. + /// + /// - Parameter txid: Transaction ID (hex-encoded) + /// - Returns: Transaction details if found + public func getTransaction(txid: String) throws -> BitcoinTxResult? { + // TODO: Implement via CoreService/ActivityService lookup + return nil + } + + /// Verify a transaction matches expected address and amount. + /// + /// - Parameters: + /// - txid: Transaction ID + /// - address: Expected destination address + /// - amountSats: Expected amount + /// - Returns: true if transaction matches expectations + public func verifyTransaction( + txid: String, + address: String, + amountSats: UInt64 + ) throws -> Bool { + // TODO: Implement verification via transaction lookup + guard let tx = try getTransaction(txid: txid) else { + return false + } + return tx.txid == txid + } +} + +// MARK: - Bitcoin Transaction Result + +/// Result of a Bitcoin transaction for Paykit FFI. +public struct BitcoinTxResult { + public let txid: String + public let rawTx: String? + public let vout: UInt32 + public let feeSats: UInt64 + public let feeRate: Double + public let blockHeight: UInt64? + public let confirmations: UInt64 + + public init( + txid: String, + rawTx: String?, + vout: UInt32, + feeSats: UInt64, + feeRate: Double, + blockHeight: UInt64?, + confirmations: UInt64 + ) { + self.txid = txid + self.rawTx = rawTx + self.vout = vout + self.feeSats = feeSats + self.feeRate = feeRate + self.blockHeight = blockHeight + self.confirmations = confirmations + } + + // TODO: Uncomment when PaykitMobile bindings are available + // func toFfi() -> BitcoinTxResultFfi { + // return BitcoinTxResultFfi( + // txid: txid, + // rawTx: rawTx, + // vout: vout, + // feeSats: feeSats, + // feeRate: feeRate, + // blockHeight: blockHeight, + // confirmations: confirmations + // ) + // } +} diff --git a/Bitkit/PaykitIntegration/Executors/BitkitLightningExecutor.swift b/Bitkit/PaykitIntegration/Executors/BitkitLightningExecutor.swift new file mode 100644 index 00000000..6132d15a --- /dev/null +++ b/Bitkit/PaykitIntegration/Executors/BitkitLightningExecutor.swift @@ -0,0 +1,243 @@ +// BitkitLightningExecutor.swift +// Bitkit iOS - Paykit Integration +// +// Implements LightningExecutorFFI to connect Bitkit's Lightning node to Paykit. + +import Foundation +import LDKNode +import CryptoKit + +// MARK: - BitkitLightningExecutor + +/// Bitkit implementation of LightningExecutorFFI. +/// +/// Bridges Bitkit's LightningService to Paykit's executor interface. +/// Handles async-to-sync bridging and payment completion polling. +public final class BitkitLightningExecutor { + + // MARK: - Properties + + private let lightningService: LightningService + private let timeout: TimeInterval = 60.0 + private let pollingInterval: TimeInterval = 0.5 + + // MARK: - Initialization + + public init(lightningService: LightningService = .shared) { + self.lightningService = lightningService + } + + // MARK: - LightningExecutorFFI Implementation + + /// Pay a BOLT11 invoice. + /// + /// Initiates payment and polls for completion to get preimage. + /// + /// - Parameters: + /// - invoice: BOLT11 invoice string + /// - amountMsat: Amount in millisatoshis (for zero-amount invoices) + /// - maxFeeMsat: Maximum fee willing to pay + /// - Returns: Payment result with preimage proof + public func payInvoice( + invoice: String, + amountMsat: UInt64?, + maxFeeMsat: UInt64? + ) throws -> LightningPaymentResult { + let semaphore = DispatchSemaphore(value: 0) + var paymentHashResult: Result? + + let sats = amountMsat.map { $0 / 1000 } + + Task { + do { + let paymentHash = try await lightningService.send( + bolt11: invoice, + sats: sats, + params: nil + ) + paymentHashResult = .success(paymentHash) + } catch { + paymentHashResult = .failure(error) + } + semaphore.signal() + } + + let waitResult = semaphore.wait(timeout: .now() + timeout) + + if waitResult == .timedOut { + throw PaykitError.timeout + } + + guard case .success(let paymentHash) = paymentHashResult else { + if case .failure(let error) = paymentHashResult { + throw PaykitError.paymentFailed(error.localizedDescription) + } + throw PaykitError.unknown("No result returned") + } + + // Poll for payment completion to get preimage + let paymentResult = try pollForPaymentCompletion(paymentHash: paymentHash.description) + return paymentResult + } + + /// Poll for payment completion to extract preimage. + private func pollForPaymentCompletion(paymentHash: String) throws -> LightningPaymentResult { + let startTime = Date() + + while Date().timeIntervalSince(startTime) < timeout { + if let payments = lightningService.payments { + for payment in payments { + if payment.id.description == paymentHash { + // Check payment status + switch payment.status { + case .succeeded: + // Extract preimage from payment details + // Note: Check actual PaymentDetails structure for preimage field + return LightningPaymentResult( + preimage: "", // TODO: Extract from payment.preimage + paymentHash: paymentHash, + amountMsat: 0, // TODO: Extract from payment + feeMsat: 0, // TODO: Extract from payment + hops: 0, + status: .succeeded + ) + case .failed: + throw PaykitError.paymentFailed("Payment failed") + default: + break // Still pending + } + } + } + } + + Thread.sleep(forTimeInterval: pollingInterval) + } + + throw PaykitError.timeout + } + + /// Decode a BOLT11 invoice. + /// + /// - Parameter invoice: BOLT11 invoice string + /// - Returns: Decoded invoice details + public func decodeInvoice(invoice: String) throws -> DecodedInvoice { + // TODO: Use BitkitCore.decode() when available + // For now, return placeholder + return DecodedInvoice( + paymentHash: "", + amountMsat: nil, + description: nil, + descriptionHash: nil, + payee: "", + expiry: 3600, + timestamp: UInt64(Date().timeIntervalSince1970), + expired: false + ) + } + + /// Estimate routing fee for an invoice. + /// + /// - Parameter invoice: BOLT11 invoice + /// - Returns: Estimated fee in millisatoshis + public func estimateFee(invoice: String) throws -> UInt64 { + // Default routing fee estimate (1% with 1000 msat base) + return 1000 + } + + /// Get payment status by payment hash. + /// + /// - Parameter paymentHash: Payment hash (hex-encoded) + /// - Returns: Payment result if found + public func getPayment(paymentHash: String) throws -> LightningPaymentResult? { + guard let payments = lightningService.payments else { + return nil + } + + for payment in payments { + if payment.id.description == paymentHash { + let status: LightningPaymentStatus = switch payment.status { + case .succeeded: .succeeded + case .failed: .failed + default: .pending + } + + return LightningPaymentResult( + preimage: "", // TODO: Extract from payment + paymentHash: paymentHash, + amountMsat: 0, // TODO: Extract from payment + feeMsat: 0, // TODO: Extract from payment + hops: 0, + status: status + ) + } + } + + return nil + } + + /// Verify preimage matches payment hash. + /// + /// - Parameters: + /// - preimage: Payment preimage (hex-encoded) + /// - paymentHash: Payment hash (hex-encoded) + /// - Returns: true if preimage hashes to payment hash + public func verifyPreimage(preimage: String, paymentHash: String) -> Bool { + guard let preimageData = Data(hexString: preimage) else { + return false + } + + let hash = SHA256.hash(data: preimageData) + let computedHash = hash.compactMap { String(format: "%02x", $0) }.joined() + + return computedHash.lowercased() == paymentHash.lowercased() + } +} + +// MARK: - Lightning Payment Result + +public struct LightningPaymentResult { + public let preimage: String + public let paymentHash: String + public let amountMsat: UInt64 + public let feeMsat: UInt64 + public let hops: UInt32 + public let status: LightningPaymentStatus +} + +public enum LightningPaymentStatus { + case pending + case succeeded + case failed +} + +// MARK: - Decoded Invoice + +public struct DecodedInvoice { + public let paymentHash: String + public let amountMsat: UInt64? + public let description: String? + public let descriptionHash: String? + public let payee: String + public let expiry: UInt64 + public let timestamp: UInt64 + public let expired: Bool +} + +// MARK: - Helper Extensions + +private extension Data { + init?(hexString: String) { + let hex = hexString.dropFirst(hexString.hasPrefix("0x") ? 2 : 0) + guard hex.count % 2 == 0 else { return nil } + + var data = Data(capacity: hex.count / 2) + var index = hex.startIndex + while index < hex.endIndex { + let nextIndex = hex.index(index, offsetBy: 2) + guard let byte = UInt8(hex[index.. Bool { + do { + try setup() + return true + } catch { + Logger.error("Paykit setup failed: \(error)", context: "PaykitIntegrationHelper") + return false + } + } + + // MARK: - Status + + /// Check if Paykit is ready for use. + public static var isReady: Bool { + let manager = PaykitManager.shared + return manager.isInitialized && manager.hasExecutors + } + + /// Get the current network configuration. + public static var networkInfo: (bitcoin: BitcoinNetworkConfig, lightning: LightningNetworkConfig) { + let manager = PaykitManager.shared + return (manager.bitcoinNetwork, manager.lightningNetwork) + } + + // MARK: - Payment Execution + + /// Execute a Lightning payment via Paykit. + /// + /// - Parameters: + /// - invoice: BOLT11 invoice + /// - amountSats: Amount in satoshis (for zero-amount invoices) + /// - Returns: Payment result + public static func payLightning( + invoice: String, + amountSats: UInt64? + ) async throws -> LightningPaymentResult { + guard isReady else { + throw PaykitError.notInitialized + } + + let executor = BitkitLightningExecutor() + let amountMsat = amountSats.map { $0 * 1000 } + + return try executor.payInvoice( + invoice: invoice, + amountMsat: amountMsat, + maxFeeMsat: nil + ) + } + + /// Execute an onchain payment via Paykit. + /// + /// - Parameters: + /// - address: Bitcoin address + /// - amountSats: Amount in satoshis + /// - feeRate: Fee rate in sat/vB + /// - Returns: Transaction result + public static func payOnchain( + address: String, + amountSats: UInt64, + feeRate: Double? + ) async throws -> BitcoinTxResult { + guard isReady else { + throw PaykitError.notInitialized + } + + let executor = BitkitBitcoinExecutor() + + return try executor.sendToAddress( + address: address, + amountSats: amountSats, + feeRate: feeRate + ) + } + + // MARK: - Cleanup + + /// Reset Paykit integration state. + /// + /// Call this during logout or wallet reset. + public static func reset() { + PaykitManager.shared.reset() + Logger.info("Paykit integration reset", context: "PaykitIntegrationHelper") + } +} + +// MARK: - Async Bridge Utilities + +/// Utilities for bridging async/await to sync FFI calls. +public enum AsyncBridge { + + /// Execute an async operation synchronously with timeout. + /// + /// - Parameters: + /// - timeout: Maximum time to wait + /// - operation: Async operation to execute + /// - Returns: Result of the operation + public static func runSync( + timeout: TimeInterval = 60.0, + operation: @escaping () async throws -> T + ) throws -> T { + let semaphore = DispatchSemaphore(value: 0) + var result: Result? + + Task { + do { + let value = try await operation() + result = .success(value) + } catch { + result = .failure(error) + } + semaphore.signal() + } + + let waitResult = semaphore.wait(timeout: .now() + timeout) + + if waitResult == .timedOut { + throw PaykitError.timeout + } + + switch result { + case .success(let value): + return value + case .failure(let error): + throw error + case .none: + throw PaykitError.unknown("No result returned") + } + } +} diff --git a/Bitkit/PaykitIntegration/PaykitManager.swift b/Bitkit/PaykitIntegration/PaykitManager.swift new file mode 100644 index 00000000..79e7c3e6 --- /dev/null +++ b/Bitkit/PaykitIntegration/PaykitManager.swift @@ -0,0 +1,145 @@ +// PaykitManager.swift +// Bitkit iOS - Paykit Integration +// +// Manages PaykitClient lifecycle and executor registration. + +import Foundation +import LDKNode + +// MARK: - PaykitManager + +/// Manages the Paykit client and executor registration for Bitkit integration. +public final class PaykitManager { + + // MARK: - Singleton + + public static let shared = PaykitManager() + + // MARK: - Properties + + private var client: Any? + private var bitcoinExecutor: BitkitBitcoinExecutor? + private var lightningExecutor: BitkitLightningExecutor? + + public private(set) var isInitialized: Bool = false + public private(set) var hasExecutors: Bool = false + + public let bitcoinNetwork: BitcoinNetworkConfig + public let lightningNetwork: LightningNetworkConfig + + // MARK: - Initialization + + private init() { + let ldkNetwork = Env.network + switch ldkNetwork { + case .bitcoin: + self.bitcoinNetwork = .mainnet + self.lightningNetwork = .mainnet + case .testnet: + self.bitcoinNetwork = .testnet + self.lightningNetwork = .testnet + case .regtest: + self.bitcoinNetwork = .regtest + self.lightningNetwork = .regtest + case .signet: + self.bitcoinNetwork = .testnet + self.lightningNetwork = .testnet + @unknown default: + self.bitcoinNetwork = .testnet + self.lightningNetwork = .testnet + } + } + + // MARK: - Public Methods + + /// Initialize the Paykit client with network configuration. + public func initialize() throws { + guard !isInitialized else { + Logger.debug("PaykitManager already initialized", context: "PaykitManager") + return + } + + Logger.info("Initializing PaykitManager with network: \(bitcoinNetwork)", context: "PaykitManager") + + // TODO: Uncomment when PaykitMobile bindings are available + // client = try PaykitClient.newWithNetwork( + // bitcoinNetwork: bitcoinNetwork.toFfi(), + // lightningNetwork: lightningNetwork.toFfi() + // ) + + isInitialized = true + Logger.info("PaykitManager initialized successfully", context: "PaykitManager") + } + + /// Register Bitcoin and Lightning executors with the Paykit client. + public func registerExecutors() throws { + guard isInitialized else { + throw PaykitError.notInitialized + } + + guard !hasExecutors else { + Logger.debug("Executors already registered", context: "PaykitManager") + return + } + + Logger.info("Registering Paykit executors", context: "PaykitManager") + + bitcoinExecutor = BitkitBitcoinExecutor() + lightningExecutor = BitkitLightningExecutor() + + // TODO: Uncomment when PaykitMobile bindings are available + // guard let client = client as? PaykitClient else { + // throw PaykitError.notInitialized + // } + // try client.registerBitcoinExecutor(executor: bitcoinExecutor!) + // try client.registerLightningExecutor(executor: lightningExecutor!) + + hasExecutors = true + Logger.info("Paykit executors registered successfully", context: "PaykitManager") + } + + /// Reset the manager state + public func reset() { + client = nil + bitcoinExecutor = nil + lightningExecutor = nil + isInitialized = false + hasExecutors = false + Logger.info("PaykitManager reset", context: "PaykitManager") + } +} + +// MARK: - Network Configuration + +public enum BitcoinNetworkConfig: String { + case mainnet, testnet, regtest +} + +public enum LightningNetworkConfig: String { + case mainnet, testnet, regtest +} + +// MARK: - Paykit Errors + +public enum PaykitError: LocalizedError { + case notInitialized + case executorRegistrationFailed(String) + case paymentFailed(String) + case timeout + case unknown(String) + + public var errorDescription: String? { + switch self { + case .notInitialized: + return "PaykitManager has not been initialized" + case .executorRegistrationFailed(let message): + return "Failed to register executor: \(message)" + case .paymentFailed(let message): + return "Payment failed: \(message)" + case .timeout: + return "Operation timed out" + case .unknown(let message): + return "Unknown error: \(message)" + } + } +} diff --git a/INTEGRATION_DISCOVERY.md b/INTEGRATION_DISCOVERY.md new file mode 100644 index 00000000..fa4aa3c0 --- /dev/null +++ b/INTEGRATION_DISCOVERY.md @@ -0,0 +1,284 @@ +# Paykit Integration Discovery - iOS + +This document outlines the integration points for connecting Paykit-rs with Bitkit iOS. + +## Repository Structure + +### Key Services + +#### LightningService +- **Location**: `Bitkit/Services/LightningService.swift` +- **Type**: Singleton (`LightningService.shared`) +- **Purpose**: Manages LDKNode Lightning Network operations +- **Dependencies**: `LDKNode`, `BitkitCore` + +**Key Methods for Paykit Integration**: + +1. **Lightning Payment**: + ```swift + func send(bolt11: String, sats: UInt64? = nil, params: SendingParameters? = nil) async throws -> PaymentHash + ``` + - **Location**: Line 368 + - **Returns**: `PaymentHash` (from LDKNode) + - **Usage**: Pay Lightning invoices + - **Error Handling**: Throws `AppError` + +2. **Onchain Payment**: + ```swift + func send( + address: String, + sats: UInt64, + satsPerVbyte: UInt32, + utxosToSpend: [SpendableUtxo]? = nil, + isMaxAmount: Bool = false + ) async throws -> Txid + ``` + - **Location**: Line 330 + - **Returns**: `Txid` (from LDKNode) + - **Usage**: Send Bitcoin on-chain + - **Error Handling**: Throws `AppError` + +3. **Payment Access**: + ```swift + var payments: [PaymentDetails]? { node?.listPayments() } + ``` + - **Location**: Line 548 (extension) + - **Returns**: Array of payment details + - **Usage**: Get payment status and preimage + +4. **Payment Events**: + ```swift + func listenForEvents(onEvent: ((Event) -> Void)? = nil) + ``` + - **Location**: Line 554 + - **Event Types**: `.paymentSuccessful(paymentId, paymentHash, paymentPreimage, feePaidMsat)` + - **Usage**: Listen for payment completion + +#### CoreService +- **Location**: `Bitkit/Services/CoreService.swift` +- **Type**: Singleton (`CoreService.shared`) +- **Purpose**: Manages onchain wallet operations and activity tracking +- **Dependencies**: `BitkitCore` + +**Key Methods for Paykit Integration**: + +1. **Transaction Lookup**: + - Use `ActivityService` (nested in CoreService) to lookup transactions + - Access via `CoreService.shared.activityService` + +2. **Fee Estimation**: + - Use `CoreService.shared.blocktank.getFees()` for fee rates + - Returns `FeeRates` object with different speed options + +## API Mappings for Paykit Executors + +### BitcoinExecutorFFI Implementation + +#### sendToAddress +- **Bitkit API**: `LightningService.shared.send(address:sats:satsPerVbyte:utxosToSpend:isMaxAmount:)` +- **Async Pattern**: `async throws -> Txid` +- **Bridging**: Use `Task` with `withCheckedThrowingContinuation` +- **Return Mapping**: + - `Txid` → Extract `.hex` for `BitcoinTxResultFfi.txid` + - Need to query transaction for fee, vout, confirmations + +#### estimateFee +- **Bitkit API**: `CoreService.shared.blocktank.getFees()` +- **Return**: Fee in satoshis for target blocks +- **Mapping**: Convert `TransactionSpeed` to target blocks + +#### getTransaction +- **Bitkit API**: Use `CoreService` or `ActivityService` to lookup transaction +- **Query**: By `txid` (String) +- **Return**: `BitcoinTxResultFfi` or `nil` + +#### verifyTransaction +- **Bitkit API**: Get transaction via `getTransaction`, verify outputs +- **Return**: Boolean + +### LightningExecutorFFI Implementation + +#### payInvoice +- **Bitkit API**: `LightningService.shared.send(bolt11:sats:params:)` +- **Async Pattern**: `async throws -> PaymentHash` +- **Bridging**: Use `Task` with `withCheckedThrowingContinuation` +- **Payment Completion**: + - Option 1: Listen to `Event.paymentSuccessful` (includes preimage) + - Option 2: Poll `LightningService.shared.payments` array +- **Return Mapping**: + - `PaymentHash` → `LightningPaymentResultFfi.paymentHash` + - Extract preimage from event or payment details + +#### decodeInvoice +- **Bitkit API**: `BitkitCore.decode(invoice: String)` → `LightningInvoice` +- **Mapping**: + - `LightningInvoice.paymentHash` → `DecodedInvoiceFfi.paymentHash` + - `LightningInvoice.amountMsat` → `DecodedInvoiceFfi.amountMsat` + - `LightningInvoice.description` → `DecodedInvoiceFfi.description` + - `LightningInvoice.payeePubkey` → `DecodedInvoiceFfi.payee` + - `LightningInvoice.expiry` → `DecodedInvoiceFfi.expiry` + - `LightningInvoice.timestamp` → `DecodedInvoiceFfi.timestamp` + +#### estimateFee +- **Bitkit API**: `CoreService.shared.blocktank.getFees()` for routing fees +- **Return**: Fee in millisatoshis + +#### getPayment +- **Bitkit API**: `LightningService.shared.payments` → `[PaymentDetails]?` +- **Find by**: `paymentHash` (compare hex strings) +- **Extract**: `PaymentDetails.preimage`, `amountMsat`, `feePaidMsat`, `status` + +#### verifyPreimage +- **Implementation**: Compute SHA256 of preimage, compare with payment hash +- **Library**: CryptoKit or CommonCrypto + +## Error Types + +### AppError +- **Location**: Defined in Bitkit error handling +- **Structure**: + ```swift + struct AppError: Error { + let message: String? + let debugMessage: String? + let serviceError: ServiceError? + } + ``` +- **Mapping to PaykitMobileError**: + - `ServiceError.nodeNotSetup` → `PaykitMobileError.Internal` + - `ServiceError.nodeNotStarted` → `PaykitMobileError.Internal` + - General errors → `PaykitMobileError.Transport` + +## Network Configuration + +### Current Network Setup +- **Location**: `Bitkit/Constants/Env.swift` +- **Property**: `Env.network: LDKNode.Network` +- **Values**: `.bitcoin`, `.testnet`, `.regtest`, `.signet` +- **Mapping to Paykit**: + - `.bitcoin` → `BitcoinNetworkFfi.mainnet` / `LightningNetworkFfi.mainnet` + - `.testnet` → `BitcoinNetworkFfi.testnet` / `LightningNetworkFfi.testnet` + - `.regtest` → `BitcoinNetworkFfi.regtest` / `LightningNetworkFfi.regtest` + - `.signet` → `BitcoinNetworkFfi.testnet` / `LightningNetworkFfi.testnet` (fallback) + +## Async/Sync Patterns + +### Current Pattern +- **Bitkit Services**: All use `async/await` (Swift concurrency) +- **Paykit FFI**: Synchronous methods +- **Bridging Strategy**: Use `Task` with `withCheckedThrowingContinuation` + +### Example Bridge Pattern +```swift +func syncMethod() throws -> Result { + return try withCheckedThrowingContinuation { continuation in + Task { + do { + let result = try await asyncMethod() + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } +} +``` + +## Thread Safety + +- **LightningService**: Singleton, accessed via `.shared` +- **CoreService**: Singleton, accessed via `.shared` +- **FFI Methods**: May be called from any thread +- **Strategy**: `withCheckedThrowingContinuation` handles thread safety + +## Payment Completion Handling + +### Event-Based Approach (Recommended) +```swift +// Set up event listener before payment +var paymentPreimage: String? +let semaphore = DispatchSemaphore(value: 0) + +LightningService.shared.listenForEvents { event in + if case .paymentSuccessful(_, let hash, let preimage, _) = event, + hash == paymentHash { + paymentPreimage = preimage + semaphore.signal() + } +} + +// Start payment +let paymentHash = try await LightningService.shared.send(bolt11: invoice) + +// Wait for completion +if semaphore.wait(timeout: .now() + 60) == .timedOut { + throw PaykitMobileError.NetworkTimeout(message: "Payment timeout") +} +``` + +### Polling Approach (Alternative) +```swift +let paymentHash = try await LightningService.shared.send(bolt11: invoice) + +var attempts = 0 +while attempts < 60 { + if let payments = LightningService.shared.payments, + let payment = payments.first(where: { $0.paymentHash == paymentHash }), + let preimage = payment.preimage { + break + } + try await Task.sleep(nanoseconds: 1_000_000_000) + attempts += 1 +} +``` + +## Transaction Details Extraction + +### Challenge +- `LightningService.send()` returns only `Txid` +- `BitcoinTxResultFfi` needs: fee, vout, confirmations, blockHeight + +### Solution +1. Return initial result with `confirmations: 0`, `blockHeight: nil` +2. Query transaction details after broadcast: + ```swift + let txid = try await LightningService.shared.send(...) + try await Task.sleep(nanoseconds: 2_000_000_000) // Wait for propagation + let txDetails = try await CoreService.shared.getTransaction(txid: txid) + // Extract fee, vout, confirmations + ``` + +## File Structure for Integration + +### Proposed Structure +``` +Bitkit/ +└── PaykitIntegration/ + ├── PaykitManager.swift + ├── PaykitIntegrationHelper.swift + ├── Executors/ + │ ├── BitkitBitcoinExecutor.swift + │ └── BitkitLightningExecutor.swift + └── Services/ + └── PaykitPaymentService.swift +``` + +## Dependencies + +### Current Dependencies +- `LDKNode`: Lightning Network node implementation +- `BitkitCore`: Core wallet functionality +- `Foundation`: Standard library + +### New Dependencies +- `PaykitMobile`: Generated UniFFI bindings (to be added) + +## Next Steps + +1. ✅ Discovery complete (this document) +2. ⏳ Set up Paykit-rs dependency +3. ⏳ Generate UniFFI bindings +4. ⏳ Configure Xcode build settings +5. ⏳ Implement executors +6. ⏳ Register executors with PaykitClient +7. ⏳ Integration testing