lnlink-server 1.0.1 → 1.0.2

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.
Files changed (91) hide show
  1. package/README.md +461 -0
  2. package/{app.js → dist/app.js} +329 -321
  3. package/dist/binaries.json +25 -0
  4. package/{build-info.json → dist/build-info.json} +2 -1
  5. package/{index.js → dist/index.js} +690 -121
  6. package/dist/index.js.map +7 -0
  7. package/dist/node_modules/debug/.coveralls.yml +1 -0
  8. package/dist/node_modules/debug/.eslintrc +11 -0
  9. package/dist/node_modules/debug/.travis.yml +14 -0
  10. package/dist/node_modules/debug/CHANGELOG.md +362 -0
  11. package/dist/node_modules/debug/LICENSE +19 -0
  12. package/dist/node_modules/debug/Makefile +50 -0
  13. package/dist/node_modules/debug/README.md +312 -0
  14. package/dist/node_modules/debug/component.json +19 -0
  15. package/dist/node_modules/debug/karma.conf.js +70 -0
  16. package/dist/node_modules/debug/node.js +1 -0
  17. package/dist/node_modules/debug/package.json +49 -0
  18. package/dist/node_modules/debug/src/browser.js +185 -0
  19. package/dist/node_modules/debug/src/debug.js +202 -0
  20. package/dist/node_modules/debug/src/index.js +10 -0
  21. package/dist/node_modules/debug/src/inspector-log.js +15 -0
  22. package/dist/node_modules/debug/src/node.js +248 -0
  23. package/dist/node_modules/depd/History.md +96 -0
  24. package/dist/node_modules/depd/LICENSE +22 -0
  25. package/dist/node_modules/depd/Readme.md +280 -0
  26. package/dist/node_modules/depd/index.js +522 -0
  27. package/dist/node_modules/depd/lib/browser/index.js +77 -0
  28. package/dist/node_modules/depd/lib/compat/callsite-tostring.js +103 -0
  29. package/dist/node_modules/depd/lib/compat/event-listener-count.js +22 -0
  30. package/dist/node_modules/depd/lib/compat/index.js +79 -0
  31. package/dist/node_modules/depd/package.json +41 -0
  32. package/dist/node_modules/http-errors/HISTORY.md +132 -0
  33. package/dist/node_modules/http-errors/LICENSE +23 -0
  34. package/dist/node_modules/http-errors/README.md +135 -0
  35. package/dist/node_modules/http-errors/index.js +260 -0
  36. package/dist/node_modules/http-errors/package.json +48 -0
  37. package/dist/node_modules/inherits/LICENSE +16 -0
  38. package/dist/node_modules/inherits/README.md +42 -0
  39. package/dist/node_modules/inherits/inherits.js +7 -0
  40. package/dist/node_modules/inherits/inherits_browser.js +23 -0
  41. package/dist/node_modules/inherits/package.json +29 -0
  42. package/dist/node_modules/ms/index.js +152 -0
  43. package/dist/node_modules/ms/license.md +21 -0
  44. package/dist/node_modules/ms/package.json +37 -0
  45. package/dist/node_modules/ms/readme.md +51 -0
  46. package/dist/node_modules/setprototypeof/LICENSE +13 -0
  47. package/dist/node_modules/setprototypeof/README.md +26 -0
  48. package/dist/node_modules/setprototypeof/index.d.ts +2 -0
  49. package/dist/node_modules/setprototypeof/index.js +15 -0
  50. package/dist/node_modules/setprototypeof/package.json +25 -0
  51. package/dist/node_modules/statuses/HISTORY.md +65 -0
  52. package/dist/node_modules/statuses/LICENSE +23 -0
  53. package/dist/node_modules/statuses/README.md +127 -0
  54. package/dist/node_modules/statuses/codes.json +66 -0
  55. package/dist/node_modules/statuses/index.js +113 -0
  56. package/dist/node_modules/statuses/package.json +48 -0
  57. package/dist/package.json +62 -0
  58. package/dist/public/css/initOwner.css +705 -0
  59. package/dist/public/img/logo.svg +1 -0
  60. package/{public → dist/public}/init.html +5 -5
  61. package/dist/public/js/init.js +840 -0
  62. package/{setting.regtest.json → dist/setting.regtest.json} +2 -1
  63. package/package.json +51 -20
  64. package/binaries.json +0 -20
  65. package/index.js.map +0 -7
  66. package/public/css/initOwner.css +0 -553
  67. package/public/js/init.js +0 -454
  68. /package/{config.default.js → dist/config.default.js} +0 -0
  69. /package/{prisma → dist/prisma}/migrations/20250918020814_/migration.sql +0 -0
  70. /package/{prisma → dist/prisma}/migrations/20251114105314_auto_update/migration.sql +0 -0
  71. /package/{prisma → dist/prisma}/migrations/migration_lock.toml +0 -0
  72. /package/{prisma → dist/prisma}/schema.prisma +0 -0
  73. /package/{proto → dist/proto}/chainkit.proto +0 -0
  74. /package/{proto → dist/proto}/lightning.proto +0 -0
  75. /package/{proto → dist/proto}/lit-status.proto +0 -0
  76. /package/{proto → dist/proto}/looprpc/client.proto +0 -0
  77. /package/{proto → dist/proto}/price_oracle.proto +0 -0
  78. /package/{proto → dist/proto}/rfqrpc/rfq.proto +0 -0
  79. /package/{proto → dist/proto}/routerrpc/router.proto +0 -0
  80. /package/{proto → dist/proto}/signrpc/signer.proto +0 -0
  81. /package/{proto → dist/proto}/stateservice.proto +0 -0
  82. /package/{proto → dist/proto}/swapserverrpc/common.proto +0 -0
  83. /package/{proto → dist/proto}/tapchannel.proto +0 -0
  84. /package/{proto → dist/proto}/tapcommon.proto +0 -0
  85. /package/{proto → dist/proto}/taprootassets.proto +0 -0
  86. /package/{proto → dist/proto}/universe.proto +0 -0
  87. /package/{proto → dist/proto}/walletkit.proto +0 -0
  88. /package/{proto → dist/proto}/walletunlocker.proto +0 -0
  89. /package/{public → dist/public}/favicon.ico +0 -0
  90. /package/{setting.mainnet.json → dist/setting.mainnet.json} +0 -0
  91. /package/{setting.testnet.json → dist/setting.testnet.json} +0 -0
@@ -0,0 +1,840 @@
1
+ /* global bootstrap */
2
+
3
+ // Global toggle state
4
+ let isAdvancedConfigVisible = false;
5
+
6
+ // Function to open external URL in default browser
7
+ function openExternalUrl(url) {
8
+ if (window.electronAPI && window.electronAPI.openExternal) {
9
+ window.electronAPI.openExternal(url);
10
+ } else {
11
+ // Fallback for non-Electron environments
12
+ window.open(url, "_blank");
13
+ }
14
+ }
15
+
16
+ // Network configuration presets
17
+ const defaultNetworkConfigs = {
18
+ regtest: {
19
+ network: "regtest",
20
+ },
21
+ testnet: {
22
+ network: "testnet",
23
+ },
24
+ mainnet: {
25
+ network: "mainnet",
26
+ },
27
+ };
28
+
29
+ // Initialize networkConfigs with default values
30
+ let networkConfigs = JSON.parse(JSON.stringify(defaultNetworkConfigs));
31
+
32
+ // Fetch network configurations from API
33
+ async function fetchNetworkConfigs() {
34
+ try {
35
+ const response = await fetch(
36
+ "https://dev-edge-api.unift.xyz/edge/nodeconfig/getList",
37
+ {
38
+ method: "POST",
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ },
42
+ body: JSON.stringify({}),
43
+ },
44
+ );
45
+
46
+ const result = await response.json();
47
+
48
+ if (result.code === "0" && result.data && Array.isArray(result.data)) {
49
+ const apiConfigs = {};
50
+
51
+ result.data.forEach((item) => {
52
+ if (!item.network) return;
53
+ const networkKey = item.network.toLowerCase();
54
+
55
+ apiConfigs[networkKey] = {
56
+ bitcoindIndex: item.rgbIndex,
57
+ bitcoindPass: item.bitcoindRpcPassword,
58
+ bitcoindRpcHost: item.bitcoindRpcHost,
59
+ bitcoindRpcPort: String(item.bitcoindRpcPort),
60
+ bitcoindUser: item.bitcoindRpcUser,
61
+ bitcoindZmqBlock: item.bitcoindZmqBlock,
62
+ bitcoindZmqRawTx: item.bitcoindZmqRawTx,
63
+ network: networkKey,
64
+ nostrRelays: item.nostrRelays ? [item.nostrRelays] : [],
65
+ officialLndPeer: item.lndPeerId,
66
+ officialLndPeerHost: item.lndPeerHost,
67
+ officialNostrPubKey: item.nostrPubkey,
68
+ officialRgbPeer: item.rgbPeerId,
69
+ officialRgbPeerHost: item.rgbPeerHost,
70
+ officialUniverseServer: item.lndUniverseServer,
71
+ priceOracle: item.lndPriceOracle,
72
+ rgbProxy: item.rgbProxy,
73
+ };
74
+ });
75
+
76
+ // Update global configs, strictly using API response if valid
77
+ if (Object.keys(apiConfigs).length > 0) {
78
+ networkConfigs = apiConfigs;
79
+ console.log("Network configs updated from API");
80
+ }
81
+ } else {
82
+ console.warn(
83
+ "API returned success but no data or invalid format, using default configs.",
84
+ );
85
+ }
86
+ } catch (error) {
87
+ console.error(
88
+ "Error fetching network configs, using default configs:",
89
+ error,
90
+ );
91
+ }
92
+ }
93
+
94
+ // Field display name mapping
95
+ const fieldLabels = {
96
+ // Bitcoin Config
97
+ bitcoindRpcHost: "Bitcoind RPC Host",
98
+ bitcoindRpcPort: "Port",
99
+ bitcoindUser: "User",
100
+ bitcoindPass: "Password",
101
+ bitcoindZmqBlock: "ZMQ Block",
102
+ bitcoindZmqRawTx: "ZMQ Raw TX",
103
+ // LND Config
104
+ officialLndPeer: "Peer ID",
105
+ officialLndPeerHost: "Peer Host",
106
+ officialUniverseServer: "Universe Server",
107
+ priceOracle: "Price Oracle",
108
+ // RGB Config
109
+ officialRgbPeer: "Peer ID",
110
+ officialRgbPeerHost: "Peer Host",
111
+ rgbProxy: "Proxy",
112
+ bitcoindIndex: "Index",
113
+ // Nostr Config
114
+ officialNostrPubKey: "PubKey",
115
+ nostrRelays: "Relays",
116
+ network: "Network Type",
117
+ owner: "Owner Address",
118
+ };
119
+
120
+ // Show error modal
121
+ function showErrorModal(message) {
122
+ const errorModalBody = document.getElementById("errorModalBody");
123
+ errorModalBody.textContent = message;
124
+ if (typeof bootstrap !== "undefined") {
125
+ const errorModal = new bootstrap.Modal(
126
+ document.getElementById("errorModal"),
127
+ );
128
+ errorModal.show();
129
+ } else {
130
+ console.error("Error:", message);
131
+ }
132
+ }
133
+
134
+ // Show success modal
135
+ function showSuccessModal(message) {
136
+ const successModalBody = document.getElementById("successModalBody");
137
+ successModalBody.textContent = message;
138
+ if (typeof bootstrap !== "undefined") {
139
+ const successModal = new bootstrap.Modal(
140
+ document.getElementById("successModal"),
141
+ );
142
+ successModal.show();
143
+ } else {
144
+ console.log("Success:", message);
145
+ }
146
+ }
147
+
148
+ // Status determination function
149
+ function getStatusClass(value) {
150
+ const strValue = String(value).toLowerCase();
151
+ if (
152
+ strValue === "true" ||
153
+ strValue === "running" ||
154
+ strValue === "active" ||
155
+ strValue === "online"
156
+ ) {
157
+ return { class: "status-success", icon: "fas fa-check-circle" };
158
+ } else if (
159
+ strValue === "false" ||
160
+ strValue === "stopped" ||
161
+ strValue === "inactive" ||
162
+ strValue === "offline"
163
+ ) {
164
+ return { class: "status-error", icon: "fas fa-times-circle" };
165
+ } else if (strValue.includes("pending") || strValue.includes("waiting")) {
166
+ return { class: "status-warning", icon: "fas fa-clock" };
167
+ }
168
+ return { class: "", icon: "fas fa-info-circle" };
169
+ }
170
+
171
+ // Get system information
172
+ async function getInfo(showLoading = true) {
173
+ const mainContent = document.getElementById("main-content");
174
+
175
+ // Show loading state
176
+ if (showLoading) {
177
+ mainContent.innerHTML =
178
+ '<div class="loading-container"><div class="loading"></div><div>Loading information...</div></div>';
179
+ }
180
+
181
+ try {
182
+ const res = await fetch("/api/lnd/info").then((res) => res.json());
183
+
184
+ if (res.code === 200) {
185
+ if (res.data) {
186
+ const { owner, settings } = res.data;
187
+
188
+ // If both owner and settings exist, show configured view
189
+ if (owner && settings) {
190
+ renderConfiguredView(res.data, settings);
191
+ }
192
+ // If missing owner or settings, show configuration form
193
+ else {
194
+ // Fetch network configs only when needed for configuration
195
+ await fetchNetworkConfigs();
196
+
197
+ fetch("https://api.example.com/login", {
198
+ method: "POST",
199
+ headers: {
200
+ "Content-Type": "application/json",
201
+ Authorization: "Bearer your-token-here", // 可选
202
+ },
203
+ body: JSON.stringify({
204
+ username: "test",
205
+ password: "123456",
206
+ }),
207
+ });
208
+ renderConfigurationForm(res.data);
209
+ }
210
+ }
211
+ } else if (
212
+ res.code === 500 &&
213
+ res.message === "Please init the wallet or init owner first"
214
+ ) {
215
+ // Fetch network configs only when needed for configuration
216
+ await fetchNetworkConfigs();
217
+
218
+ // If wallet/owner not initialized, show configuration form
219
+ renderConfigurationForm({});
220
+ } else {
221
+ mainContent.innerHTML =
222
+ '<div class="alert alert-danger"><i class="fas fa-exclamation-triangle"></i> Failed to load information</div>';
223
+ }
224
+ } catch (error) {
225
+ console.error("Failed to get info:", error);
226
+ mainContent.innerHTML =
227
+ '<div class="alert alert-danger"><i class="fas fa-exclamation-triangle"></i> Network error occurred</div>';
228
+ }
229
+ }
230
+
231
+ // Render configured view
232
+ function renderConfiguredView(basicData, settings) {
233
+ const mainContent = document.getElementById("main-content");
234
+
235
+ // Extract Node ID (Nostr PubKey)
236
+ const nodeId =
237
+ settings.officialNostrPubKey || basicData.officialNostrPubKey || "Unknown";
238
+
239
+ // Determine Service Status
240
+ // We check basicData for status indicators or default to design mocks if not present
241
+ // Assuming keys might exist, otherwise using placeholders for the design requirement
242
+ const rgbStatus = basicData.rgb || "Stopped"; // Mock/Default based on design
243
+ const litdStatus = basicData.litd || "Stopped"; // Mock/Default based on design
244
+
245
+ const isRgbRunning =
246
+ rgbStatus.toLowerCase() === "running" || rgbStatus === "true";
247
+ const isLitdRunning =
248
+ litdStatus.toLowerCase() === "running" || litdStatus === "true";
249
+
250
+ const rgbClass = isRgbRunning ? "running" : "stopped";
251
+ const litdClass = isLitdRunning ? "running" : "stopped";
252
+
253
+ const rgbText = isRgbRunning ? "Running" : "Stopped";
254
+ const litdText = isLitdRunning ? "Running" : "Stopped";
255
+
256
+ // Helper to shorten address in the middle
257
+ const shortenAddress = (address, chars = 10) => {
258
+ if (!address || address.length <= chars * 2) return address || "";
259
+ return `${address.substring(0, chars)}...${address.substring(address.length - chars)}`;
260
+ };
261
+
262
+ const html = `
263
+ <div class="header-logo text-center mb-5">
264
+ <img src="./img/logo.svg" alt="NodeFlow Logo" style="height: 110px;">
265
+ </div>
266
+
267
+ <div class="welcome-container">
268
+ <h1 class="welcome-title">Welcome!</h1>
269
+ <p class="welcome-subtitle">Your nodes, managed effortlessly.</p>
270
+
271
+ <div class="status-card">
272
+ <!-- Owner Address -->
273
+ <div class="read-only-input-group">
274
+ <label class="card-label">Owner Address</label>
275
+ <div class="input-container" style="position: relative;">
276
+ <div class="read-only-input" id="display-owner">${shortenAddress(basicData.owner || "")}</div>
277
+ <button class="copy-icon-btn" onclick="copyToClipboard('${basicData.owner || ""}', this)">
278
+ <i class="far fa-copy"></i>
279
+ </button>
280
+ </div>
281
+ </div>
282
+
283
+ <!-- Node ID -->
284
+ <div class="read-only-input-group">
285
+ <label class="card-label">Node ID</label>
286
+ <div class="input-container" style="position: relative;">
287
+ <div class="read-only-input" id="display-node-id">${shortenAddress(nodeId)}</div>
288
+ <button class="copy-icon-btn" onclick="copyToClipboard('${nodeId}', this)">
289
+ <i class="far fa-copy"></i>
290
+ </button>
291
+ </div>
292
+ </div>
293
+
294
+ <!-- Service Status -->
295
+ <div class="service-status-section">
296
+ <label class="card-label">Service Status</label>
297
+ <div class="status-grid">
298
+ <div class="status-box">
299
+ <div class="status-name">
300
+ <span class="status-dot ${rgbClass}"></span> RGB
301
+ </div>
302
+ <span class="status-value ${rgbClass}">${rgbText}</span>
303
+ </div>
304
+ <div class="status-box">
305
+ <div class="status-name">
306
+ <span class="status-dot ${litdClass}"></span> LITD
307
+ </div>
308
+ <span class="status-value ${litdClass}">${litdText}</span>
309
+ </div>
310
+ </div>
311
+ </div>
312
+
313
+ <!-- Footer -->
314
+ <div class="card-footer-row">
315
+ <div class="regtest-badge">${settings.network || "Regtest"}</div>
316
+ <button class="settings-icon-btn" onclick="openSettingsModal()">
317
+ <i class="fas fa-cog"></i>
318
+ </button>
319
+ </div>
320
+ </div>
321
+
322
+ <button class="manage-btn" onclick="window.location.href='${basicData.manageUrl || "#"}'">
323
+ Manage My Node <i class="fas fa-arrow-right"></i>
324
+ </button>
325
+ </div>
326
+
327
+ <!-- Settings Modal Placeholder -->
328
+ <div id="settings-modal-container"></div>
329
+ `;
330
+
331
+ mainContent.innerHTML = html;
332
+
333
+ // Store data globally for modal use
334
+ window.currentSettings = settings;
335
+ window.currentBasicData = basicData;
336
+ }
337
+
338
+ // Open Settings Modal
339
+ function openSettingsModal() {
340
+ const container = document.getElementById("settings-modal-container");
341
+ const settings = window.currentSettings || {};
342
+ const basicData = window.currentBasicData || {};
343
+
344
+ // Helper to create read-only field
345
+ const createField = (label, value, isPassword = false) => {
346
+ const type = isPassword ? "password" : "text";
347
+ const toggleHtml = isPassword
348
+ ? `<i class="far fa-eye password-toggle-icon" onclick="togglePasswordVisibility(this)"></i>`
349
+ : "";
350
+
351
+ return `
352
+ <div class="settings-field">
353
+ <label>${label}</label>
354
+ <div class="settings-input-group">
355
+ <input type="${type}" class="settings-input" value="${value || ""}" readonly>
356
+ ${toggleHtml}
357
+ </div>
358
+ </div>
359
+ `;
360
+ };
361
+
362
+ const modalHtml = `
363
+ <div class="settings-modal-overlay" onclick="closeSettingsModal(event)">
364
+ <div class="settings-modal" onclick="event.stopPropagation()">
365
+ <div class="settings-header">
366
+ <h3>Settings</h3>
367
+ <button class="close-modal-btn" onclick="closeSettingsModal()"><i class="fas fa-times"></i></button>
368
+ </div>
369
+ <div class="settings-content">
370
+ <!-- Node Information -->
371
+ <div class="settings-section-title">Node Information</div>
372
+ <div class="settings-grid">
373
+ ${createField("Owner Address", basicData.owner)}
374
+ ${createField("Node ID", settings.officialNostrPubKey || basicData.officialNostrPubKey)}
375
+ ${createField("Network Type", settings.network)}
376
+ </div>
377
+
378
+ <!-- Bitcoin Core -->
379
+ <div class="settings-section-title">Bitcoin Core</div>
380
+ <div class="settings-grid">
381
+ ${createField("Bitcoind RPC Host", settings.bitcoindRpcHost)}
382
+ ${createField("Port", settings.bitcoindRpcPort)}
383
+ ${createField("User", settings.bitcoindUser)}
384
+ ${createField("Password", settings.bitcoindPass, true)}
385
+ ${createField("ZMQ Block", settings.bitcoindZmqBlock)}
386
+ ${createField("ZMQ Raw TX", settings.bitcoindZmqRawTx)}
387
+ </div>
388
+
389
+ <!-- LND -->
390
+ <div class="settings-section-title">LND</div>
391
+ <div class="settings-grid">
392
+ ${createField("Peer ID", settings.officialLndPeer)}
393
+ ${createField("Peer Host", settings.officialLndPeerHost)}
394
+ ${createField("Universe Server", settings.officialUniverseServer)}
395
+ ${createField("Price Oracle", settings.priceOracle)}
396
+ </div>
397
+
398
+ <!-- RGB -->
399
+ <div class="settings-section-title">RGB</div>
400
+ <div class="settings-grid">
401
+ ${createField("Peer ID", settings.officialRgbPeer)}
402
+ ${createField("Peer Host", settings.officialRgbPeerHost)}
403
+ ${createField("Proxy", settings.rgbProxy)}
404
+ ${createField("Index", settings.bitcoindIndex)}
405
+ </div>
406
+
407
+ <!-- Nostr -->
408
+ <div class="settings-section-title">Nostr</div>
409
+ <div class="settings-grid">
410
+ ${createField("Nostr Relay", settings.nostrRelays)}
411
+ ${createField("Nostr Private Key", settings.officialNostrPubKey)}
412
+ </div>
413
+ </div>
414
+ </div>
415
+ </div>
416
+ `;
417
+
418
+ container.innerHTML = modalHtml;
419
+ }
420
+
421
+ // Close Settings Modal
422
+ function closeSettingsModal(event) {
423
+ const container = document.getElementById("settings-modal-container");
424
+ container.innerHTML = "";
425
+ }
426
+
427
+ // Toggle Password Visibility
428
+ function togglePasswordVisibility(icon) {
429
+ const input = icon.previousElementSibling;
430
+ if (input.type === "password") {
431
+ input.type = "text";
432
+ icon.classList.remove("fa-eye");
433
+ icon.classList.add("fa-eye-slash");
434
+ } else {
435
+ input.type = "password";
436
+ icon.classList.remove("fa-eye-slash");
437
+ icon.classList.add("fa-eye");
438
+ }
439
+ }
440
+
441
+ // Copy to clipboard function
442
+ function copyToClipboard(text, triggerElement) {
443
+ const showFeedback = () => {
444
+ if (triggerElement) {
445
+ const icon = triggerElement.querySelector("i");
446
+ if (icon) {
447
+ const originalClass = icon.className;
448
+ icon.className = "fas fa-check";
449
+
450
+ setTimeout(() => {
451
+ icon.className = originalClass;
452
+ }, 2000);
453
+ }
454
+ }
455
+ };
456
+
457
+ if (navigator.clipboard && window.isSecureContext) {
458
+ navigator.clipboard.writeText(text).then(
459
+ () => {
460
+ showFeedback();
461
+ },
462
+ (err) => {
463
+ console.error("Could not copy text: ", err);
464
+ },
465
+ );
466
+ } else {
467
+ const textArea = document.createElement("textarea");
468
+ textArea.value = text;
469
+ textArea.style.position = "fixed";
470
+ textArea.style.left = "-9999px";
471
+ document.body.appendChild(textArea);
472
+ textArea.focus();
473
+ textArea.select();
474
+ try {
475
+ document.execCommand("copy");
476
+ showFeedback();
477
+ } catch (err) {
478
+ console.error("Fallback: Oops, unable to copy", err);
479
+ }
480
+ document.body.removeChild(textArea);
481
+ }
482
+ }
483
+
484
+ // Render configuration form (when owner or settings missing)
485
+ function renderConfigurationForm(basicData) {
486
+ const mainContent = document.getElementById("main-content");
487
+
488
+ const html = `
489
+ <div class="header-logo text-center mb-4">
490
+ <img src="img/logo.svg" alt="NodeFlow" height="110">
491
+ </div>
492
+
493
+ <h2 class="text-center text-white mb-2">Node Initial Configuration</h2>
494
+ <p class="text-center text-muted mb-5">You can start the node after viewing and saving the configuration details.</p>
495
+
496
+ <div class="info-section">
497
+ <form class="needs-validation" novalidate id="configForm" autocomplete="off">
498
+ <!-- Owner Configuration -->
499
+ <div class="config-group">
500
+ <label for="owner" class="form-label text-muted">Owner Address</label>
501
+ <div class="input-group">
502
+ <input type="text" class="form-control" id="owner" name="owner" required
503
+ placeholder="Enter owner address" value="${basicData?.owner || ""}" readonly>
504
+ <span class="input-icon copy-btn" onclick="copyToClipboard(document.getElementById('owner').value, this)">
505
+ <i class="far fa-copy"></i>
506
+ </span>
507
+ </div>
508
+ <div class="invalid-feedback">
509
+ Please provide a valid owner address.
510
+ </div>
511
+ </div>
512
+
513
+ <!-- Network Configuration -->
514
+ <div class="config-group network-type mt-4">
515
+ <label class="form-label text-muted">Network Type</label>
516
+ <div class="network-selector d-flex gap-2">
517
+ <button type="button" class="btn btn-network active" data-value="regtest" onclick="selectNetwork('regtest')">Regtest</button>
518
+ <button type="button" class="btn btn-network" data-value="testnet" onclick="selectNetwork('testnet')">Testnet</button>
519
+ <button type="button" class="btn btn-network" data-value="mainnet" onclick="selectNetwork('mainnet')">Mainnet</button>
520
+ </div>
521
+ <input type="hidden" id="network" name="network" value="regtest">
522
+ </div>
523
+
524
+ <!-- Advanced Toggle -->
525
+ <div class="mt-3 mb-3 d-flex align-items-center justify-content-between">
526
+ <a href="javascript:void(0)" class="advanced-toggle text-decoration-none text-lime" onclick="toggleAdvanced()">
527
+ Show Advanced Configuration <i class="fas fa-chevron-down" id="advanced-icon"></i>
528
+ </a>
529
+ <span id="mainnet-badge" class="customizable-badge" style="display: none;">Customizable parameters available.</span>
530
+ </div>
531
+
532
+ <!-- Dynamic Configuration Fields -->
533
+ <div id="config-fields-container" style="display: none;"></div>
534
+
535
+ <div class="row mb-3 mt-4">
536
+ <button type="submit" id="btn_config_submit" class="btn btn-lime w-100 py-2 fw-bold">
537
+ Save and Continue
538
+ </button>
539
+ </div>
540
+ </form>
541
+ </div>
542
+ `;
543
+
544
+ mainContent.innerHTML = html;
545
+
546
+ // Initialize configuration fields
547
+ updateConfigFields();
548
+ }
549
+
550
+ // Toggle Advanced Configuration
551
+ window.toggleAdvanced = function () {
552
+ isAdvancedConfigVisible = !isAdvancedConfigVisible;
553
+ const container = document.getElementById("config-fields-container");
554
+ const icon = document.getElementById("advanced-icon");
555
+
556
+ if (isAdvancedConfigVisible) {
557
+ container.style.display = "block";
558
+ icon.classList.remove("fa-chevron-down");
559
+ icon.classList.add("fa-chevron-up");
560
+ } else {
561
+ container.style.display = "none";
562
+ icon.classList.remove("fa-chevron-up");
563
+ icon.classList.add("fa-chevron-down");
564
+ }
565
+ };
566
+
567
+ // Select Network
568
+ window.selectNetwork = function (network) {
569
+ if (networkConfigs && !networkConfigs[network]) {
570
+ showErrorModal("This network is not supported");
571
+ return;
572
+ }
573
+ document.getElementById("network").value = network;
574
+
575
+ // Update active button state
576
+ document.querySelectorAll(".btn-network").forEach((btn) => {
577
+ if (btn.dataset.value === network) {
578
+ btn.classList.add("active");
579
+ } else {
580
+ btn.classList.remove("active");
581
+ }
582
+ });
583
+
584
+ // Toggle mainnet badge
585
+ const badge = document.getElementById("mainnet-badge");
586
+ if (badge) {
587
+ badge.style.display = network === "mainnet" ? "inline-block" : "none";
588
+ }
589
+
590
+ updateConfigFields();
591
+ };
592
+
593
+ window.copyToClipboard = copyToClipboard;
594
+
595
+ window.togglePassword = function (id) {
596
+ const input = document.getElementById(id);
597
+ if (input.type === "password") {
598
+ input.type = "text";
599
+ } else {
600
+ input.type = "password";
601
+ }
602
+ };
603
+
604
+ // Render basic information list
605
+ function renderBasicInfoList(data) {
606
+ let html = "";
607
+ const excludeKeys = ["settings"];
608
+
609
+ for (const key in data) {
610
+ if (excludeKeys.includes(key)) continue;
611
+
612
+ const value = data[key];
613
+ const status = getStatusClass(value);
614
+
615
+ let valueHtml = `<span class="info-value ${status.class}">
616
+ <i class="${status.icon} status-icon"></i>${value}
617
+ </span>`;
618
+
619
+ if (key === "manageUrl") {
620
+ valueHtml = `<span class="info-value">
621
+ <a href="#" class="info-link" onclick="this.innerHTML='<i class=\\'fas fa-spinner fa-spin status-icon\\'></i>Loading...';setTimeout(()=>{window.location.href='${value}';},100);return false;">
622
+ <i class="fas fa-external-link-alt status-icon"></i>Click to manage
623
+ </a>
624
+ </span>`;
625
+ }
626
+
627
+ html += `
628
+ <li class="list-group-item">
629
+ <div class="info-key">${key}:</div>
630
+ <div class="info-value-container">${valueHtml}</div>
631
+ </li>
632
+ `;
633
+ }
634
+ return html;
635
+ }
636
+
637
+ // Render settings list
638
+ function renderSettingsList(settings) {
639
+ let html = "";
640
+ for (const key in settings) {
641
+ const value = Array.isArray(settings[key])
642
+ ? settings[key].join(", ")
643
+ : settings[key];
644
+ const label = fieldLabels[key] || key;
645
+
646
+ html += `
647
+ <li class="list-group-item">
648
+ <div class="info-key">${label}:</div>
649
+ <div class="info-value-container">
650
+ <span class="info-value">${value}</span>
651
+ </div>
652
+ </li>
653
+ `;
654
+ }
655
+ return html;
656
+ }
657
+
658
+ // Update configuration fields
659
+ function updateConfigFields() {
660
+ const networkInput = document.getElementById("network");
661
+ const container = document.getElementById("config-fields-container");
662
+
663
+ if (!networkInput || !container) return;
664
+
665
+ const selectedNetwork = networkInput.value;
666
+ const config = networkConfigs[selectedNetwork];
667
+
668
+ if (!config) return;
669
+
670
+ let fieldsHtml = "";
671
+
672
+ // Group fields by category
673
+ const groups = {
674
+ "Bitcoin Config": [
675
+ "bitcoindRpcHost",
676
+ "bitcoindRpcPort",
677
+ "bitcoindUser",
678
+ "bitcoindPass",
679
+ "bitcoindZmqBlock",
680
+ "bitcoindZmqRawTx",
681
+ ],
682
+ "LND Config": [
683
+ "officialLndPeer",
684
+ "officialLndPeerHost",
685
+ "officialUniverseServer",
686
+ "priceOracle",
687
+ ],
688
+ "RGB Config": [
689
+ "officialRgbPeer",
690
+ "officialRgbPeerHost",
691
+ "rgbProxy",
692
+ "bitcoindIndex",
693
+ ],
694
+ "Nostr Config": ["officialNostrPubKey", "nostrRelays"],
695
+ };
696
+
697
+ // Define editable fields per network
698
+ let editableFields = [];
699
+ if (selectedNetwork === "regtest") {
700
+ editableFields = ["bitcoindPass"];
701
+ } else if (selectedNetwork === "mainnet") {
702
+ editableFields = [
703
+ "bitcoindRpcHost",
704
+ "bitcoindRpcPort",
705
+ "bitcoindUser",
706
+ "bitcoindPass",
707
+ "bitcoindZmqBlock",
708
+ "bitcoindZmqRawTx",
709
+ "rgbProxy",
710
+ "bitcoindIndex",
711
+ ];
712
+ } else if (selectedNetwork === "testnet") {
713
+ editableFields = ["bitcoindPass"];
714
+ }
715
+
716
+ for (const groupName in groups) {
717
+ fieldsHtml += `<div class="config-section mb-4">`;
718
+ fieldsHtml += `<h6 class="text-lime mb-3">${groupName}</h6>`;
719
+ fieldsHtml += `<div class="row g-3">`;
720
+
721
+ groups[groupName].forEach((key) => {
722
+ if (config[key] !== undefined) {
723
+ const label = fieldLabels[key] || key;
724
+ const value = Array.isArray(config[key])
725
+ ? config[key].join(",")
726
+ : config[key];
727
+
728
+ const isEditable = editableFields.includes(key);
729
+ const isPassword = key.toLowerCase().includes("pass");
730
+ const inputType = isPassword ? "password" : "text";
731
+
732
+ fieldsHtml += `
733
+ <div class="col-md-4">
734
+ <label for="${key}" class="form-label text-muted small">${label}</label>
735
+ <div class="input-group">
736
+ <input type="${inputType}" class="form-control" id="${key}" name="${key}"
737
+ value="${value}" ${isEditable ? "" : "readonly"}>
738
+ ${
739
+ isPassword && isEditable
740
+ ? `
741
+ <span class="input-icon toggle-password" onclick="togglePassword('${key}')">
742
+ <i class="far fa-eye"></i>
743
+ </span>
744
+ `
745
+ : ""
746
+ }
747
+ </div>
748
+ </div>
749
+ `;
750
+ }
751
+ });
752
+
753
+ fieldsHtml += `</div></div>`;
754
+ }
755
+
756
+ container.innerHTML = fieldsHtml;
757
+ }
758
+
759
+ // Handle configuration form submission
760
+ async function handleConfigFormSubmit(event) {
761
+ event.preventDefault();
762
+ const form = document.getElementById("configForm");
763
+ const submitBtn = document.getElementById("btn_config_submit");
764
+
765
+ if (!form.checkValidity()) {
766
+ event.stopPropagation();
767
+ form.classList.add("was-validated");
768
+ return;
769
+ }
770
+
771
+ form.classList.add("was-validated");
772
+ submitBtn.setAttribute("disabled", "disabled");
773
+ // Use the new loading spinner style
774
+ submitBtn.innerHTML = '<div class="loading"></div>Save and Continue';
775
+ const formData = new FormData(form);
776
+ const myHeaders = new Headers();
777
+ myHeaders.append("Content-Type", "application/json");
778
+ // Extract owner
779
+ const owner = formData.get("owner");
780
+
781
+ // Build settings object (exclude owner)
782
+ const settings = {};
783
+ for (const [key, value] of formData.entries()) {
784
+ if (key === "owner") continue;
785
+
786
+ if (key === "nostrRelays") {
787
+ // Handle comma-separated array
788
+ settings[key] = value
789
+ .split(",")
790
+ .map((item) => item.trim())
791
+ .filter((item) => item);
792
+ } else {
793
+ settings[key] = value;
794
+ }
795
+ }
796
+
797
+ const raw = JSON.stringify({
798
+ owner,
799
+ settings,
800
+ });
801
+
802
+ const requestOptions = {
803
+ method: "POST",
804
+ headers: myHeaders,
805
+ body: raw,
806
+ redirect: "follow",
807
+ };
808
+
809
+ try {
810
+ const response = await fetch("/api/lnd/init", requestOptions);
811
+ const result = await response.json();
812
+
813
+ if (result.code === 200) {
814
+ await getInfo(false);
815
+ showSuccessModal("Complete configuration saved successfully!");
816
+ } else {
817
+ showErrorModal(result.message || "Configuration failed");
818
+ }
819
+ } catch (error) {
820
+ console.error("Network error:", error);
821
+ showErrorModal("Network error occurred");
822
+ } finally {
823
+ submitBtn.removeAttribute("disabled");
824
+ submitBtn.innerHTML =
825
+ '<i class="fas fa-save"></i> Save Complete Configuration';
826
+ }
827
+ }
828
+
829
+ // Page initialization
830
+ document.addEventListener("DOMContentLoaded", async () => {
831
+ // Get information
832
+ await getInfo();
833
+
834
+ // Bind form submission events (using event delegation)
835
+ document.addEventListener("submit", (event) => {
836
+ if (event.target.id === "configForm") {
837
+ handleConfigFormSubmit(event);
838
+ }
839
+ });
840
+ });