@mcpsovereign/sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Automated LND Setup for mcpSovereign
3
+ *
4
+ * This module handles:
5
+ * 1. Docker installation check
6
+ * 2. LND container deployment (Neutrino mode - no full node needed)
7
+ * 3. Wallet creation
8
+ * 4. Credential extraction
9
+ * 5. Configuration for SDK
10
+ *
11
+ * Based on the same setup used for mcpsovereign.com
12
+ */
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+ import { spawn, exec } from 'child_process';
16
+ import { promisify } from 'util';
17
+ const execAsync = promisify(exec);
18
+ // ============================================================
19
+ // DEFAULT CONFIG
20
+ // ============================================================
21
+ const DEFAULT_CONFIG = {
22
+ containerName: 'sovereign-agent-lnd',
23
+ network: 'mainnet',
24
+ alias: 'SovereignAgent',
25
+ restPort: 8089,
26
+ grpcPort: 10009,
27
+ p2pPort: 9735,
28
+ dataDir: path.join(process.env.HOME || '.', '.sovereign-lnd')
29
+ };
30
+ // ============================================================
31
+ // DOCKER COMPOSE TEMPLATE
32
+ // ============================================================
33
+ function generateDockerCompose(config) {
34
+ return `# Generated by mcpSovereign SDK
35
+ # LND Lightning Node (Neutrino mode - no full Bitcoin node required)
36
+
37
+ services:
38
+ lnd:
39
+ image: lightninglabs/lnd:v0.18.0-beta
40
+ container_name: ${config.containerName}
41
+ restart: unless-stopped
42
+ volumes:
43
+ - ${config.dataDir}:/root/.lnd
44
+ ports:
45
+ - "${config.p2pPort}:9735"
46
+ - "${config.grpcPort}:10009"
47
+ - "${config.restPort}:8080"
48
+ command: >
49
+ lnd
50
+ --bitcoin.active
51
+ --bitcoin.${config.network}
52
+ --bitcoin.node=neutrino
53
+ --neutrino.addpeer=btcd-mainnet.lightning.computer
54
+ --neutrino.addpeer=mainnet1-btcd.zaphq.io
55
+ --neutrino.addpeer=mainnet2-btcd.zaphq.io
56
+ --fee.url=https://nodes.lightning.computer/fees/v1/btc-fee-estimates.json
57
+ --alias=${config.alias}
58
+ --rpclisten=0.0.0.0:10009
59
+ --restlisten=0.0.0.0:8080
60
+ --tlsextradomain=${config.containerName}
61
+ --tlsextraip=0.0.0.0
62
+ --accept-keysend
63
+ --accept-amp
64
+ --gc-canceled-invoices-on-startup
65
+ networks:
66
+ - sovereign-net
67
+
68
+ networks:
69
+ sovereign-net:
70
+ driver: bridge
71
+ `;
72
+ }
73
+ // ============================================================
74
+ // SYSTEM CHECKS
75
+ // ============================================================
76
+ async function checkDocker() {
77
+ try {
78
+ const { stdout } = await execAsync('docker --version');
79
+ const version = stdout.trim();
80
+ // Check if Docker daemon is running
81
+ try {
82
+ await execAsync('docker ps');
83
+ return { installed: true, running: true, version };
84
+ }
85
+ catch {
86
+ return { installed: true, running: false, version };
87
+ }
88
+ }
89
+ catch {
90
+ return { installed: false, running: false };
91
+ }
92
+ }
93
+ async function checkDockerCompose() {
94
+ try {
95
+ await execAsync('docker compose version');
96
+ return true;
97
+ }
98
+ catch {
99
+ try {
100
+ await execAsync('docker-compose --version');
101
+ return true;
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ }
108
+ // ============================================================
109
+ // LND SETUP CLASS
110
+ // ============================================================
111
+ export class LNDSetup {
112
+ config;
113
+ logs = [];
114
+ constructor(config = {}) {
115
+ this.config = { ...DEFAULT_CONFIG, ...config };
116
+ }
117
+ log(message) {
118
+ this.logs.push(`[${new Date().toISOString()}] ${message}`);
119
+ console.log(message);
120
+ }
121
+ /**
122
+ * Check if system is ready for LND setup
123
+ */
124
+ async checkPrerequisites() {
125
+ this.log('Checking prerequisites...');
126
+ // Check Docker
127
+ const docker = await checkDocker();
128
+ this.log(`Docker: ${docker.installed ? 'Installed' : 'Not installed'} ${docker.running ? '(running)' : '(not running)'}`);
129
+ // Check Docker Compose
130
+ const compose = await checkDockerCompose();
131
+ this.log(`Docker Compose: ${compose ? 'Available' : 'Not available'}`);
132
+ // Check disk space (Neutrino needs ~1GB, we'll ask for 5GB to be safe)
133
+ let diskSpace = { available: 0, required: 5 * 1024 * 1024 * 1024, sufficient: false };
134
+ try {
135
+ const { stdout } = await execAsync(`df -B1 ${this.config.dataDir} 2>/dev/null || df -B1 ${process.env.HOME || '.'}`);
136
+ const lines = stdout.trim().split('\n');
137
+ if (lines.length > 1) {
138
+ const parts = lines[1].split(/\s+/);
139
+ diskSpace.available = parseInt(parts[3]) || 0;
140
+ diskSpace.sufficient = diskSpace.available > diskSpace.required;
141
+ }
142
+ }
143
+ catch {
144
+ diskSpace.sufficient = true; // Assume OK if we can't check
145
+ }
146
+ this.log(`Disk space: ${Math.round(diskSpace.available / 1024 / 1024 / 1024)}GB available`);
147
+ return {
148
+ ready: docker.installed && docker.running && compose && diskSpace.sufficient,
149
+ docker,
150
+ compose,
151
+ diskSpace
152
+ };
153
+ }
154
+ /**
155
+ * Install Docker if not present (Linux only)
156
+ */
157
+ async installDocker() {
158
+ this.log('Attempting to install Docker...');
159
+ const platform = process.platform;
160
+ if (platform !== 'linux') {
161
+ this.log(`Automatic Docker installation not supported on ${platform}.`);
162
+ this.log('Please install Docker manually:');
163
+ this.log(' macOS: https://docs.docker.com/desktop/install/mac-install/');
164
+ this.log(' Windows: https://docs.docker.com/desktop/install/windows-install/');
165
+ return false;
166
+ }
167
+ try {
168
+ // Use Docker's official install script
169
+ this.log('Running Docker install script...');
170
+ await execAsync('curl -fsSL https://get.docker.com | sh');
171
+ // Add current user to docker group
172
+ const user = process.env.USER || 'root';
173
+ await execAsync(`sudo usermod -aG docker ${user}`);
174
+ // Start Docker service
175
+ await execAsync('sudo systemctl start docker');
176
+ await execAsync('sudo systemctl enable docker');
177
+ this.log('Docker installed successfully!');
178
+ this.log('NOTE: You may need to log out and back in for group changes to take effect.');
179
+ return true;
180
+ }
181
+ catch (error) {
182
+ this.log(`Docker installation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
183
+ return false;
184
+ }
185
+ }
186
+ /**
187
+ * Set up LND container
188
+ */
189
+ async setupLND() {
190
+ const result = { success: false, logs: [] };
191
+ try {
192
+ // 1. Create data directory
193
+ this.log(`Creating data directory: ${this.config.dataDir}`);
194
+ fs.mkdirSync(this.config.dataDir, { recursive: true });
195
+ // 2. Generate docker-compose.yml
196
+ const composePath = path.join(this.config.dataDir, 'docker-compose.yml');
197
+ const composeContent = generateDockerCompose(this.config);
198
+ fs.writeFileSync(composePath, composeContent);
199
+ this.log('Generated docker-compose.yml');
200
+ // 3. Start the container
201
+ this.log('Starting LND container (this may take a minute)...');
202
+ await execAsync(`docker compose -f ${composePath} up -d`);
203
+ // 4. Wait for LND to initialize
204
+ this.log('Waiting for LND to initialize...');
205
+ await this.waitForLND();
206
+ // 5. Check if wallet exists
207
+ const walletExists = await this.checkWalletExists();
208
+ if (!walletExists) {
209
+ // 6. Create new wallet
210
+ this.log('Creating new Lightning wallet...');
211
+ const walletResult = await this.createWallet();
212
+ if (!walletResult.success) {
213
+ result.error = walletResult.error;
214
+ result.logs = this.logs;
215
+ return result;
216
+ }
217
+ result.walletSeed = walletResult.seed;
218
+ }
219
+ else {
220
+ this.log('Existing wallet found.');
221
+ }
222
+ // 7. Wait for sync
223
+ this.log('Waiting for Neutrino sync (this can take 5-15 minutes on first run)...');
224
+ await this.waitForSync();
225
+ // 8. Extract credentials
226
+ this.log('Extracting credentials...');
227
+ const credentials = await this.extractCredentials();
228
+ if (!credentials) {
229
+ result.error = 'Failed to extract LND credentials';
230
+ result.logs = this.logs;
231
+ return result;
232
+ }
233
+ result.success = true;
234
+ result.credentials = credentials;
235
+ result.logs = this.logs;
236
+ this.log('LND setup complete!');
237
+ return result;
238
+ }
239
+ catch (error) {
240
+ result.error = error instanceof Error ? error.message : 'Unknown error';
241
+ result.logs = this.logs;
242
+ return result;
243
+ }
244
+ }
245
+ /**
246
+ * Wait for LND REST API to be available
247
+ */
248
+ async waitForLND(maxAttempts = 30) {
249
+ for (let i = 0; i < maxAttempts; i++) {
250
+ try {
251
+ await execAsync(`docker exec ${this.config.containerName} lncli --network=${this.config.network} getinfo 2>&1`);
252
+ return true;
253
+ }
254
+ catch (error) {
255
+ const errorMsg = error instanceof Error ? error.message : '';
256
+ // Wallet not created yet is OK - LND is up
257
+ if (errorMsg.includes('wallet not created') || errorMsg.includes('wallet locked')) {
258
+ return true;
259
+ }
260
+ await new Promise(resolve => setTimeout(resolve, 2000));
261
+ }
262
+ }
263
+ return false;
264
+ }
265
+ /**
266
+ * Check if wallet already exists
267
+ */
268
+ async checkWalletExists() {
269
+ try {
270
+ const { stderr } = await execAsync(`docker exec ${this.config.containerName} lncli --network=${this.config.network} getinfo 2>&1`);
271
+ // If we get wallet locked, it exists
272
+ return !stderr.includes('wallet not created');
273
+ }
274
+ catch (error) {
275
+ const errorMsg = error instanceof Error ? error.message : '';
276
+ return errorMsg.includes('wallet locked') || !errorMsg.includes('wallet not created');
277
+ }
278
+ }
279
+ /**
280
+ * Create new LND wallet
281
+ */
282
+ async createWallet() {
283
+ return new Promise((resolve) => {
284
+ // Generate a random password
285
+ const password = this.generatePassword();
286
+ // Save password for auto-unlock
287
+ const passwordPath = path.join(this.config.dataDir, 'wallet_password');
288
+ fs.writeFileSync(passwordPath, password);
289
+ this.log('Creating wallet with secure password...');
290
+ // Use expect-style interaction
291
+ const lncli = spawn('docker', [
292
+ 'exec', '-i', this.config.containerName,
293
+ 'lncli', `--network=${this.config.network}`, 'create'
294
+ ]);
295
+ let output = '';
296
+ let seed = [];
297
+ let step = 0;
298
+ lncli.stdout.on('data', (data) => {
299
+ const text = data.toString();
300
+ output += text;
301
+ // Step through the prompts
302
+ if (text.includes('wallet password')) {
303
+ if (step === 0) {
304
+ lncli.stdin.write(password + '\n');
305
+ step = 1;
306
+ }
307
+ else if (step === 1) {
308
+ lncli.stdin.write(password + '\n');
309
+ step = 2;
310
+ }
311
+ }
312
+ if (text.includes('cipher seed mnemonic')) {
313
+ lncli.stdin.write('n\n'); // Don't use existing seed
314
+ step = 3;
315
+ }
316
+ if (text.includes('passphrase')) {
317
+ lncli.stdin.write('\n'); // No passphrase
318
+ step = 4;
319
+ }
320
+ // Extract seed words
321
+ const seedMatch = text.match(/\d+\.\s+(\w+)/g);
322
+ if (seedMatch) {
323
+ seed = seedMatch.map(s => s.replace(/\d+\.\s+/, ''));
324
+ }
325
+ });
326
+ lncli.stderr.on('data', (data) => {
327
+ output += data.toString();
328
+ });
329
+ lncli.on('close', (code) => {
330
+ if (code === 0 && seed.length === 24) {
331
+ // Save seed (in production, user should back this up and delete)
332
+ const seedPath = path.join(this.config.dataDir, 'BACKUP_SEED_DELETE_AFTER_BACKUP.txt');
333
+ fs.writeFileSync(seedPath, `IMPORTANT: Back up these 24 words and DELETE this file!\n\n${seed.join(' ')}`);
334
+ resolve({ success: true, seed });
335
+ }
336
+ else {
337
+ resolve({ success: false, error: `Wallet creation failed: ${output}` });
338
+ }
339
+ });
340
+ lncli.on('error', (err) => {
341
+ resolve({ success: false, error: err.message });
342
+ });
343
+ });
344
+ }
345
+ /**
346
+ * Wait for Neutrino to sync
347
+ */
348
+ async waitForSync(maxMinutes = 30) {
349
+ const maxAttempts = maxMinutes * 6; // Check every 10 seconds
350
+ for (let i = 0; i < maxAttempts; i++) {
351
+ try {
352
+ const { stdout } = await execAsync(`docker exec ${this.config.containerName} lncli --network=${this.config.network} getinfo`);
353
+ const info = JSON.parse(stdout);
354
+ if (info.synced_to_chain) {
355
+ this.log('Neutrino sync complete!');
356
+ return true;
357
+ }
358
+ const progress = info.block_height ? `Block ${info.block_height}` : 'Syncing...';
359
+ if (i % 6 === 0) { // Log every minute
360
+ this.log(`Sync progress: ${progress}`);
361
+ }
362
+ }
363
+ catch {
364
+ // Still starting up
365
+ }
366
+ await new Promise(resolve => setTimeout(resolve, 10000));
367
+ }
368
+ this.log('Warning: Sync timed out. Node may still be syncing in background.');
369
+ return false;
370
+ }
371
+ /**
372
+ * Extract LND credentials for SDK use
373
+ */
374
+ async extractCredentials() {
375
+ try {
376
+ // Copy cert and macaroon from container
377
+ const certPath = path.join(this.config.dataDir, 'tls.cert');
378
+ const macaroonPath = path.join(this.config.dataDir, 'admin.macaroon');
379
+ await execAsync(`docker cp ${this.config.containerName}:/root/.lnd/tls.cert ${certPath}`);
380
+ await execAsync(`docker cp ${this.config.containerName}:/root/.lnd/data/chain/bitcoin/${this.config.network}/admin.macaroon ${macaroonPath}`);
381
+ // Read and encode
382
+ const tlsCert = fs.readFileSync(certPath).toString('base64');
383
+ const adminMacaroon = fs.readFileSync(macaroonPath).toString('hex');
384
+ return {
385
+ tlsCert,
386
+ adminMacaroon,
387
+ restHost: `localhost:${this.config.restPort}`,
388
+ grpcHost: `localhost:${this.config.grpcPort}`
389
+ };
390
+ }
391
+ catch (error) {
392
+ this.log(`Failed to extract credentials: ${error instanceof Error ? error.message : 'Unknown error'}`);
393
+ return null;
394
+ }
395
+ }
396
+ /**
397
+ * Generate a secure random password
398
+ */
399
+ generatePassword() {
400
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
401
+ let password = '';
402
+ for (let i = 0; i < 24; i++) {
403
+ password += chars[Math.floor(Math.random() * chars.length)];
404
+ }
405
+ return password;
406
+ }
407
+ /**
408
+ * Get container status
409
+ */
410
+ async getStatus() {
411
+ try {
412
+ // Check if container is running
413
+ const { stdout: psOutput } = await execAsync(`docker ps --filter "name=${this.config.containerName}" --format "{{.Status}}"`);
414
+ if (!psOutput.trim()) {
415
+ return { running: false, synced: false };
416
+ }
417
+ // Get node info
418
+ const { stdout } = await execAsync(`docker exec ${this.config.containerName} lncli --network=${this.config.network} getinfo`);
419
+ const info = JSON.parse(stdout);
420
+ // Get balance
421
+ const { stdout: balanceOut } = await execAsync(`docker exec ${this.config.containerName} lncli --network=${this.config.network} walletbalance`);
422
+ const balance = JSON.parse(balanceOut);
423
+ return {
424
+ running: true,
425
+ synced: info.synced_to_chain,
426
+ blockHeight: info.block_height,
427
+ peers: info.num_peers,
428
+ channels: info.num_active_channels,
429
+ balance: {
430
+ confirmed: parseInt(balance.confirmed_balance) || 0,
431
+ unconfirmed: parseInt(balance.unconfirmed_balance) || 0
432
+ }
433
+ };
434
+ }
435
+ catch {
436
+ return { running: false, synced: false };
437
+ }
438
+ }
439
+ /**
440
+ * Stop LND container
441
+ */
442
+ async stop() {
443
+ try {
444
+ const composePath = path.join(this.config.dataDir, 'docker-compose.yml');
445
+ await execAsync(`docker compose -f ${composePath} down`);
446
+ return true;
447
+ }
448
+ catch {
449
+ return false;
450
+ }
451
+ }
452
+ /**
453
+ * Start LND container (if stopped)
454
+ */
455
+ async start() {
456
+ try {
457
+ const composePath = path.join(this.config.dataDir, 'docker-compose.yml');
458
+ await execAsync(`docker compose -f ${composePath} up -d`);
459
+ return true;
460
+ }
461
+ catch {
462
+ return false;
463
+ }
464
+ }
465
+ getLogs() {
466
+ return [...this.logs];
467
+ }
468
+ }
469
+ // ============================================================
470
+ // CONVENIENCE FUNCTIONS
471
+ // ============================================================
472
+ /**
473
+ * Quick setup for agents who want their own node
474
+ */
475
+ export async function quickSetupLND(alias) {
476
+ const setup = new LNDSetup({
477
+ alias: alias || `SovereignAgent_${Date.now().toString(36)}`
478
+ });
479
+ // Check prerequisites
480
+ const prereqs = await setup.checkPrerequisites();
481
+ if (!prereqs.docker.installed) {
482
+ console.log('\nDocker is required for self-hosted Lightning.');
483
+ console.log('Would you like to install it? (This requires sudo on Linux)');
484
+ // In real implementation, this would be interactive
485
+ return {
486
+ success: false,
487
+ error: 'Docker not installed. Please install Docker first: https://docs.docker.com/get-docker/',
488
+ logs: setup.getLogs()
489
+ };
490
+ }
491
+ if (!prereqs.docker.running) {
492
+ return {
493
+ success: false,
494
+ error: 'Docker is installed but not running. Please start Docker.',
495
+ logs: setup.getLogs()
496
+ };
497
+ }
498
+ // Run setup
499
+ return setup.setupLND();
500
+ }
501
+ export default LNDSetup;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Wallet Types for mcpSovereign
3
+ *
4
+ * Lightning-first wallet setup and management
5
+ */
6
+ export type WalletProvider = 'alby' | 'phoenix' | 'walletofsatoshi' | 'muun' | 'bluewallet' | 'breez' | 'zeus' | 'custom';
7
+ export interface WalletProviderInfo {
8
+ id: WalletProvider;
9
+ name: string;
10
+ emoji: string;
11
+ description: string;
12
+ type: 'custodial' | 'non-custodial' | 'self-hosted';
13
+ platforms: ('web' | 'ios' | 'android' | 'desktop')[];
14
+ supportsLNURL: boolean;
15
+ supportsLightningAddress: boolean;
16
+ setupUrl: string;
17
+ difficulty: 'easy' | 'medium' | 'advanced';
18
+ recommended?: boolean;
19
+ }
20
+ export declare const WALLET_PROVIDERS: Record<WalletProvider, WalletProviderInfo>;
21
+ export interface WalletConfig {
22
+ provider: WalletProvider;
23
+ lightningAddress?: string;
24
+ lnurlPay?: string;
25
+ nodeUri?: string;
26
+ setupComplete: boolean;
27
+ verified: boolean;
28
+ verifiedAt?: string;
29
+ createdAt: string;
30
+ lastUsed?: string;
31
+ }
32
+ export interface LightningAddressInfo {
33
+ address: string;
34
+ username: string;
35
+ domain: string;
36
+ valid: boolean;
37
+ lnurlPayUrl?: string;
38
+ metadata?: {
39
+ description?: string;
40
+ minSendable?: number;
41
+ maxSendable?: number;
42
+ nostrPubkey?: string;
43
+ };
44
+ }
45
+ export interface WalletSetupProgress {
46
+ currentStep: number;
47
+ completed: boolean;
48
+ provider?: WalletProvider;
49
+ lightningAddress?: string;
50
+ verified: boolean;
51
+ skipped: boolean;
52
+ }
53
+ export interface WalletSetupStep {
54
+ id: string;
55
+ title: string;
56
+ description: string;
57
+ emoji: string;
58
+ action: string;
59
+ }
60
+ export declare const WALLET_SETUP_STEPS: WalletSetupStep[];
61
+ export interface PaymentRequest {
62
+ paymentHash: string;
63
+ paymentRequest: string;
64
+ amountSats: number;
65
+ description: string;
66
+ expiresAt: string;
67
+ status: 'pending' | 'paid' | 'expired' | 'failed';
68
+ }
69
+ export interface PaymentResult {
70
+ success: boolean;
71
+ paymentHash?: string;
72
+ preimage?: string;
73
+ feeSats?: number;
74
+ error?: string;
75
+ }
76
+ export interface WithdrawalRequest {
77
+ amountCredits: number;
78
+ amountSats: number;
79
+ lightningAddress: string;
80
+ status: 'pending' | 'processing' | 'complete' | 'failed';
81
+ }
82
+ export interface WithdrawalResult {
83
+ success: boolean;
84
+ withdrawalId?: string;
85
+ amountSats?: number;
86
+ feeSats?: number;
87
+ paymentHash?: string;
88
+ error?: string;
89
+ }