Skip to main content
Glama

ldk_test_scenario

Execute Lightning network test scenarios, including payments, channel management, and edge cases, to validate iOS wallet functionality using LDK MCP Server's development tools.

Instructions

Run complete Lightning development scenarios for testing

Input Schema

NameRequiredDescriptionDefault
scenarioYesTest scenario to run

Input Schema (JSON Schema)

{ "properties": { "scenario": { "description": "Test scenario to run", "enum": [ "basic_payment", "channel_lifecycle", "multi_hop_payment", "payment_failure", "force_close", "offline_receive", "fee_spike", "backup_restore" ], "type": "string" } }, "required": [ "scenario" ], "type": "object" }

Implementation Reference

  • Complete tool implementation defining the 'ldk_test_scenario' handler, including name, description, input schema, and execute function that provides detailed test scenarios for LDK Lightning network operations.
    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 }; } } };
  • Input schema for the tool specifying the 'scenario' parameter with allowed enum values for different LDK test scenarios.
    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'] },
  • src/index.ts:32-62 (registration)
    Import of testScenarioTool and inclusion in the tools array used by the MCP server for tool listing and execution handling.
    import { testScenarioTool } from './tools/testScenario.js'; import { networkGraphTool } from './tools/networkGraph.js'; import { eventHandlingTool } from './tools/eventHandling.js'; 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