From 98459972509eb53abf0f4d0a8877acc6037910e8 Mon Sep 17 00:00:00 2001 From: Abu Date: Thu, 16 Oct 2025 11:58:03 +0100 Subject: [PATCH 1/4] dynamic contract loading --- neuron/nodes/buyer.js | 127 +++++++-- neuron/nodes/global-contract-monitor.js | 157 +++++++---- neuron/nodes/process-manager.js | 2 +- neuron/nodes/seller.js | 125 +++++++-- neuron/nodes/stderr.js | 18 +- neuron/nodes/stdin-w.js | 26 +- neuron/nodes/stdin.js | 26 +- neuron/nodes/stdout.js | 26 +- neuron/services/ContractRegistryService.js | 297 +++++++++++++++++++++ 9 files changed, 673 insertions(+), 131 deletions(-) create mode 100644 neuron/services/ContractRegistryService.js diff --git a/neuron/nodes/buyer.js b/neuron/nodes/buyer.js index 3bd1578281..76632472c5 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'; @@ -678,6 +680,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/process-manager.js b/neuron/nodes/process-manager.js index 29ebea1a15..5248f4cbe9 100644 --- a/neuron/nodes/process-manager.js +++ b/neuron/nodes/process-manager.js @@ -404,7 +404,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.js b/neuron/nodes/seller.js index 1d2ec2d8ab..b105748f34 100644 --- a/neuron/nodes/seller.js +++ b/neuron/nodes/seller.js @@ -17,6 +17,7 @@ const { } = require('./global-contract-monitor.js'); const { getConnectionMonitor, removeConnectionMonitor } = require('./connection-monitor.js'); const ProcessManager = require('./process-manager.js'); +const ContractRegistryService = require('../services/ContractRegistryService'); // At the top of the file, add a global mapping for seller nodes const sellerTemplateToInstanceMap = new Map(); @@ -171,7 +172,7 @@ module.exports = function (RED) { let hederaServiceError = null; try { - waitForEnvReady(() => { + waitForEnvReady(async () => { console.log("Hedera credentials loaded for seller"); const operatorId = process.env.HEDERA_OPERATOR_ID; @@ -184,16 +185,19 @@ 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 seller"); @@ -369,13 +373,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 = "seller"; @@ -428,12 +431,11 @@ module.exports = function (RED) { return; } - 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(); let device; try { @@ -663,6 +665,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 + }); + } + }); + + // ============================================ + // Seller Device API Endpoints + // ============================================ + RED.httpAdmin.get('/seller/devices', function (req, res) { const contract = req.query.contract || 'jetvision'; const devices = getGlobalAllDevices(contract); diff --git a/neuron/nodes/stderr.js b/neuron/nodes/stderr.js index a562e1db77..ece69a8ae9 100644 --- a/neuron/nodes/stderr.js +++ b/neuron/nodes/stderr.js @@ -5,6 +5,7 @@ const { HederaAccountService } = require('neuron-js-registration-sdk'); const fs = require('fs'); const path = require('path'); const waitForEnvReady = require('../services/WaitForEnvReady'); +const ContractRegistryService = require('../services/ContractRegistryService'); module.exports = function (RED) { function StderrNode(config) { @@ -14,17 +15,20 @@ module.exports = function (RED) { // Initialize Hedera service try { - waitForEnvReady(() => { + waitForEnvReady(async () => { + // 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: { - "adsb": process.env.CONTRACT_ID, - "mcp": process.env.MCP_CONTRACT_ID, - "weather": process.env.WEATHER_CONTRACT_ID, - "radiation": process.env.RADIATION_CONTRACT_ID - } + contracts: contractsMap }); }); diff --git a/neuron/nodes/stdin-w.js b/neuron/nodes/stdin-w.js index c50e552d77..9616048faf 100644 --- a/neuron/nodes/stdin-w.js +++ b/neuron/nodes/stdin-w.js @@ -5,6 +5,7 @@ const { HederaAccountService } = require('neuron-js-registration-sdk'); const fs = require('fs'); const path = require('path'); const waitForEnvReady = require('../services/WaitForEnvReady'); +const ContractRegistryService = require('../services/ContractRegistryService'); module.exports = function (RED) { function StdinWNode(config) { @@ -14,19 +15,22 @@ module.exports = function (RED) { // Initialize Hedera service try { - waitForEnvReady(() => { - hederaService = new HederaAccountService({ - network: process.env.HEDERA_NETWORK || 'testnet', - operatorId: process.env.HEDERA_OPERATOR_ID, - operatorKey: process.env.HEDERA_OPERATOR_KEY, - contracts: { - "adsb": process.env.CONTRACT_ID, - "mcp": process.env.MCP_CONTRACT_ID, - "weather": process.env.WEATHER_CONTRACT_ID, - "radiation": process.env.RADIATION_CONTRACT_ID + waitForEnvReady(async () => { + // 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: contractsMap + }); }); - }); // node.status({ fill: "green", shape: "dot", text: "initialized" }); let targetNode = null; RED.nodes.eachNode(function(n) { diff --git a/neuron/nodes/stdin.js b/neuron/nodes/stdin.js index 90d1510f08..b422ec4996 100644 --- a/neuron/nodes/stdin.js +++ b/neuron/nodes/stdin.js @@ -5,6 +5,7 @@ const { HederaAccountService } = require('neuron-js-registration-sdk'); const fs = require('fs'); const path = require('path'); const waitForEnvReady = require('../services/WaitForEnvReady'); +const ContractRegistryService = require('../services/ContractRegistryService'); module.exports = function (RED) { function StdinNode(config) { @@ -14,19 +15,22 @@ module.exports = function (RED) { // Initialize Hedera service try { - waitForEnvReady(() => { - hederaService = new HederaAccountService({ - network: process.env.HEDERA_NETWORK || 'testnet', - operatorId: process.env.HEDERA_OPERATOR_ID, - operatorKey: process.env.HEDERA_OPERATOR_KEY, - contracts: { - "adsb": process.env.CONTRACT_ID, - "mcp": process.env.MCP_CONTRACT_ID, - "weather": process.env.WEATHER_CONTRACT_ID, - "radiation": process.env.RADIATION_CONTRACT_ID + waitForEnvReady(async () => { + // 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: contractsMap + }); }); - }); let targetNode = null; RED.nodes.eachNode(function(n) { if (n.id === config.selectedNode) { diff --git a/neuron/nodes/stdout.js b/neuron/nodes/stdout.js index aa7c16927a..3958a9d8f2 100644 --- a/neuron/nodes/stdout.js +++ b/neuron/nodes/stdout.js @@ -5,6 +5,7 @@ const { HederaAccountService } = require('neuron-js-registration-sdk'); const fs = require('fs'); const path = require('path'); const waitForEnvReady = require('../services/WaitForEnvReady'); +const ContractRegistryService = require('../services/ContractRegistryService'); module.exports = function (RED) { function StdoutNode(config) { @@ -14,19 +15,22 @@ module.exports = function (RED) { // Initialize Hedera service try { - waitForEnvReady(() => { - hederaService = new HederaAccountService({ - network: process.env.HEDERA_NETWORK || 'testnet', - operatorId: process.env.HEDERA_OPERATOR_ID, - operatorKey: process.env.HEDERA_OPERATOR_KEY, - contracts: { - "adsb": process.env.CONTRACT_ID, - "mcp": process.env.MCP_CONTRACT_ID, - "weather": process.env.WEATHER_CONTRACT_ID, - "radiation": process.env.RADIATION_CONTRACT_ID + waitForEnvReady(async () => { + // 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: contractsMap + }); }); - }); // node.status({ fill: "green", shape: "dot", text: "initialized" }); let targetNode = null; RED.nodes.eachNode(function(n) { diff --git a/neuron/services/ContractRegistryService.js b/neuron/services/ContractRegistryService.js new file mode 100644 index 0000000000..5aa7cbc58f --- /dev/null +++ b/neuron/services/ContractRegistryService.js @@ -0,0 +1,297 @@ +// Load environment variables +require('./NeuronEnvironment').load(); + +const path = require('path'); +const fs = require('fs'); + +/** + * ContractRegistryService - Manages smart contract registry + * + * Current Implementation: Returns hardcoded contract list from environment variables + * Future Implementation: Will fetch contracts from mother contract via SDK + */ +class ContractRegistryService { + constructor() { + this.contracts = {}; + this.initialized = false; + this.cacheFile = path.join(require('./NeuronUserHome').load(), 'cache', 'contract-registry-cache.json'); + + // Ensure cache directory exists + const cacheDir = path.dirname(this.cacheFile); + if (!fs.existsSync(cacheDir)) { + fs.mkdirSync(cacheDir, { recursive: true }); + } + } + + /** + * Initialize the contract registry + * Loads contracts from hardcoded list (will be replaced with mother contract API call) + */ + async initialize() { + if (this.initialized) { + console.log('[ContractRegistry] Already initialized'); + return; + } + + try { + // Load from cache first for fast startup + this.loadFromCache(); + + // Then load/refresh from source + await this.refreshContracts(); + + this.initialized = true; + console.log('[ContractRegistry] Initialized successfully with', Object.keys(this.contracts).length, 'contracts'); + } catch (error) { + console.error('[ContractRegistry] Failed to initialize:', error.message); + + // If we have cached contracts, use them + if (Object.keys(this.contracts).length > 0) { + console.log('[ContractRegistry] Using cached contracts as fallback'); + this.initialized = true; + } else { + throw new Error('Failed to initialize contract registry and no cache available'); + } + } + } + + /** + * Refresh contracts from source + * TODO: Replace hardcoded list with SDK call to mother contract + */ + async refreshContracts() { + console.log('[ContractRegistry] Refreshing contracts...'); + + // HARDCODED CONTRACT LIST - TO BE REPLACED WITH MOTHER CONTRACT API CALL + // This mimics what would be returned from the mother contract + const contractList = this._getHardcodedContracts(); + + // Convert array to map for easy lookup + this.contracts = {}; + contractList.forEach(contract => { + this.contracts[contract.name] = contract; + }); + + // Save to cache + this.saveToCache(); + + console.log('[ContractRegistry] Refreshed', contractList.length, 'contracts'); + return contractList; + } + + /** + * Get hardcoded contract list from environment variables + * TODO: Replace this with actual API call to mother contract + * + * @returns {Array} Array of contract objects + */ + _getHardcodedContracts() { + const contracts = []; + + // Jetvision contract + if (process.env.JETVISION_CONTRACT_ID && process.env.JETVISION_CONTRACT_EVM) { + contracts.push({ + name: 'jetvision', + contractId: process.env.JETVISION_CONTRACT_ID, + contractEvm: process.env.JETVISION_CONTRACT_EVM, + description: 'Jetvision Aviation Data Contract' + }); + } + + // Chat contract + if (process.env.CHAT_CONTRACT_ID && process.env.CHAT_CONTRACT_EVM) { + contracts.push({ + name: 'chat', + contractId: process.env.CHAT_CONTRACT_ID, + contractEvm: process.env.CHAT_CONTRACT_EVM, + description: 'P2P Chat Contract' + }); + } + + // Challenges contract + if (process.env.CHALLENGES_CONTRACT_ID && process.env.CHALLENGES_CONTRACT_EVM) { + contracts.push({ + name: 'challenges', + contractId: process.env.CHALLENGES_CONTRACT_ID, + contractEvm: process.env.CHALLENGES_CONTRACT_EVM, + description: 'AI Challenges Contract' + }); + } + + // Generic ADSB contract (used by stdin/stdout/stderr) + if (process.env.CONTRACT_ID) { + contracts.push({ + name: 'adsb', + contractId: process.env.CONTRACT_ID, + contractEvm: process.env.CONTRACT_EVM || '', + description: 'ADSB Data Contract' + }); + } + + // MCP contract + if (process.env.MCP_CONTRACT_ID) { + contracts.push({ + name: 'mcp', + contractId: process.env.MCP_CONTRACT_ID, + contractEvm: process.env.MCP_CONTRACT_EVM || '', + description: 'MCP Contract' + }); + } + + // Weather contract + if (process.env.WEATHER_CONTRACT_ID) { + contracts.push({ + name: 'weather', + contractId: process.env.WEATHER_CONTRACT_ID, + contractEvm: process.env.WEATHER_CONTRACT_EVM || '', + description: 'Weather Data Contract' + }); + } + + // Radiation contract + if (process.env.RADIATION_CONTRACT_ID) { + contracts.push({ + name: 'radiation', + contractId: process.env.RADIATION_CONTRACT_ID, + contractEvm: process.env.RADIATION_CONTRACT_EVM || '', + description: 'Radiation Monitoring Contract' + }); + } + + return contracts; + } + + /** + * Get all contracts + * @returns {Array} Array of all contract objects + */ + getAllContracts() { + return Object.values(this.contracts); + } + + /** + * Get contract by name + * @param {string} name - Contract name (e.g., 'jetvision', 'chat') + * @returns {Object|null} Contract object or null if not found + */ + getContract(name) { + const contract = this.contracts[name.toLowerCase()]; + if (!contract) { + console.warn(`[ContractRegistry] Contract '${name}' not found`); + return null; + } + return contract; + } + + /** + * Get contract ID by name + * @param {string} name - Contract name + * @returns {string|null} Contract ID (e.g., '0.0.12345') or null + */ + getContractId(name) { + const contract = this.getContract(name); + return contract ? contract.contractId : null; + } + + /** + * Get contract EVM address by name + * @param {string} name - Contract name + * @returns {string|null} Contract EVM address or null + */ + getContractEvm(name) { + const contract = this.getContract(name); + return contract ? contract.contractEvm : null; + } + + /** + * Get all contract names + * @returns {Array} Array of contract names + */ + getContractNames() { + return Object.keys(this.contracts); + } + + /** + * Get contracts map for HederaAccountService + * Returns object with contract names as keys and contract IDs as values + * @returns {Object} Map of contract names to IDs + */ + getContractsMapForHedera() { + const map = {}; + Object.keys(this.contracts).forEach(name => { + map[name] = this.contracts[name].contractId; + }); + return map; + } + + /** + * Get contracts map for EVM addresses + * Returns object with contract names as keys and EVM addresses as values + * @returns {Object} Map of contract names to EVM addresses + */ + getContractsMapForEvm() { + const map = {}; + Object.keys(this.contracts).forEach(name => { + if (this.contracts[name].contractEvm) { + map[name] = this.contracts[name].contractEvm; + } + }); + return map; + } + + /** + * Load contracts from cache + */ + loadFromCache() { + try { + if (fs.existsSync(this.cacheFile)) { + const cached = JSON.parse(fs.readFileSync(this.cacheFile, 'utf-8')); + + if (cached.contracts && cached.timestamp) { + this.contracts = cached.contracts; + const age = Date.now() - cached.timestamp; + const ageMinutes = Math.floor(age / 60000); + + console.log(`[ContractRegistry] Loaded ${Object.keys(this.contracts).length} contracts from cache (${ageMinutes} minutes old)`); + return true; + } + } + } catch (error) { + console.error('[ContractRegistry] Failed to load cache:', error.message); + } + return false; + } + + /** + * Save contracts to cache + */ + saveToCache() { + try { + const cacheData = { + contracts: this.contracts, + timestamp: Date.now(), + lastUpdated: new Date().toISOString() + }; + + fs.writeFileSync(this.cacheFile, JSON.stringify(cacheData, null, 2)); + console.log('[ContractRegistry] Saved contracts to cache'); + } catch (error) { + console.error('[ContractRegistry] Failed to save cache:', error.message); + } + } + + /** + * Check if a contract exists + * @param {string} name - Contract name + * @returns {boolean} True if contract exists + */ + hasContract(name) { + return !!this.contracts[name.toLowerCase()]; + } +} + +// Export singleton instance +const contractRegistryService = new ContractRegistryService(); + +module.exports = contractRegistryService; + From 46892f4a5e78583c44a6f5685e6eaa75997ee96e Mon Sep 17 00:00:00 2001 From: Abu Date: Fri, 17 Oct 2025 10:14:35 +0100 Subject: [PATCH 2/4] seller configuration styling --- neuron/nodes/seller.html | 65 +++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 14 deletions(-) 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

-
- - -
+ +
@@ -196,9 +200,9 @@

Runtime Information

EVM Address - Shared Account - Status - Last Seen + Shared Account + Status + Last Seen @@ -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