From 696b006668dee104f7d57df7f1d50e875edacd48 Mon Sep 17 00:00:00 2001 From: Abu Date: Thu, 18 Sep 2025 10:14:12 +0100 Subject: [PATCH 1/6] disable --enable-upnp --- neuron/nodes/buyer.js | 1 + neuron/nodes/process-manager.js | 28 ++++++++++++++++------------ neuron/nodes/seller.js | 1 + 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/neuron/nodes/buyer.js b/neuron/nodes/buyer.js index 6664bbde10..6a904b15b2 100644 --- a/neuron/nodes/buyer.js +++ b/neuron/nodes/buyer.js @@ -343,6 +343,7 @@ module.exports = function (RED) { loadedDeviceInfo.deviceType = config.deviceType; loadedDeviceInfo.price = config.price; node.deviceInfo = loadedDeviceInfo; + node.deviceInfo.nodeType = "buyer"; fs.writeFileSync(deviceFile, JSON.stringify(node.deviceInfo, null, 2), 'utf-8'); context.set('deviceInfo', node.deviceInfo); node.status({ fill: "green", shape: "dot", text: "Device loaded." }); diff --git a/neuron/nodes/process-manager.js b/neuron/nodes/process-manager.js index 10791da4d1..4a335d75f2 100644 --- a/neuron/nodes/process-manager.js +++ b/neuron/nodes/process-manager.js @@ -341,7 +341,7 @@ class ProcessManager { const args = [ `--port=${port}`, // '--use-local-address', - '--enable-upnp', + // '--enable-upnp', '--mode=peer', `--buyer-or-seller=${nodeType}`, nodeType === 'seller' ? '--list-of-buyers-source=env' : '--list-of-sellers-source=env', @@ -710,43 +710,47 @@ class ProcessManager { async testWebSocketConnection(port, maxRetries = 10, nodeType = 'buyer') { const retryDelayMs = 1000; const endpoint = nodeType === 'seller' ? '/seller/p2p' : '/buyer/p2p'; - + for (let attempt = 1; attempt <= maxRetries; attempt++) { try { + console.log(`Attempting WebSocket connection to ws://localhost:${port}${endpoint} (attempt ${attempt}/${maxRetries})`); const ws = new WebSocket(`ws://localhost:${port}${endpoint}`); - + const result = await new Promise((resolve) => { const timeout = setTimeout(() => { + console.log(`WebSocket connection timed out on port ${port}`); ws.close(); resolve(false); }, 2000); - + ws.on('open', () => { + console.log(`WebSocket connection opened successfully on port ${port}`); clearTimeout(timeout); ws.close(); resolve(true); }); - - ws.on('error', () => { + + ws.on('error', (error) => { + console.error(`WebSocket error on port ${port}:`, error.message); clearTimeout(timeout); resolve(false); }); }); - + if (result) { console.log(`WebSocket connection successful on port ${port} (attempt ${attempt})`); return true; } } catch (error) { - // Connection failed, continue to retry + console.error(`WebSocket connection attempt failed on port ${port} (attempt ${attempt}):`, error.message); } - + if (attempt < maxRetries) { - console.log(`WebSocket connection failed on port ${port}, retrying in ${retryDelayMs}ms (attempt ${attempt}/${maxRetries})`); + console.log(`Retrying WebSocket connection to port ${port} in ${retryDelayMs}ms (attempt ${attempt + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, retryDelayMs)); } } - + console.log(`WebSocket connection failed on port ${port} after ${maxRetries} attempts`); return false; } @@ -1111,4 +1115,4 @@ module.exports.killPid = async function(pid, signal = 'SIGTERM') { module.exports.listProcesses = function() { const manager = new ProcessManager(); return manager.listAllProcesses(); -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/neuron/nodes/seller.js b/neuron/nodes/seller.js index 19b4ee9c43..9b0bc40cd7 100644 --- a/neuron/nodes/seller.js +++ b/neuron/nodes/seller.js @@ -338,6 +338,7 @@ module.exports = function (RED) { loadedDeviceInfo.deviceType = config.deviceType; loadedDeviceInfo.price = config.price; node.deviceInfo = loadedDeviceInfo; + node.deviceInfo.nodeType = "seller"; fs.writeFileSync(deviceFile, JSON.stringify(node.deviceInfo, null, 2), 'utf-8'); context.set('deviceInfo', node.deviceInfo); node.status({ fill: "green", shape: "dot", text: "Device loaded." }); From 610407ece296b9ed2819cbc7b370cbb9524bd3a5 Mon Sep 17 00:00:00 2001 From: Abu Date: Thu, 18 Sep 2025 11:40:48 +0100 Subject: [PATCH 2/6] bug fix - health monitor reporting --- neuron/services/HealthMonitor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neuron/services/HealthMonitor.js b/neuron/services/HealthMonitor.js index 4ac3477fc3..fdc6ce0b40 100644 --- a/neuron/services/HealthMonitor.js +++ b/neuron/services/HealthMonitor.js @@ -76,10 +76,10 @@ async function checkMirrorNode() { const url = `http://localhost:1880/buyer/last-seen/${topic}`; try { const result = await fetchJson(url, 2000); - if (result && result.lastSeenFormatted && result.lastSeenFormatted !== 'Never') { + if (result && result.lastSeen && result.lastSeen !== 'Never') { return { ok: true, lastSeen: result.lastSeenFormatted }; } else { - return { ok: false, error: 'No recent message', lastSeen: result.lastSeenFormatted }; + return { ok: false, error: 'No recent message', lastSeen: result.lastSeen }; } } catch (e) { return { ok: false, error: e.message }; From 885a0382b81df2f839cf5a25b3e6e319d4f05c3e Mon Sep 17 00:00:00 2001 From: Abu Date: Sat, 20 Sep 2025 11:09:00 +0100 Subject: [PATCH 3/6] node reinstatement --- .gitignore | 1 + neuron/lib/deviceManager.js | 245 ++++++++++++++ neuron/nodes/buyer.html | 550 +++++++++++++++++++++++++------- neuron/nodes/buyer.js | 220 ++++++++++++- neuron/nodes/process-manager.js | 2 +- neuron/nodes/seller.html | 442 +++++++++++++++++++++---- neuron/nodes/seller.js | 194 ++++++++++- 7 files changed, 1455 insertions(+), 199 deletions(-) create mode 100644 neuron/lib/deviceManager.js diff --git a/.gitignore b/.gitignore index 4c7f38c077..97414c9429 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,4 @@ build/releases/ # Temporary files *.log .DS_Store +.cursor/rules/directive.mdc diff --git a/neuron/lib/deviceManager.js b/neuron/lib/deviceManager.js new file mode 100644 index 0000000000..8ce5245767 --- /dev/null +++ b/neuron/lib/deviceManager.js @@ -0,0 +1,245 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Central utility module for managing device files + * This module serves as the single source of truth for all filesystem operations + * related to device files in the Neuron system. + */ +class DeviceManager { + constructor() { + this.devicesDir = path.join(require('../services/NeuronUserHome').load(), 'devices'); + this.ensureDevicesDirectory(); + } + + /** + * Ensure the devices directory exists + */ + ensureDevicesDirectory() { + if (!fs.existsSync(this.devicesDir)) { + fs.mkdirSync(this.devicesDir, { recursive: true }); + } + } + + /** + * List all JSON files in the devices directory + * @returns {Array} Array of filenames (without .json extension) + */ + listAllDeviceFiles() { + try { + if (!fs.existsSync(this.devicesDir)) { + return []; + } + + const files = fs.readdirSync(this.devicesDir); + return files + .filter(file => file.endsWith('.json')) + .map(file => file.replace('.json', '')); + } catch (error) { + console.error('Error listing device files:', error); + return []; + } + } + + /** + * Determine the role of a device based on its content + * @param {Object} deviceData - The parsed device data + * @returns {string} 'buyer' or 'seller' + */ + determineDeviceRole(deviceData) { + // Explicit nodeType property takes precedence + if (deviceData.nodeType === 'buyer') { + return 'buyer'; + } + if (deviceData.nodeType === 'seller') { + return 'seller'; + } + + // Fallback logic: check for sellerEvmAddress property + // A file is a Buyer if it contains sellerEvmAddress property + if (deviceData.sellerEvmAddress !== undefined) { + return 'buyer'; + } + + // Default to seller if no sellerEvmAddress property + return 'seller'; + } + + /** + * List device files filtered by role and excluding active nodes + * @param {string} role - 'buyer' or 'seller' + * @param {Array} activeNodeIds - Array of currently active node IDs to exclude + * @returns {Array} Array of eligible device objects with filename and role + */ + listEligibleDevices(role, activeNodeIds = []) { + try { + const allFiles = this.listAllDeviceFiles(); + const eligibleDevices = []; + + for (const filename of allFiles) { + // Skip if this file corresponds to an active node (check filename first) + if (activeNodeIds.includes(filename)) { + console.log(`[DEVICE FILTER] Skipping ${filename}.json - filename matches active node ID`); + continue; + } + + try { + const deviceData = this.loadDeviceFile(filename); + + // Also check if the nodeId field inside the JSON matches an active node + if (deviceData.nodeId && activeNodeIds.includes(deviceData.nodeId)) { + console.log(`[DEVICE FILTER] Skipping ${filename}.json - nodeId field (${deviceData.nodeId}) matches active node ID`); + continue; + } + + const deviceRole = this.determineDeviceRole(deviceData); + + // Only include devices that match the requested role + if (deviceRole === role) { + eligibleDevices.push({ + filename: filename, + role: deviceRole, + deviceName: deviceData.deviceName || 'Unnamed Device', + evmAddress: deviceData.evmAddress || 'No EVM Address', + nodeId: deviceData.nodeId || null // Include the nodeId field for reference + }); + } + } catch (error) { + console.error(`Error processing device file ${filename}:`, error); + // Continue processing other files even if one fails + } + } + + return eligibleDevices; + } catch (error) { + console.error('Error listing eligible devices:', error); + return []; + } + } + + /** + * Load and parse a device file + * @param {string} filename - The filename without .json extension + * @returns {Object} Parsed device data + */ + loadDeviceFile(filename) { + try { + const filePath = path.join(this.devicesDir, `${filename}.json`); + + if (!fs.existsSync(filePath)) { + throw new Error(`Device file not found: ${filename}.json`); + } + + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const deviceData = JSON.parse(fileContent); + + return deviceData; + } catch (error) { + console.error(`Error loading device file ${filename}:`, error); + throw error; + } + } + + /** + * Save device data to a file + * @param {string} nodeId - The node ID (used as filename) + * @param {Object} deviceData - The device data to save + */ + saveDeviceFile(nodeId, deviceData) { + try { + this.ensureDevicesDirectory(); + const filePath = path.join(this.devicesDir, `${nodeId}.json`); + + fs.writeFileSync(filePath, JSON.stringify(deviceData, null, 2), 'utf-8'); + console.log(`Device file saved: ${nodeId}.json`); + } catch (error) { + console.error(`Error saving device file ${nodeId}:`, error); + throw error; + } + } + + /** + * Rename a device file + * @param {string} oldFilename - Current filename (without .json extension) + * @param {string} newNodeId - New node ID (new filename without .json extension) + */ + renameDeviceFile(oldFilename, newNodeId) { + try { + const oldFilePath = path.join(this.devicesDir, `${oldFilename}.json`); + const newFilePath = path.join(this.devicesDir, `${newNodeId}.json`); + + if (!fs.existsSync(oldFilePath)) { + throw new Error(`Device file not found: ${oldFilename}.json`); + } + + if (fs.existsSync(newFilePath)) { + throw new Error(`Target device file already exists: ${newNodeId}.json`); + } + + fs.renameSync(oldFilePath, newFilePath); + console.log(`Device file renamed: ${oldFilename}.json -> ${newNodeId}.json`); + } catch (error) { + console.error(`Error renaming device file from ${oldFilename} to ${newNodeId}:`, error); + throw error; + } + } + + /** + * Check if a device file exists + * @param {string} nodeId - The node ID to check + * @returns {boolean} True if file exists + */ + deviceFileExists(nodeId) { + const filePath = path.join(this.devicesDir, `${nodeId}.json`); + return fs.existsSync(filePath); + } + + /** + * Delete a device file + * @param {string} nodeId - The node ID (filename without .json extension) + */ + deleteDeviceFile(nodeId) { + try { + const filePath = path.join(this.devicesDir, `${nodeId}.json`); + + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + console.log(`Device file deleted: ${nodeId}.json`); + } + } catch (error) { + console.error(`Error deleting device file ${nodeId}:`, error); + throw error; + } + } + + /** + * Get all active node IDs from the current Node-RED editor + * This is a helper method that can be called by the nodes to get active IDs + * @param {Object} RED - Node-RED instance + * @returns {Array} Array of active node IDs + */ + getActiveNodeIds(RED) { + try { + const activeNodeIds = []; + + // Get all nodes from the editor + RED.nodes.eachNode((node) => { + // Check for both buyer and seller config nodes + if (node.type === 'buyer config' || node.type === 'seller config' || + node.type === 'buyer' || node.type === 'seller') { + activeNodeIds.push(node.id); + console.log(`[ACTIVE NODES] Found active node: ${node.type} (${node.id})`); + } + }); + + console.log(`[ACTIVE NODES] Total active nodes found: ${activeNodeIds.length}`); + return activeNodeIds; + } catch (error) { + console.error('Error getting active node IDs:', error); + return []; + } + } +} + +// Export a singleton instance +module.exports = new DeviceManager(); diff --git a/neuron/nodes/buyer.html b/neuron/nodes/buyer.html index 839040130f..a07b1b73a8 100644 --- a/neuron/nodes/buyer.html +++ b/neuron/nodes/buyer.html @@ -17,10 +17,29 @@ +
+ + + +
+ Select an existing device to reinstantiate its configuration +
+
+ + + +
+ + + Sellers I want data from (Should be in selected Smart Contract above )
@@ -187,7 +206,9 @@

Runtime Information

// Add these three new fields stdInTopic: { value: "" }, stdOutTopic: { value: "" }, - stdErrTopic: { value: "" } + stdErrTopic: { value: "" }, + // Device reinstatement field + loadedDeviceFilename: { value: "" } }, inputs: 0, outputs: 1, @@ -214,6 +235,199 @@

Runtime Information

} }; + // ===== DEVICE REINSTATEMENT FUNCTIONALITY ===== + + // Load eligible devices into dropdown + async function loadEligibleDevices() { + try { + const response = await fetch('/buyer/devices/eligible'); + const data = await response.json(); + + const dropdown = $('#node-input-loadDevice'); + const currentValue = dropdown.val(); + + // Clear existing options except the first one + dropdown.find('option:not(:first)').remove(); + + if (data.success && data.devices.length > 0) { + data.devices.forEach(device => { + const option = $(``); + dropdown.append(option); + }); + console.log(`[BUYER DEVICE LOAD] Loaded ${data.devices.length} eligible devices`); + } else { + console.log('[BUYER DEVICE LOAD] No eligible devices found'); + } + + // Restore previous selection if it still exists + if (currentValue && dropdown.find(`option[value="${currentValue}"]`).length > 0) { + dropdown.val(currentValue); + } + + } catch (error) { + console.error('[BUYER DEVICE LOAD] Error loading eligible devices:', error); + } + } + + // Load device configuration when selected + async function loadDeviceConfiguration(filename) { + if (!filename) { + console.log('[BUYER DEVICE LOAD] No device selected, keeping current configuration'); + return; + } + + try { + const response = await fetch(`/device/${filename}`); + const data = await response.json(); + + if (data.success && data.data) { + const deviceData = data.data; + console.log('[BUYER DEVICE LOAD] Loading device configuration:', filename); + + // Pre-fill form fields with loaded device data + if (deviceData.deviceType) $('#node-input-deviceType').val(deviceData.deviceType); + + // ===== POPULATE RUNTIME INFORMATION ===== + + // Show the runtime information section and hide the "no device" message + $('#runtime-information-section').show(); + $('#no-device-message').hide(); + + // Populate EVM Address + if (deviceData.evmAddress) { + $('#node-input-evmAddress').val(deviceData.evmAddress); + } + + // Populate Public Key (fetch fresh from EVM address) + if (deviceData.evmAddress) { + try { + const publicKeyResponse = await fetch(`/buyer/evm-to-publickey/${deviceData.evmAddress}`); + const publicKeyData = await publicKeyResponse.json(); + + if (publicKeyData.success && publicKeyData.publicKey) { + $('#node-input-publicKey').val(publicKeyData.publicKey); + console.log('[BUYER DEVICE LOAD] Fetched fresh public key from EVM address'); + } else if (deviceData.publicKey) { + // Fallback to stored public key if fetch fails + $('#node-input-publicKey').val(deviceData.publicKey); + console.log('[BUYER DEVICE LOAD] Using stored public key (fetch failed)'); + } + } catch (error) { + console.error('[BUYER DEVICE LOAD] Error fetching public key:', error); + if (deviceData.publicKey) { + $('#node-input-publicKey').val(deviceData.publicKey); + } + } + } else if (deviceData.publicKey) { + $('#node-input-publicKey').val(deviceData.publicKey); + } + + // Populate Topics from topics array + if (deviceData.topics && Array.isArray(deviceData.topics)) { + // Map topics array to individual fields (typically [stdIn, stdOut, stdErr]) + if (deviceData.topics.length >= 3) { + $('#node-input-stdInTopic').val(deviceData.topics[0] || ''); + $('#node-input-stdOutTopic').val(deviceData.topics[1] || ''); + $('#node-input-stdErrTopic').val(deviceData.topics[2] || ''); + } else if (deviceData.topics.length >= 1) { + // If only one topic, assume it's stdOut + $('#node-input-stdOutTopic').val(deviceData.topics[0]); + } + } + + // Populate individual topic fields if they exist + if (deviceData.stdInTopic) $('#node-input-stdInTopic').val(deviceData.stdInTopic); + if (deviceData.stdOutTopic) $('#node-input-stdOutTopic').val(deviceData.stdOutTopic); + if (deviceData.stdErrTopic) $('#node-input-stdErrTopic').val(deviceData.stdErrTopic); + + // Fetch and populate balance using accountId from device data + if (deviceData.accountId) { + try { + const balanceResponse = await fetch(`/device/balance/by-account/${deviceData.accountId}`); + const balanceData = await balanceResponse.json(); + + if (balanceData.success && balanceData.balance) { + $('#node-input-balance').val(`${balanceData.balance} ${balanceData.unit}`); + console.log('[BUYER DEVICE LOAD] Fetched current balance:', balanceData.balance, balanceData.unit); + } else { + $('#node-input-balance').val('Balance not available'); + console.log('[BUYER DEVICE LOAD] Balance fetch failed:', balanceData.error); + } + } catch (error) { + console.error('[BUYER DEVICE LOAD] Error fetching balance:', error); + $('#node-input-balance').val('Balance fetch error'); + } + } else if (deviceData.evmAddress) { + // Fallback to EVM address method if accountId is not available + try { + const balanceResponse = await fetch(`/device/balance/${deviceData.evmAddress}`); + const balanceData = await balanceResponse.json(); + + if (balanceData.success && balanceData.balance) { + $('#node-input-balance').val(`${balanceData.balance} ${balanceData.unit}`); + console.log('[BUYER DEVICE LOAD] Fetched current balance via EVM:', balanceData.balance, balanceData.unit); + } else { + $('#node-input-balance').val('Balance not available'); + console.log('[BUYER DEVICE LOAD] Balance fetch failed:', balanceData.error); + } + } catch (error) { + console.error('[BUYER DEVICE LOAD] Error fetching balance:', error); + $('#node-input-balance').val('Balance fetch error'); + } + } + + // Update seller devices list if present + if (deviceData.sellerEvmAddress) { + try { + const sellerAddresses = JSON.parse(deviceData.sellerEvmAddress); + if (Array.isArray(sellerAddresses) && sellerAddresses.length > 0) { + node.devicesList = sellerAddresses.map(addr => ({ + evmAddress: addr, + deviceName: 'Loaded Device', + deviceType: 'Loaded', + stdInTopic: 'Unknown', + stdOutTopic: 'Unknown', + stdErrTopic: 'Unknown', + status: 'Unknown', + lastSeen: 'Unknown' + })); + updateDevicesTable(); + console.log('[BUYER DEVICE LOAD] Loaded seller addresses:', sellerAddresses); + } + } catch (parseError) { + console.error('[BUYER DEVICE LOAD] Error parsing seller addresses:', parseError); + } + } + + // Store the loaded device filename for later use + node.loadedDeviceFilename = filename; + $('#node-input-loadedDeviceFilename').val(filename); + + console.log('[BUYER DEVICE LOAD] Device configuration and runtime information loaded successfully'); + } else { + console.error('[BUYER DEVICE LOAD] Failed to load device data:', data.error); + } + + } catch (error) { + console.error('[BUYER DEVICE LOAD] Error loading device configuration:', error); + } + } + + // Event handlers for device loading + $('#refresh-devices-btn').on('click', function () { + console.log('[BUYER DEVICE LOAD] Refreshing device list...'); + loadEligibleDevices(); + }); + + $('#node-input-loadDevice').on('change', function () { + const selectedFilename = $(this).val(); + console.log('[BUYER DEVICE LOAD] Device selection changed:', selectedFilename); + loadDeviceConfiguration(selectedFilename); + }); + + // Initialize device dropdown on dialog open + loadEligibleDevices(); + console.log('[SCOPE TEST] node defined at start:', typeof node); // Initialize devices from existing configuration @@ -336,6 +550,27 @@

Runtime Information

} try { + // First check if the node is deployed (has a device file) + const existsResponse = await fetch(`/buyer/device-exists/${node.id}`); + if (!existsResponse.ok) { + // Node not deployed, show not initialized + for (let index = 0; index < node.devicesList.length; index++) { + $(`#status-cell-${index}`).html('Not deployed'); + $(`#lastseen-cell-${index}`).html('Not deployed'); + } + return; + } + + const existsData = await existsResponse.json(); + if (!existsData.success || !existsData.exists) { + // Device file doesn't exist, node not deployed + for (let index = 0; index < node.devicesList.length; index++) { + $(`#status-cell-${index}`).html('Not deployed'); + $(`#lastseen-cell-${index}`).html('Not deployed'); + } + return; + } + // Get connection status for this buyer node const connectionResponse = await fetch(`/buyer/connection-status/${node.id}`); if (!connectionResponse.ok) { @@ -572,51 +807,73 @@

Runtime Information

// Fetch and populate EVM address, publicKey, and topics if available function updateEvmAddressAndPublicKeyAndTopics() { if (node.id) { - // console.log(`Fetching device info for node ID: ${node.id}`); - $.get(`/buyer/device-info/${node.id}`) - .done(function (data) { - // console.log(`Device info received for ${node.id}:`, data); - - // Handle EVM Address - if (data.evmAddress) { - $('#node-input-evmAddress').val(data.evmAddress); - } else { - $('#node-input-evmAddress').val('Not initialized yet'); - } - - // Handle publicKey - if (data.publicKey) { - $('#node-input-publicKey').val(data.publicKey); - } else { - $('#node-input-publicKey').val('Not initialized yet'); - } - - // Handle Topics - if (data.stdInTopic) { - $('#node-input-stdInTopic').val(data.stdInTopic); - } else { - $('#node-input-stdInTopic').val('Not initialized yet'); - } - - if (data.stdOutTopic) { - $('#node-input-stdOutTopic').val(data.stdOutTopic); - } else { - $('#node-input-stdOutTopic').val('Not initialized yet'); - } - - if (data.stdErrTopic) { - $('#node-input-stdErrTopic').val(data.stdErrTopic); + // First check if device file exists before making API calls + $.get(`/buyer/device-exists/${node.id}`) + .done(function (existsData) { + if (existsData.success && existsData.exists) { + // Device file exists, fetch device info + // console.log(`Fetching device info for node ID: ${node.id}`); + $.get(`/buyer/device-info/${node.id}`) + .done(function (data) { + // console.log(`Device info received for ${node.id}:`, data); + + // Handle EVM Address + if (data.evmAddress) { + $('#node-input-evmAddress').val(data.evmAddress); + } else { + $('#node-input-evmAddress').val('Not initialized yet'); + } + + // Handle publicKey + if (data.publicKey) { + $('#node-input-publicKey').val(data.publicKey); + } else { + $('#node-input-publicKey').val('Not initialized yet'); + } + + // Handle Topics + if (data.stdInTopic) { + $('#node-input-stdInTopic').val(data.stdInTopic); + } else { + $('#node-input-stdInTopic').val('Not initialized yet'); + } + + if (data.stdOutTopic) { + $('#node-input-stdOutTopic').val(data.stdOutTopic); + } else { + $('#node-input-stdOutTopic').val('Not initialized yet'); + } + + if (data.stdErrTopic) { + $('#node-input-stdErrTopic').val(data.stdErrTopic); + } else { + $('#node-input-stdErrTopic').val('Not initialized yet'); + } + }) + .fail(function (xhr, status, error) { + // console.error(`Failed to fetch device info for ${node.id}:`, error); + $('#node-input-evmAddress').val('EVM address not intialised'); + $('#node-input-publicKey').val('Public key not intialised'); + $('#node-input-stdInTopic').val('StdIn topic not intialised'); + $('#node-input-stdOutTopic').val('StdOut topic not intialised'); + $('#node-input-stdErrTopic').val('StdErr topic not intialised'); + }); } else { - $('#node-input-stdErrTopic').val('Not initialized yet'); + // Device file doesn't exist, show new node messages + $('#node-input-evmAddress').val('New node - EVM address will be generated after deployment'); + $('#node-input-publicKey').val('New node - Public key will be generated after deployment'); + $('#node-input-stdInTopic').val('New node - StdIn topic will be generated after deployment'); + $('#node-input-stdOutTopic').val('New node - StdOut topic will be generated after deployment'); + $('#node-input-stdErrTopic').val('New node - StdErr topic will be generated after deployment'); } }) - .fail(function (xhr, status, error) { - // console.error(`Failed to fetch device info for ${node.id}:`, error); - $('#node-input-evmAddress').val('EVM address not intialised'); - $('#node-input-publicKey').val('Public key not intialised'); - $('#node-input-stdInTopic').val('StdIn topic not intialised'); - $('#node-input-stdOutTopic').val('StdOut topic not intialised'); - $('#node-input-stdErrTopic').val('StdErr topic not intialised'); + .fail(function () { + // If device-exists check fails, assume new node + $('#node-input-evmAddress').val('New node - EVM address will be generated after deployment'); + $('#node-input-publicKey').val('New node - Public key will be generated after deployment'); + $('#node-input-stdInTopic').val('New node - StdIn topic will be generated after deployment'); + $('#node-input-stdOutTopic').val('New node - StdOut topic will be generated after deployment'); + $('#node-input-stdErrTopic').val('New node - StdErr topic will be generated after deployment'); }); } else { $('#node-input-evmAddress').val('New node - EVM address will be generated after deployment'); @@ -630,16 +887,30 @@

Runtime Information

// Fetch and populate device balance if available function updateDeviceBalance() { if (node.id) { - $.get(`/buyer/device-balance/${node.id}`) - .done(function (data) { - if (data.success && data.balance) { - $('#node-input-balance').val(data.balance + ' USDC'); + // First check if device file exists before making API calls + $.get(`/buyer/device-exists/${node.id}`) + .done(function (existsData) { + if (existsData.success && existsData.exists) { + // Device file exists, fetch balance + $.get(`/buyer/device-balance/${node.id}`) + .done(function (data) { + if (data.success && data.balance) { + $('#node-input-balance').val(data.balance + ' USDC'); + } else { + $('#node-input-balance').val('Not initialized yet'); + } + }) + .fail(function () { + $('#node-input-balance').val('Error loading balance'); + }); } else { - $('#node-input-balance').val('Not initialized yet'); + // Device file doesn't exist, show new node message + $('#node-input-balance').val('New node - balance will be available after deployment'); } }) .fail(function () { - $('#node-input-balance').val('Error loading balance'); + // If device-exists check fails, assume new node + $('#node-input-balance').val('New node - balance will be available after deployment'); }); } else { $('#node-input-balance').val('New node - balance will be available after deployment'); @@ -653,7 +924,7 @@

Runtime Information

$.get(`/buyer/device-exists/${node.id}`) .done(function (data) { console.log(`[DEVICE EXISTS] Response for ${node.id}:`, data); - + if (data.success && data.exists) { // Device file exists, show runtime information section and hide message $('#runtime-information-section').show(); @@ -1123,82 +1394,113 @@

Runtime Information

diff --git a/neuron/nodes/buyer.js b/neuron/nodes/buyer.js index 6a904b15b2..e5b40385dc 100644 --- a/neuron/nodes/buyer.js +++ b/neuron/nodes/buyer.js @@ -317,11 +317,33 @@ module.exports = function (RED) { (async () => { let loadedDeviceInfo = null; + let isDeviceReinstatement = false; - const contextDevice = context.get('deviceInfo'); - if (contextDevice) { - loadedDeviceInfo = contextDevice; - console.log(`Node ${node.id}: Device loaded from context.`); + // Check if a device was loaded from the dropdown (device reinstatement) + if (config.loadedDeviceFilename && config.loadedDeviceFilename.trim() !== '') { + try { + const deviceManager = require('../lib/deviceManager'); + loadedDeviceInfo = deviceManager.loadDeviceFile(config.loadedDeviceFilename); + isDeviceReinstatement = true; + console.log(`Node ${node.id}: Device reinstatement - loaded device from: ${config.loadedDeviceFilename}`); + + // Rename the device file to match the new node ID if needed + if (config.loadedDeviceFilename !== `${node.id}.json`) { + console.log(`Node ${node.id}: Renaming device file from ${config.loadedDeviceFilename} to ${node.id}.json`); + deviceManager.renameDeviceFile(config.loadedDeviceFilename, node.id); + } + } catch (err) { + node.warn(`Node ${node.id}: Failed to load device from filename ${config.loadedDeviceFilename}. Error: ${err.message}`); + } + } + + // Fallback to existing logic for context and disk loading + if (!loadedDeviceInfo) { + const contextDevice = context.get('deviceInfo'); + if (contextDevice) { + loadedDeviceInfo = contextDevice; + console.log(`Node ${node.id}: Device loaded from context.`); + } } if (!loadedDeviceInfo && fs.existsSync(deviceFile)) { @@ -335,6 +357,7 @@ module.exports = function (RED) { } if (loadedDeviceInfo) { + // Update configuration fields while preserving existing Hedera account data loadedDeviceInfo.sellerEvmAddress = config.sellerEvmAddress; loadedDeviceInfo.description = config.description; loadedDeviceInfo.deviceName = config.deviceName; @@ -342,11 +365,29 @@ module.exports = function (RED) { loadedDeviceInfo.serialNumber = config.serialNumber; loadedDeviceInfo.deviceType = config.deviceType; loadedDeviceInfo.price = config.price; + + // Set smart contract from configuration + const contracts = { + "jetvision": process.env.JETVISION_CONTRACT_EVM, + "chat": process.env.CHAT_CONTRACT_EVM, + "challenges": process.env.CHALLENGES_CONTRACT_EVM, + }; + loadedDeviceInfo.smartContract = contracts[config.smartContract.toLowerCase()]; + node.deviceInfo = loadedDeviceInfo; node.deviceInfo.nodeType = "buyer"; + node.deviceInfo.nodeId = node.id; + + // Save the updated device info fs.writeFileSync(deviceFile, JSON.stringify(node.deviceInfo, null, 2), 'utf-8'); context.set('deviceInfo', node.deviceInfo); - node.status({ fill: "green", shape: "dot", text: "Device loaded." }); + + if (isDeviceReinstatement) { + node.status({ fill: "green", shape: "dot", text: "Device reinstated." }); + console.log(`Node ${node.id}: Device reinstatement completed - using existing Hedera account and topics`); + } else { + node.status({ fill: "green", shape: "dot", text: "Device loaded." }); + } } if (!node.deviceInfo) { @@ -1256,7 +1297,7 @@ module.exports = function (RED) { RED.httpAdmin.get('/buyer/last-seen/:topicId', async function (req, res) { const topicId = req.params.topicId; - console.log(`[DEBUG] Last seen requested for topic: ${topicId}`); + // console.log(`[DEBUG] Last seen requested for topic: ${topicId}`); try { if (!hederaService) { @@ -1275,7 +1316,7 @@ module.exports = function (RED) { // Handle timestamp format: '1753899626.468846000' // This is Unix timestamp in seconds with nanosecond precision const timestampString = lastMessage.timestamp; - console.log(`[DEBUG] Raw timestamp: ${timestampString}`); + // console.log(`[DEBUG] Raw timestamp: ${timestampString}`); // Parse the timestamp string as a float (seconds.nanoseconds) const timestampSeconds = parseFloat(timestampString); @@ -1290,7 +1331,7 @@ module.exports = function (RED) { const millisecondsAgo = now - lastSeenTime; const secondsAgo = Math.floor(millisecondsAgo / 1000); - console.log(`[DEBUG] Timestamp: ${timestampString}, LastSeenTime: ${lastSeenTime}ms, Now: ${now}ms, SecondsAgo: ${secondsAgo}`); + // console.log(`[DEBUG] Timestamp: ${timestampString}, LastSeenTime: ${lastSeenTime}ms, Now: ${now}ms, SecondsAgo: ${secondsAgo}`); res.json({ success: true, @@ -1403,4 +1444,167 @@ module.exports = function (RED) { }); } }); + + // ===== NEW DEVICE MANAGEMENT ENDPOINTS ===== + + // Import deviceManager for new endpoints + const deviceManager = require('../lib/deviceManager'); + + // Endpoint for Buyer Device List - Get eligible buyer devices for reinstatement + RED.httpAdmin.get('/buyer/devices/eligible', function (req, res) { + try { + // Get currently active node IDs to exclude from the list + const activeNodeIds = deviceManager.getActiveNodeIds(RED); + + // Get eligible buyer devices + const eligibleDevices = deviceManager.listEligibleDevices('buyer', activeNodeIds); + + console.log(`[BUYER DEVICES] Found ${eligibleDevices.length} eligible buyer devices (excluding ${activeNodeIds.length} active nodes)`); + + res.json({ + success: true, + devices: eligibleDevices, + activeNodeIds: activeNodeIds, + count: eligibleDevices.length + }); + + } catch (error) { + console.error('[BUYER DEVICES] Error fetching eligible buyer devices:', error); + res.status(500).json({ + success: false, + error: `Failed to fetch eligible buyer devices: ${error.message}` + }); + } + }); + + // Endpoint for Device File Content - Get complete device data by filename + RED.httpAdmin.get('/device/:filename', function (req, res) { + const filename = req.params.filename; + + try { + const deviceData = deviceManager.loadDeviceFile(filename); + + console.log(`[DEVICE LOAD] Successfully loaded device file: ${filename}.json`); + + res.json({ + success: true, + filename: filename, + data: deviceData + }); + + } catch (error) { + console.error(`[DEVICE LOAD] Error loading device file ${filename}:`, error); + res.status(500).json({ + success: false, + error: `Failed to load device file: ${error.message}` + }); + } + }); + + // Endpoint to get balance by account ID (efficient direct access) + RED.httpAdmin.get('/device/balance/by-account/:accountId', async function (req, res) { + const accountId = req.params.accountId; + + try { + if (!hederaService) { + return res.status(500).json({ + success: false, + error: 'Hedera service not initialized' + }); + } + + console.log(`[DEVICE BALANCE] Fetching balance for account ID: ${accountId}`); + + // Get balance in tinybars directly using account ID + const balanceTinybars = await hederaService.getAccountBalanceTinybars(accountId); + + // Convert to HBAR (1 HBAR = 100,000,000 tinybars) + const balanceHbar = (balanceTinybars / 100000000).toFixed(8); + + console.log(`[DEVICE BALANCE] Balance for ${accountId}: ${balanceHbar} HBAR`); + + res.json({ + success: true, + balance: balanceHbar, + balanceTinybars: balanceTinybars.toString(), + accountId: accountId, + unit: 'HBAR' + }); + + } catch (error) { + console.error(`[DEVICE BALANCE] Error fetching balance for ${accountId}:`, error); + res.status(500).json({ + success: false, + error: `Failed to fetch balance: ${error.message}` + }); + } + }); + + // Endpoint to get balance by EVM address (fallback method) + RED.httpAdmin.get('/device/balance/:evmAddress', async function (req, res) { + const evmAddress = req.params.evmAddress; + + try { + if (!hederaService) { + return res.status(500).json({ + success: false, + error: 'Hedera service not initialized' + }); + } + + console.log(`[DEVICE BALANCE] Fetching balance for EVM address: ${evmAddress}`); + + // Find the device file that contains this EVM address + const deviceManager = require('../lib/deviceManager'); + const allFiles = deviceManager.listAllDeviceFiles(); + let accountId = null; + let deviceData = null; + + // Search through device files to find the one with matching EVM address + for (const filename of allFiles) { + try { + const data = deviceManager.loadDeviceFile(filename); + if (data.evmAddress && data.evmAddress.toLowerCase() === evmAddress.toLowerCase()) { + accountId = data.accountId; + deviceData = data; + break; + } + } catch (error) { + console.error(`[DEVICE BALANCE] Error loading device file ${filename}:`, error); + continue; + } + } + + if (!accountId) { + return res.status(404).json({ + success: false, + error: 'Device not found for EVM address' + }); + } + + // Get balance in tinybars + const balanceTinybars = await hederaService.getAccountBalanceTinybars(accountId); + + // Convert to HBAR (1 HBAR = 100,000,000 tinybars) + const balanceHbar = (balanceTinybars / 100000000).toFixed(8); + + console.log(`[DEVICE BALANCE] Balance for ${evmAddress} (${accountId}): ${balanceHbar} HBAR`); + + res.json({ + success: true, + balance: balanceHbar, + balanceTinybars: balanceTinybars.toString(), + accountId: accountId, + evmAddress: evmAddress, + unit: 'HBAR' + }); + + } catch (error) { + console.error(`[DEVICE BALANCE] Error fetching balance for ${evmAddress}:`, error); + res.status(500).json({ + success: false, + error: `Failed to fetch balance: ${error.message}` + }); + } + }); }; \ No newline at end of file diff --git a/neuron/nodes/process-manager.js b/neuron/nodes/process-manager.js index 4a335d75f2..2afa568c99 100644 --- a/neuron/nodes/process-manager.js +++ b/neuron/nodes/process-manager.js @@ -340,7 +340,7 @@ class ProcessManager { const args = [ `--port=${port}`, - // '--use-local-address', + '--use-local-address', // '--enable-upnp', '--mode=peer', `--buyer-or-seller=${nodeType}`, diff --git a/neuron/nodes/seller.html b/neuron/nodes/seller.html index f34224426f..aba7f162dd 100644 --- a/neuron/nodes/seller.html +++ b/neuron/nodes/seller.html @@ -1,17 +1,11 @@