Skip to main content
Glama
testScenario.tsโ€ข13.9 kB
import { Tool, ToolResult } from '../types/tool.js'; export const testScenarioTool: Tool = { name: 'ldk_test_scenario', description: 'Run complete Lightning development scenarios for testing', inputSchema: { type: 'object', properties: { scenario: { type: 'string', enum: [ 'basic_payment', 'channel_lifecycle', 'multi_hop_payment', 'payment_failure', 'force_close', 'offline_receive', 'fee_spike', 'backup_restore' ], description: 'Test scenario to run' } }, required: ['scenario'] }, execute: async (args: any): Promise<ToolResult> => { const scenarios: Record<string, any> = { basic_payment: { title: 'Basic Lightning Payment Test', description: 'Test a simple payment between two nodes', steps: [ { step: 1, action: 'Setup nodes', code: `// Initialize two LDK nodes let alice = try await LDKNode(name: "Alice", network: .regtest) let bob = try await LDKNode(name: "Bob", network: .regtest) // Connect nodes try await alice.connectToPeer( pubkey: bob.nodeId, address: "127.0.0.1", port: 9735 )` }, { step: 2, action: 'Open channel', code: `// Alice opens channel to Bob let channelId = try await alice.openChannel( peerPubkey: bob.nodeId, amountSats: 1_000_000, pushSats: 100_000 ) // Wait for channel confirmation try await alice.waitForChannelReady(channelId: channelId)` }, { step: 3, action: 'Create invoice', code: `// Bob creates invoice let invoice = try await bob.createInvoice( amountSats: 10_000, description: "Test payment" ) print("Invoice: \\(invoice.bolt11)")` }, { step: 4, action: 'Send payment', code: `// Alice pays invoice let paymentResult = try await alice.payInvoice(invoice.bolt11) assert(paymentResult.status == .succeeded) print("Payment sent! Preimage: \\(paymentResult.preimage)")` }, { step: 5, action: 'Verify payment', code: `// Verify balances updated let aliceBalance = try await alice.getBalance() let bobBalance = try await bob.getBalance() assert(aliceBalance.totalSats < 900_000) // Deducted payment + fees assert(bobBalance.totalSats > 109_000) // Received payment` } ], expectedResults: [ 'Channel opens successfully', 'Payment completes within 5 seconds', 'Balances update correctly', 'Payment events fire properly' ] }, channel_lifecycle: { title: 'Complete Channel Lifecycle Test', description: 'Test opening, using, and closing a channel', steps: [ { step: 1, action: 'Open channel', code: `// Open channel with specific parameters let config = ChannelConfig( isPublic: false, minDepth: 3, maxHtlcCount: 10, forceCloseAvoidanceMaxFeeSats: 1000 ) let channel = try await node.openChannel( peerPubkey: peerNode.nodeId, amountSats: 500_000, config: config )` }, { step: 2, action: 'Send payments', code: `// Send multiple payments for i in 1...5 { let invoice = try await peerNode.createInvoice( amountSats: UInt64(i * 1000), description: "Payment \\(i)" ) try await node.payInvoice(invoice.bolt11) print("Payment \\(i) completed") }` }, { step: 3, action: 'Check channel state', code: `// Monitor channel health let channelInfo = try await node.getChannel(channelId: channel.id) print("Local balance: \\(channelInfo.localBalanceSats)") print("Remote balance: \\(channelInfo.remoteBalanceSats)") print("Total sent: \\(channelInfo.totalSatsSent)") print("Total received: \\(channelInfo.totalSatsReceived)")` }, { step: 4, action: 'Cooperative close', code: `// Close channel cooperatively try await node.closeChannel( channelId: channel.id, targetFeeSatsPerVbyte: 5 ) // Wait for close transaction try await node.waitForChannelClose(channelId: channel.id) // Verify funds returned to wallet let finalBalance = try await node.getOnChainBalance() assert(finalBalance > 490_000) // Most funds returned minus fees` } ] }, multi_hop_payment: { title: 'Multi-Hop Payment Test', description: 'Test payment routing through multiple nodes', setup: `// Setup: Alice -> Bob -> Carol -> Dave let nodes = try await createNodeNetwork(["Alice", "Bob", "Carol", "Dave"]) // Open channels in sequence try await openChannel(from: nodes[0], to: nodes[1], sats: 1_000_000) try await openChannel(from: nodes[1], to: nodes[2], sats: 1_000_000) try await openChannel(from: nodes[2], to: nodes[3], sats: 1_000_000)`, test: `// Dave creates invoice let invoice = try await nodes[3].createInvoice( amountSats: 50_000, description: "Multi-hop test" ) // Alice finds route and pays let route = try await nodes[0].findRoute( to: nodes[3].nodeId, amountSats: 50_000 ) print("Route: \\(route.hops.map { $0.nodeId.prefix(8) }.joined(separator: " -> "))") print("Total fees: \\(route.totalFeeSats) sats") let payment = try await nodes[0].payInvoice(invoice.bolt11) assert(payment.status == .succeeded)`, validation: [ 'Payment routes through all 3 hops', 'Each node earns routing fees', 'Payment completes in < 10 seconds' ] }, payment_failure: { title: 'Payment Failure Handling Test', description: 'Test various payment failure scenarios', scenarios: [ { name: 'Insufficient liquidity', code: `// Try to pay more than channel capacity let invoice = try await createLargeInvoice(sats: 2_000_000) do { try await node.payInvoice(invoice) XCTFail("Payment should have failed") } catch PaymentError.insufficientBalance { print("โœ“ Correctly failed with insufficient balance") }` }, { name: 'Invalid invoice', code: `// Test expired invoice let expiredInvoice = "lnbc1..." // Old invoice do { try await node.payInvoice(expiredInvoice) XCTFail("Should fail on expired invoice") } catch PaymentError.invoiceExpired { print("โœ“ Correctly rejected expired invoice") }` }, { name: 'Route not found', code: `// Try to pay disconnected node let isolatedNode = try await createIsolatedNode() let invoice = try await isolatedNode.createInvoice(sats: 1000) do { try await node.payInvoice(invoice.bolt11) XCTFail("Should fail to find route") } catch PaymentError.noRoute { print("โœ“ Correctly failed to find route") }` } ] }, force_close: { title: 'Force Close Channel Test', description: 'Test unilateral channel closure and fund recovery', steps: [ { action: 'Setup channel with pending HTLCs', code: `// Create channel with in-flight payments let channel = try await alice.openChannel(to: bob, sats: 1_000_000) // Create pending payment (don't claim yet) let preimage = generatePreimage() let paymentHash = sha256(preimage) let invoice = bob.createHoldInvoice( paymentHash: paymentHash, amountSats: 50_000 ) // Alice sends but Bob doesn't claim Task { try await alice.payInvoice(invoice) }` }, { action: 'Force close channel', code: `// Bob goes offline bob.disconnect() // Alice force closes let closeTx = try await alice.forceCloseChannel(channelId: channel.id) print("Force close tx: \\(closeTx.txid)") // Wait for confirmation try await alice.waitForTransaction(txid: closeTx.txid)` }, { action: 'Claim funds after timeout', code: `// Wait for CSV timeout (144 blocks on regtest) try await mineBlocks(count: 144) // Alice claims her funds let claimTx = try await alice.claimForceCloseOutputs() print("Claimed funds in tx: \\(claimTx.txid)") // Verify funds recovered let balance = try await alice.getOnChainBalance() assert(balance > 940_000) // Most funds recovered minus fees` } ] }, offline_receive: { title: 'Offline Payment Receive Test', description: 'Test receiving payments while offline', implementation: `// Generate invoices while online let invoices = try await (0..<5).asyncMap { i in try await node.createInvoice( amountSats: 10_000, description: "Offline test \\(i)" ) } // Go offline node.disconnect() print("Node offline, invoices still valid") // Payer sends to invoices (will be pending) for invoice in invoices { Task { try? await payerNode.payInvoice(invoice.bolt11) } } // Come back online try await node.connect() print("Node back online") // Process pending payments let received = try await node.processPendingPayments() print("Received \\(received.count) payments while offline") // Verify all payments received for payment in received { assert(payment.status == .succeeded) assert(payment.amountSats == 10_000) }`, notes: [ 'Payments are held by sender until receiver comes online', 'HTLCs have timeout, so extended offline periods may fail', 'Watchtowers can help claim funds while offline' ] }, fee_spike: { title: 'Fee Spike Handling Test', description: 'Test behavior during high on-chain fee periods', scenario: `// Simulate fee spike mockFeeEstimator.setFeeRate(.high, satsPerVbyte: 200) // Test channel operations let tests = [ "Opening channels becomes expensive", "Force close protections activate", "Anchor outputs allow fee bumping", "Routing fees may increase" ] // Test opening channel with high fees do { try await node.openChannel( to: peer, sats: 100_000, targetConf: 6 ) } catch ChannelError.feesTooHigh(let estimatedFee) { print("Channel open would cost \\(estimatedFee) sats in fees") // Wait for lower fees or use anchor outputs let config = ChannelConfig(useAnchors: true) try await node.openChannel( to: peer, sats: 100_000, config: config ) }`, adaptations: [ 'Use anchor outputs for fee flexibility', 'Batch operations when possible', 'Prefer cooperative closes', 'Implement fee estimation warnings in UI' ] }, backup_restore: { title: 'Backup and Restore Test', description: 'Test complete wallet backup and restoration', steps: [ { action: 'Create backup', code: `// Backup all critical data let backup = try await node.createBackup() let backupData = BackupData( seed: backup.seed, channelMonitors: backup.channelMonitors, channelManager: backup.channelManager, timestamp: Date() ) // Encrypt and save let encrypted = try backupData.encrypt(password: "testpass") try encrypted.write(to: backupURL)` }, { action: 'Simulate data loss', code: `// Clear all local data try FileManager.default.removeItem(at: ldkDataDirectory) // Verify node cannot start do { try await LDKNode.load(from: ldkDataDirectory) XCTFail("Should not load without data") } catch { print("โœ“ Correctly failed to load")` }, { action: 'Restore from backup', code: `// Load and decrypt backup let encryptedData = try Data(contentsOf: backupURL) let backup = try BackupData.decrypt( data: encryptedData, password: "testpass" ) // Restore node let restoredNode = try await LDKNode.restore( from: backup, network: .regtest ) // Verify channels restored let channels = try await restoredNode.listChannels() assert(channels.count == originalChannelCount) // Force close if peer is gone for channel in channels { if !channel.isUsable { try await restoredNode.forceCloseChannel( channelId: channel.id ) } }` } ], criticalData: [ 'Seed phrase (encrypted)', 'Channel monitors (latest state)', 'Channel manager state', 'Network graph (optional)', 'Payment history (optional)' ] } }; try { const scenario = scenarios[args.scenario]; if (!scenario) { throw new Error(`Unknown scenario: ${args.scenario}`); } return { content: [{ type: 'text', text: JSON.stringify({ success: true, scenario: args.scenario, testPlan: scenario, runCommand: ` // To run this scenario in your iOS app: // 1. Create a test target in Xcode // 2. Import the scenario code // 3. Run with: cmd+U or 'xcodebuild test' // Example test file: import XCTest import LightningDevKit @testable import LightningWallet class ${args.scenario.charAt(0).toUpperCase() + args.scenario.slice(1).replace(/_/g, '')}Tests: XCTestCase { func test${args.scenario.charAt(0).toUpperCase() + args.scenario.slice(1).replace(/_/g, '')}() async throws { // Scenario implementation here } }`.trim() }, null, 2) }] }; } catch (error) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }, null, 2) }], isError: true }; } } };

Latest Blog Posts

MCP directory API

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

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

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