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
};
}
}
};