diff --git a/neuron/nodes/buyer.js b/neuron/nodes/buyer.js index 3bd1578281..7b7b44b986 100644 --- a/neuron/nodes/buyer.js +++ b/neuron/nodes/buyer.js @@ -20,6 +20,7 @@ const { triggerCacheUpdate } = require('./global-contract-monitor.js'); const { getConnectionMonitor, removeConnectionMonitor } = require('./connection-monitor.js'); +const ContractRegistryService = require('../services/ContractRegistryService'); // At the top of the file, add a global mapping const templateToInstanceMap = new Map(); @@ -188,7 +189,7 @@ module.exports = function (RED) { let hederaServiceError = null; try { - waitForEnvReady(() => { + waitForEnvReady(async () => { console.log("Hedera credentials loaded for buyer"); const operatorId = process.env.HEDERA_OPERATOR_ID; @@ -201,19 +202,22 @@ module.exports = function (RED) { } try { + // Initialize contract registry + if (!ContractRegistryService.initialized) { + await ContractRegistryService.initialize(); + } + + // Get contracts from registry + const contractsMap = ContractRegistryService.getContractsMapForHedera(); + hederaService = new HederaAccountService({ network: process.env.HEDERA_NETWORK || 'testnet', operatorId: process.env.HEDERA_OPERATOR_ID, operatorKey: process.env.HEDERA_OPERATOR_KEY, - contracts: { - "jetvision": process.env.JETVISION_CONTRACT_ID, - "chat": process.env.CHAT_CONTRACT_ID, - "challenges": process.env.CHALLENGES_CONTRACT_ID, - //"radiation": process.env.RADIATION_CONTRACT_ID - } + contracts: contractsMap }); hederaServiceInitialized = true; - console.log("HederaAccountService initialized successfully for buyer"); + console.log("HederaAccountService initialized successfully for buyer with", Object.keys(contractsMap).length, "contracts"); } catch (error) { console.error("Failed to initialize HederaAccountService for buyer:", error.message); hederaServiceError = error; @@ -377,13 +381,12 @@ module.exports = function (RED) { 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()]; + // Set smart contract from configuration - get from registry + if (!ContractRegistryService.initialized) { + await ContractRegistryService.initialize(); + } + const contractsMap = ContractRegistryService.getContractsMapForEvm(); + loadedDeviceInfo.smartContract = contractsMap[config.smartContract.toLowerCase()]; node.deviceInfo = loadedDeviceInfo; node.deviceInfo.nodeType = "buyer"; @@ -449,12 +452,11 @@ module.exports = function (RED) { console.log(`Node ${node.id}: Validated ${sellerDevices.length} seller device(s)`); - const contracts = { - "jetvision": process.env.JETVISION_CONTRACT_EVM, - "chat": process.env.CHAT_CONTRACT_EVM, - "challenges": process.env.CHALLENGES_CONTRACT_EVM, - //"radiation": process.env.RADIATION_CONTRACT_EVM - }; + // Get contracts from registry + if (!ContractRegistryService.initialized) { + await ContractRegistryService.initialize(); + } + const contracts = ContractRegistryService.getContractsMapForEvm(); const deviceRole = 'buyer'; const serialNumber = 'buyer device'; const deviceName = 'buyer device'; @@ -586,6 +588,9 @@ module.exports = function (RED) { const connected = await connectionMonitor.connect(); if (connected) { + // Set initial status immediately after connection + node.status({ fill: "yellow", shape: "ring", text: "Connected - no peers" }); + // Set up status update callback to update node status connectionMonitor.onStatusUpdate((status) => { if (status.isConnected) { @@ -678,6 +683,89 @@ module.exports = function (RED) { return obj; } + // ============================================ + // Contract Registry API Endpoints + // ============================================ + + // Get all contracts from the registry + RED.httpAdmin.get('/registry/contracts', async function (req, res) { + try { + // Initialize registry if not already done + if (!ContractRegistryService.initialized) { + await ContractRegistryService.initialize(); + } + + const contracts = ContractRegistryService.getAllContracts(); + res.json({ + success: true, + contracts, + count: contracts.length + }); + } catch (error) { + console.error('[Registry API] Error getting contracts:', error); + res.json({ + success: false, + error: error.message + }); + } + }); + + // Get a specific contract by name + RED.httpAdmin.get('/registry/contract/:name', async function (req, res) { + try { + const { name } = req.params; + + // Initialize registry if not already done + if (!ContractRegistryService.initialized) { + await ContractRegistryService.initialize(); + } + + const contract = ContractRegistryService.getContract(name); + + if (contract) { + res.json({ + success: true, + contract + }); + } else { + res.json({ + success: false, + error: `Contract '${name}' not found` + }); + } + } catch (error) { + console.error('[Registry API] Error getting contract:', error); + res.json({ + success: false, + error: error.message + }); + } + }); + + // Refresh contracts from source (will trigger re-fetch from mother contract in future) + RED.httpAdmin.post('/registry/refresh', async function (req, res) { + try { + await ContractRegistryService.refreshContracts(); + const contracts = ContractRegistryService.getAllContracts(); + + res.json({ + success: true, + message: 'Contracts refreshed successfully', + count: contracts.length + }); + } catch (error) { + console.error('[Registry API] Error refreshing contracts:', error); + res.json({ + success: false, + error: error.message + }); + } + }); + + // ============================================ + // Buyer Device API Endpoints + // ============================================ + RED.httpAdmin.get('/buyer/devices', function (req, res) { const contract = req.query.contract || 'jetvision'; const devices = getGlobalAllDevices(contract); diff --git a/neuron/nodes/global-contract-monitor.js b/neuron/nodes/global-contract-monitor.js index 9370fde228..6d34aabfc5 100644 --- a/neuron/nodes/global-contract-monitor.js +++ b/neuron/nodes/global-contract-monitor.js @@ -4,24 +4,13 @@ require('../services/NeuronEnvironment').load(); const path = require('path'); const fs = require('fs'); const { HederaContractService } = require('neuron-js-registration-sdk'); +const ContractRegistryService = require('../services/ContractRegistryService'); // --- GLOBAL CONTRACT MONITORING SERVICE (Singleton) --- -// Separate data structures for each contract -let globalPeerCounts = { - jetvision: 0, - chat: 0, - challenges: 0 -}; -let globalAllDevices = { - jetvision: [], - chat: [], - challenges: [] -}; -let contractLoadingStates = { - jetvision: false, - chat: false, - challenges: false -}; +// Separate data structures for each contract (will be dynamically populated) +let globalPeerCounts = {}; +let globalAllDevices = {}; +let contractLoadingStates = {}; let contractMonitoringInterval = null; let isContractMonitoringActive = false; let contractServices = {}; @@ -31,44 +20,111 @@ const cacheDir = path.join(require('../services/NeuronUserHome').load(), 'cache' if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }); } -const cacheFiles = { - jetvision: 'contract-data-jetvision.json', - chat: 'contract-data-chat.json', - challenges: 'contract-data-challenges.json' -}; -for (let cacheFile in cacheFiles) { - const cacheFilePath = path.join(cacheDir, cacheFiles[cacheFile]); - - if (!fs.existsSync(cacheFilePath)) { - let cacheSampleData = {}; - - if (fs.existsSync(path.join(__dirname, 'cache', cacheFiles[cacheFile]))) { - cacheSampleData = fs.readFileSync(path.join(__dirname, 'cache', cacheFiles[cacheFile]), 'utf-8'); - cacheSampleData = JSON.parse(cacheSampleData); + +// Cache files will be dynamically created based on contracts from registry +let cacheFiles = {}; + +// Contract configuration - will be dynamically loaded from registry +let contracts = []; +let contractConfigs = {}; + +/** + * Initialize contracts from the registry + * Called during initializeGlobalContractMonitoring + */ +async function initializeContractsFromRegistry() { + try { + // Initialize registry if needed + if (!ContractRegistryService.initialized) { + await ContractRegistryService.initialize(); } - fs.writeFileSync(cacheFilePath, JSON.stringify(cacheSampleData, null, 2)); - } + // Get all contracts from registry + const registryContracts = ContractRegistryService.getAllContracts(); + + // Filter to only include contracts that have both ID and EVM address + // and are in the monitoring list (jetvision, chat, challenges) + const monitoredContractNames = ['jetvision', 'chat', 'challenges']; + const filteredContracts = registryContracts.filter(c => + monitoredContractNames.includes(c.name) && c.contractId && c.contractEvm + ); - cacheFiles[cacheFile] = cacheFilePath; -} + // Update contracts array + contracts = filteredContracts.map(c => c.name); + + // Update contract configs + contractConfigs = {}; + filteredContracts.forEach(contract => { + contractConfigs[contract.name] = { + contractId: contract.contractId, + contractEvm: contract.contractEvm + }; -// Contract configuration -const contracts = ['jetvision', 'chat', 'challenges']; -const contractConfigs = { - jetvision: { - contractId: process.env.JETVISION_CONTRACT_ID, - contractEvm: process.env.JETVISION_CONTRACT_EVM - }, - chat: { - contractId: process.env.CHAT_CONTRACT_ID, - contractEvm: process.env.CHAT_CONTRACT_EVM - }, - challenges: { - contractId: process.env.CHALLENGES_CONTRACT_ID, - contractEvm: process.env.CHALLENGES_CONTRACT_EVM + // Initialize data structures for this contract + globalPeerCounts[contract.name] = globalPeerCounts[contract.name] || 0; + globalAllDevices[contract.name] = globalAllDevices[contract.name] || []; + contractLoadingStates[contract.name] = contractLoadingStates[contract.name] || false; + + // Setup cache file for this contract + const cacheFileName = `contract-data-${contract.name}.json`; + const cacheFilePath = path.join(cacheDir, cacheFileName); + + if (!fs.existsSync(cacheFilePath)) { + let cacheSampleData = {}; + const sampleCachePath = path.join(__dirname, 'cache', cacheFileName); + + if (fs.existsSync(sampleCachePath)) { + cacheSampleData = JSON.parse(fs.readFileSync(sampleCachePath, 'utf-8')); + } + + fs.writeFileSync(cacheFilePath, JSON.stringify(cacheSampleData, null, 2)); + } + + cacheFiles[contract.name] = cacheFilePath; + }); + + console.log(`[GlobalContractMonitor] Initialized ${contracts.length} contracts from registry:`, contracts); + return true; + } catch (error) { + console.error('[GlobalContractMonitor] Failed to initialize contracts from registry:', error); + + // Fallback to hardcoded contracts if registry fails + console.warn('[GlobalContractMonitor] Falling back to environment variable contracts'); + contracts = ['jetvision', 'chat', 'challenges']; + contractConfigs = { + jetvision: { + contractId: process.env.JETVISION_CONTRACT_ID, + contractEvm: process.env.JETVISION_CONTRACT_EVM + }, + chat: { + contractId: process.env.CHAT_CONTRACT_ID, + contractEvm: process.env.CHAT_CONTRACT_EVM + }, + challenges: { + contractId: process.env.CHALLENGES_CONTRACT_ID, + contractEvm: process.env.CHALLENGES_CONTRACT_EVM + } + }; + + // Initialize data structures for fallback contracts + contracts.forEach(contractName => { + globalPeerCounts[contractName] = 0; + globalAllDevices[contractName] = []; + contractLoadingStates[contractName] = false; + + const cacheFileName = `contract-data-${contractName}.json`; + const cacheFilePath = path.join(cacheDir, cacheFileName); + + if (!fs.existsSync(cacheFilePath)) { + fs.writeFileSync(cacheFilePath, JSON.stringify({}, null, 2)); + } + + cacheFiles[contractName] = cacheFilePath; + }); + + return false; } -}; +} // Load cached contract data for a specific contract function loadCachedContractData(contract) { @@ -190,6 +246,9 @@ async function initializeGlobalContractMonitoring() { console.log('Initializing global contract monitoring service for all contracts...'); + // Step 0: Initialize contracts from registry + await initializeContractsFromRegistry(); + // Step 1: Load cached data immediately for fast startup const hasCachedData = {}; contracts.forEach(contract => { diff --git a/neuron/nodes/neuron-p2p.html b/neuron/nodes/neuron-p2p.html index ff7558df17..4a446589cf 100644 --- a/neuron/nodes/neuron-p2p.html +++ b/neuron/nodes/neuron-p2p.html @@ -53,6 +53,10 @@ text-align: left; border-bottom: 2px solid #ddd; font-weight: bold; + position: sticky; + top: 0; + background-color: white; + z-index: 10; } .node-table td { padding: 8px; @@ -135,6 +139,14 @@ ] }); + // Create a scrollable container for the table + const tableContainer = $('
').css({ + 'max-height': '320px', + 'overflow-y': 'auto', + 'overflow-x': 'hidden', + 'margin-bottom': '10px' + }); + // Create a table to show available nodes const table = $('').addClass('node-table'); const thead = $('').append( @@ -190,7 +202,8 @@ }); table.append(tbody); - dialog.append(table); + tableContainer.append(table); + dialog.append(tableContainer); dialog.dialog('open'); // Open the dialog }); diff --git a/neuron/nodes/process-manager.js b/neuron/nodes/process-manager.js index 29ebea1a15..fc4fce0bfc 100644 --- a/neuron/nodes/process-manager.js +++ b/neuron/nodes/process-manager.js @@ -404,14 +404,24 @@ class ProcessManager { const args = [ `--port=${port}`, - // '--use-local-address', - // '--enable-upnp', '--mode=peer', `--buyer-or-seller=${nodeType}`, nodeType === 'seller' ? '--list-of-buyers-source=env' : '--list-of-sellers-source=env', `--envFile=${envFilePath}`, `--ws-port=${port}` ]; + + // Add --use-local-address flag if enabled in .env + if (process.env.USE_LOCAL_ADDRESS === 'true' || process.env.USE_LOCAL_ADDRESS === '1') { + args.splice(1, 0, '--use-local-address'); + console.log('✅ Using local address mode (USE_LOCAL_ADDRESS=true)'); + } + + // Add --enable-upnp flag if enabled in .env + if (process.env.ENABLE_UPNP === 'true' || process.env.ENABLE_UPNP === '1') { + args.splice(1, 0, '--enable-upnp'); + console.log('✅ UPNP enabled (ENABLE_UPNP=true)'); + } console.log(`🔍 Spawning process with executable: ${executablePath}`); diff --git a/neuron/nodes/seller.html b/neuron/nodes/seller.html index da65faabbb..81688d0690 100644 --- a/neuron/nodes/seller.html +++ b/neuron/nodes/seller.html @@ -114,22 +114,26 @@

Runtime Information

-
- - -
+ +
-  Top up account - + + + + + + + +
@@ -196,9 +200,9 @@

Runtime Information

- - - + + + @@ -264,6 +268,10 @@

Runtime Information

RED.nodes.registerType('seller config', { category: 'Neuron', color: '#b3e6ff', + editDialogSize: { + width: '600px', + height: 'auto' + }, defaults: { name: { value: "Seller" }, deviceRole: { value: null, required: true }, @@ -2135,6 +2143,35 @@

Runtime Information

EVM AddressShared AccountStatusLast SeenShared AccountStatusLast Seen