lnlink-server 1.0.1 → 1.0.3

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