UNPKG

navskit

Version:

Deploy TypeScript logic on Ethereum. Includes core library, CLI tools, and utilities.

785 lines (784 loc) 38.8 kB
"use strict"; /** * NAVS - Unified package for core functionality and deployment contracts * Deploy TypeScript logic on Ethereum. Powered by EigenLayer. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.navsRegistry = exports.navs = exports.getConfig = void 0; // Export core functionality __exportStar(require("./consensus"), exports); __exportStar(require("./tools"), exports); __exportStar(require("./types"), exports); __exportStar(require("./utils"), exports); // Export deployment contracts and ABIs __exportStar(require("./deploy"), exports); /** * Main entry point for NAVS (Node-Assisted Verification Service) */ const viem_1 = require("viem"); const chains_1 = require("viem/chains"); require("reflect-metadata"); const deploy_1 = require("./deploy"); const utils_1 = require("./utils"); const tools_1 = require("./tools"); var ConsensusType; (function (ConsensusType) { ConsensusType[ConsensusType["EXACT_MATCH"] = 0] = "EXACT_MATCH"; ConsensusType[ConsensusType["CUSTOM"] = 1] = "CUSTOM"; })(ConsensusType || (ConsensusType = {})); // Constants const chain = chains_1.baseSepolia; const chainL1 = chains_1.sepolia; const NAVS_IMAGE_ID = 0x14; // TODO: actually get the real image id constant // Parameter type metadata key const PARAM_TYPES_METADATA = 'design:paramtypes'; const RETURN_TYPE_METADATA = 'design:returntype'; // Global registry to track navs-callable functions const __navsRegistry = {}; exports.navsRegistry = __navsRegistry; // globally set the navsConfig after navs has been initialized let __navsConfig = undefined; const getConfig = () => { if (__navsConfig === undefined) { throw new Error(`Navs was not initialized yet.`); } return __navsConfig; }; exports.getConfig = getConfig; const blockTag = 'pending'; // support preconfs. /** * Initialize NAVS with the provided configuration * * @param config Configuration options for NAVS * @returns The NAVS client interface */ const navs = (_config) => { const config = { ..._config, serviceVersion: _config.serviceVersion ?? "1.0.0", avsL1: _config.avsL1 ?? deploy_1.Addresses[11155111].AVS, taskDispatchL2: _config.taskDispatchL2 ?? deploy_1.Addresses['84532'].TaskDispatch, reexecutionEndpoint: _config.reexecutionEndpoint ?? `0x`, delegationManager: _config.delegationManager ?? deploy_1.Addresses[11155111].DelegationManager, // default to sepolia allocationManager: _config.allocationManager ?? deploy_1.Addresses[11155111].AllocationManager, // default to sepolia l2rpcUrl: _config.l2rpcUrl ?? `https://base-sepolia.gateway.tenderly.co`, rpcUrlL1: _config.rpcUrlL1 ?? `https://rpc.sepolia.ethpandaops.io` }; __navsConfig = config; const publicClient = (0, viem_1.createPublicClient)({ chain, pollingInterval: 200, transport: config.l2rpcUrl?.startsWith('wss://') ? (0, viem_1.webSocket)(config.l2rpcUrl) : (0, viem_1.http)(config.l2rpcUrl) }); const taskDispatchRead = (0, viem_1.getContract)({ abi: deploy_1.TaskDispatchAbi, address: config.taskDispatchL2, client: publicClient }); const publicClientL1 = (0, viem_1.createPublicClient)({ chain: chainL1, transport: (0, viem_1.http)(config.rpcUrlL1) }); // Create service-specific logger const serviceLog = (0, utils_1.createServiceLogger)(config.serviceName, _config.serviceVersion ?? `0.0.0`); /** * Creates a typed TaskId object that can be awaited to get the task result * * @param taskId The raw task ID * @param returnType The expected return type of the task * @returns A TaskId object with wait functionality */ function createTypedTaskId(taskId, returnType) { return { id: taskId, /** * Waits for the task to complete and returns the result * * @param options Optional configuration for waiting * @returns The typed task result */ wait: async (options) => { const waitOptions = { timeout: options?.timeout ?? 60000, // Default 1 minute pollInterval: options?.pollInterval ?? 2000, // Default 2 seconds }; const startTime = Date.now(); // Polling function to check task status while (Date.now() - startTime < waitOptions.timeout) { const status = await taskDispatchRead.read.getTaskStatus([taskId], { blockTag }); const [_exists, completed, failed, finalResponse] = status; if (failed) { throw new Error(`Task #${taskId} failed`); } if (completed && finalResponse) { // Decode the result based on the expected return type return (0, utils_1.decodeTaskResult)(returnType, finalResponse); } // Wait before polling again await new Promise(resolve => setTimeout(resolve, waitOptions.pollInterval)); } throw new Error(`Task #${taskId} did not complete within the timeout period (${waitOptions.timeout}ms)`); } }; } /** * Register the service with the TaskDispatch contract * * @param account The account to use for registration */ const setupService = async (account) => { const walletClientL1 = (0, viem_1.createWalletClient)({ chain: chainL1, transport: (0, viem_1.http)(config.rpcUrlL1), account: account }); console.log(`Using config: ${JSON.stringify(config, null, 2)}`); const allocationManagerWrite = (0, viem_1.getContract)({ abi: deploy_1.AllocationManagerAbi, address: config.allocationManager, client: { public: publicClientL1, wallet: walletClientL1 } }); const delegationManagerWrite = (0, viem_1.getContract)({ abi: deploy_1.DelegationManagerAbi, address: config.delegationManager, client: { public: publicClientL1, wallet: walletClientL1 } }); const isRegistered = await (async () => { try { const sets = await allocationManagerWrite.read.getRegisteredSets([account.address]); return sets.filter((set) => set.avs === config.avsL1).length > 0; } catch { return false; } })(); if (!isRegistered) { const opSetCount = await allocationManagerWrite.read.getOperatorSetCount([config.avsL1]); if (opSetCount === 0n) { const t = await allocationManagerWrite.write.createOperatorSets([ config.avsL1, [{ operatorSetId: deploy_1.DEFAULT_OPERATOR_SET_ID, strategies: [deploy_1.Addresses['11155111'].Strategy] }] ]); serviceLog.info("initialized operator sets", { t }); } if (!(await delegationManagerWrite.read.isOperator([account.address]))) { serviceLog.info(`Operator<${account.address}> joining EigenLayer...`); const raoTxn = await delegationManagerWrite.write.registerAsOperator([ account.address, // initDelegationApprover 0, // allocationDelay 'navs://' // metadataUri ]); serviceLog.info(`Operator<${account.address}> joined EigenLayer!`, { txn: raoTxn }); } else { serviceLog.info(`Operator<${account.address}> already joined EigenLayer!`); } serviceLog.info(`Registering for operator set...`); const rfosTxn = await allocationManagerWrite.write.registerForOperatorSets([ account.address, { avs: config.avsL1, operatorSetIds: [deploy_1.DEFAULT_OPERATOR_SET_ID], data: (0, viem_1.toHex)(0) } ]); serviceLog.info(`Service '${config.serviceName}': added operator (${account.address})`, { txn: rfosTxn }); } }; // Return the NAVS client interface return { config: config, setupService, /** * If running as a node, `main()` is the primary entrypoint for the node. * This will listen for task events and execute the corresponding functions. * Always outputs JSON results to stdout for processing by central operator. */ async main(account) { serviceLog.info(`Starting NAVS node for service: ${config.serviceName} (JSON output mode)`); // Setup service registration with EigenLayer if account is provided if (account) { try { await setupService(account); serviceLog.info('Service setup completed successfully'); } catch (error) { serviceLog.error('Error during service setup:', error); throw error; } } else { serviceLog.info('No account provided - skipping EigenLayer registration (worker mode)'); } // Initialize a processing queue to avoid re-processing the same tasks const processedTasks = new Set(); const taskDispatchRead = (0, viem_1.getContract)({ abi: deploy_1.TaskDispatchAbi, address: config.taskDispatchL2, client: publicClient }); const tools = new tools_1.Tools({ avs: config.avsL1 }); const consensusArgs = async (taskId) => { const resultLogs = await publicClient.getContractEvents({ address: config.taskDispatchL2, strict: true, abi: deploy_1.TaskDispatchAbi, eventName: 'ResultSubmitted', args: { taskId }, fromBlock: 0n }); const responses = {}; const operatorAddresses = new Set(); const [_serviceName, _serviceVersion, functionName, _args, _stakeThreshold, _remainingStake, _isConsensus, _consensusType] = await taskDispatchRead.read.getTaskDetails([taskId], { blockTag }); await Promise.allSettled(resultLogs.map(async (log) => { const { operator, response: { success, resultType: _resultType, result, extra: _extra } } = log.args; if (success) { operatorAddresses.add(operator); const submittedInBlock = await publicClient.getBlock({ blockHash: log.blockHash }); // Get function info to decode the response properly const fnInfo = __navsRegistry[functionName]; if (!fnInfo) { throw new Error(`Function ${functionName} not found in registry when decoding consensus responses`); } // Decode the response using the function's return type let decodedResponse; try { decodedResponse = (0, utils_1.decodeValue)(fnInfo.returnType, result); } catch (error) { console.warn(`Failed to decode response from operator ${operator}:`, error); // Skip this response if we can't decode it return; } responses[operator] = { raw: result, decoded: decodedResponse, submissionTimestampMs: Number(submittedInBlock.timestamp) * 1000 }; } })); // Get stakes for all operators who submitted responses const stake = {}; for (const operatorAddress of operatorAddresses) { const operatorStake = await taskDispatchRead.read.getOperatorStake([operatorAddress], { blockTag }); stake[operatorAddress] = operatorStake; } // Determine quorum response (most agreed upon response based on stake weight) let quorum = undefined; if (Object.keys(responses).length > 0) { // Group responses by their decoded value and calculate total stake for each const responseGroups = new Map(); for (const [operator, response] of Object.entries(responses)) { const operatorAddr = operator; const operatorStake = stake[operatorAddr] || 0n; // Use the raw response as the key for grouping (since decoded values might not be easily comparable) const responseKey = response.raw; if (responseGroups.has(responseKey)) { const group = responseGroups.get(responseKey); group.totalStake += operatorStake; group.operators.push(operatorAddr); } else { responseGroups.set(responseKey, { response, totalStake: operatorStake, operators: [operatorAddr] }); } } // Find the response with the highest total stake let maxStake = 0n; for (const group of responseGroups.values()) { if (group.totalStake > maxStake) { maxStake = group.totalStake; quorum = group.response; } } } return { responses, quorum, tools, stake }; }; const encodeResult = (result) => { if (result instanceof Error) { return ['error', (0, viem_1.encodeAbiParameters)([{ type: 'string' }], [result.message])]; } else if (typeof result === 'object') { return ['json', (0, viem_1.encodeAbiParameters)([{ type: 'string' }], [JSON.stringify(result)])]; } else if (typeof result === 'number') { if (Number.isInteger(result) && result > 0) { return ['int', (0, viem_1.encodeAbiParameters)([{ type: 'int' }], [BigInt(result)])]; } else { return ['uint', (0, viem_1.encodeAbiParameters)([{ type: 'uint256' }], [BigInt(result)])]; } } else if (typeof result === 'bigint') { return ['uint256', (0, viem_1.encodeAbiParameters)([{ type: 'uint256' }], [BigInt(result)])]; } else if (typeof result === 'boolean') { return ['bool', (0, viem_1.encodeAbiParameters)([{ type: 'bool' }], [result])]; } else { return [typeof result, (0, viem_1.toHex)(result)]; } }; /** * Check if a task needs processing and process it if needed * * @param taskId The ID of the task to check */ async function checkAndProcessTask(taskId, _atBlock) { if (processedTasks.has(taskId)) { return; } try { const [exists, completed, failed] = await taskDispatchRead.read.getTaskStatus([taskId], { blockTag }); if (!exists || completed || failed) { processedTasks.add(taskId); return; } const [serviceName, serviceVersion, functionName, args, stakeThreshold, _remainingStake, isConsensus, _consensusType] = await taskDispatchRead.read.getTaskDetails([taskId], { blockTag }); const taskLogger = serviceLog.getSubLogger({ name: `Task-${taskId}` }); taskLogger.info(`Processing task: ${serviceName}@${serviceVersion}/${functionName} (required stake: ${stakeThreshold})`); // Check if function is available locally first const localFuncInfo = __navsRegistry[functionName]; let functionExists = !!localFuncInfo; let result; try { if (functionExists) { const decodedArgs = localFuncInfo.decodeArgs(args); taskLogger.info(`Executing -> ${functionName}(${decodedArgs.join(',')})`); if (isConsensus) { if (!localFuncInfo.consensus) { taskLogger.error('requested consensus, but no consensus was found.'); return; } const consensusArgsData = await consensusArgs(taskId); result = await localFuncInfo.consensus(consensusArgsData); } else { result = await localFuncInfo.fn(...decodedArgs); } taskLogger.info(`${functionName}(${decodedArgs.join(',')}): `, result); } else { const allFunctionNames = Object.keys(__navsRegistry).map(fnName => { return `\t${fnName}: (${__navsRegistry[fnName].paramTypes.join(', ')}) => ${__navsRegistry[fnName].returnType}`; }); throw new Error(`Unknown function: ${functionName}.\n\nAvailable functions:\n${allFunctionNames}`); } } catch (e) { taskLogger.error(`Error executing function:`, e); result = e; } const [resultType, resultBytes] = encodeResult(result); const EMPTY_32_BYTES_HEX = (0, viem_1.toHex)(0, { size: 32 }); const [_l1txn, l1calldatahash] = await tools.getTransaction() ?? [undefined, EMPTY_32_BYTES_HEX]; const extra = l1calldatahash; const functionResultBytes = (0, viem_1.encodeAbiParameters)([ { name: 'resultType', type: 'string' }, { name: 'result', type: 'bytes' }, { name: 'l1calldatahash', type: 'bytes32' }, ], [resultType, resultBytes, extra ?? EMPTY_32_BYTES_HEX]); // JSON output mode: output result to stdout for central operator to process const jsonOutput = { consensus: isConsensus, taskId: Number(taskId), result: functionResultBytes, resultType, functionName, extra: l1calldatahash }; // Output JSON to stdout for central operator to process console.log(JSON.stringify(jsonOutput)); // Mark task as processed processedTasks.add(taskId); taskLogger.info(`Task result output as JSON`, jsonOutput); } catch (error) { serviceLog.error(`Error processing task #${taskId}:`, error); } } /** * Periodically check for historical tasks that need processing */ async function processHistoricalTasks() { serviceLog.info('Scanning for historical tasks...'); try { // Get current task ID counter let nextTaskId = await taskDispatchRead.read.nextTaskId({ blockTag }); // Check all existing tasks for (let i = BigInt(1); i < nextTaskId; i++) { await checkAndProcessTask(i, 0n); } serviceLog.info(`Historical task scan complete (scanned tasks 1-${nextTaskId})`); } catch (error) { serviceLog.error(`Error scanning historical tasks:`, error); } // Schedule the next scan setTimeout(processHistoricalTasks, 60000); // Scan every minute } // Start historical task scanning processHistoricalTasks().catch(err => serviceLog.error("Error in historical task scan:", err)); // Set up event listener for TaskRequested events taskDispatchRead.watchEvent.TaskRequested({}, { onLogs: async (logs) => { for (const log of logs) { try { const { taskId } = log.args; serviceLog.info(`New task requested #${taskId}`); // Process the new task await checkAndProcessTask(taskId, log.blockNumber); } catch (error) { serviceLog.error(`Error handling task event:`, error); } } }, onError: (error) => { serviceLog.error(`Error watching for TaskRequested events:`, error); } }); // Also listen for TaskSuccess and TaskFailed events to update our processed set taskDispatchRead.watchEvent.TaskSuccess({}, { onLogs: (logs) => { for (const log of logs) { const { taskId } = log.args; processedTasks.add(taskId); serviceLog.info(`Task #${taskId} completed successfully`); } } }); taskDispatchRead.watchEvent.TaskFailed({}, { onLogs: (logs) => { for (const log of logs) { const { taskId } = log.args; processedTasks.add(taskId); serviceLog.warn(`Task #${taskId} failed`); } } }); serviceLog.info('NAVS node is running and watching for tasks'); // Clean up package manager resources on process exit process.on('SIGINT', async () => { serviceLog.info('Shutting down NAVS node...'); await utils_1.packageManager.clearCache(); process.exit(0); }); process.on('SIGTERM', async () => { serviceLog.info('Shutting down NAVS node...'); await utils_1.packageManager.clearCache(); process.exit(0); }); }, /** * Challenge a task result by initiating a re-execution through the reexecution endpoint * * @param taskId The ID of the task to challenge * @param account The account to use for the transaction * @returns Transaction information for the challenge */ async challenge(task, account) { const taskId = task.id; const challengeLog = serviceLog.getSubLogger({ name: `Challenge-${taskId}` }); const walletClient = (0, viem_1.createWalletClient)({ chain, account: account, transport: config.l2rpcUrl?.startsWith('wss://') ? (0, viem_1.webSocket)(config.l2rpcUrl) : (0, viem_1.http)(config.l2rpcUrl) }); challengeLog.info(`Initiating challenge for task #${taskId}`); try { // Convert taskId to BigInt for consistency const taskIdBigInt = taskId; // Check if the task exists and has been completed const [exists, completed, failed, _finalResponse] = await taskDispatchRead.read.getTaskStatus([taskIdBigInt], { blockTag }); if (!exists) { throw new Error(`Task #${taskId} does not exist`); } if (!completed && !failed) { throw new Error(`Task #${taskId} is still in progress and cannot be challenged yet`); } if (failed) { throw new Error(`Task #${taskId} failed and has no result to challenge`); } // Get task details to build the proper challenge payload const [serviceName, _serviceVersion, functionName, args, _stakeThreshold, _remainingStake] = await taskDispatchRead.read.getTaskDetails([taskIdBigInt], { blockTag }); challengeLog.info(`Task details: ${serviceName}/${functionName}`); // Find the function info to properly decode the arguments const funcInfo = __navsRegistry[functionName]; if (!funcInfo) { throw new Error(`Cannot challenge task: function ${functionName} not registered in NAVS`); } // Build the challenge payload - this contains all the information needed for re-execution const taskDetailsBytes = (0, utils_1.encodeArgs)(['string', 'string', 'bytes'], [serviceName, functionName, args]); // Get the reexecution endpoint contract const reexecutionEndpoint = (0, viem_1.getContract)({ abi: deploy_1.ReexecutionEndpointAbi, address: config.reexecutionEndpoint, client: walletClient }); challengeLog.info(`Submitting challenge to reexecution endpoint: ${config.reexecutionEndpoint}`); // Submit the challenge const tx = await reexecutionEndpoint.write.requestReexecution([ NAVS_IMAGE_ID, (0, viem_1.toHex)(0), // the "syncPayload", which we do not use. (0, viem_1.toHex)(taskId), taskDetailsBytes, ]); // Wait for the transaction to be mined const receipt = await publicClient.waitForTransactionReceipt({ hash: tx }); challengeLog.info(`Challenge submitted successfully in transaction: ${tx}`); return { transactionHash: tx, blockNumber: receipt.blockNumber, taskId: taskIdBigInt.toString() }; } catch (error) { challengeLog.error(`Failed to challenge task:`, error); throw error; } }, /** * Get a list of all registered navs functions * * @returns Array of registered function information */ getRegisteredFunctions() { return Object.keys(__navsRegistry).map(key => ({ name: key, serviceName: __navsRegistry[key].serviceName, paramTypes: __navsRegistry[key].paramTypes, returnType: __navsRegistry[key].returnType })); }, /** * Execute a function through the TaskDispatch contract with full type safety * * @param fn The function reference to execute (must be decorated with @navs) * @param args The arguments matching the function's parameter types * @param options Execution options including account and stake threshold * @returns A typed TaskId that can be awaited for the result */ async execute(fn, args, options) { const functionName = Object.keys(__navsRegistry).find(key => { return __navsRegistry[key].fn === fn; }); if (!functionName) { throw new Error(`Function is not registered with @navs decorator`); } // Get the return type const funcInfo = __navsRegistry[functionName]; // Execute the task const taskId = await this.executeByName(functionName, args, options.stakeThreshold, options.account); // Return a typed TaskId object that can be awaited return createTypedTaskId(taskId, funcInfo.returnType); }, /** * Execute a function through the TaskDispatch contract (string-based version) * * @deprecated Use the type-safe execute method with function references instead * @param functionName The function name to call * @param args The arguments to pass * @param stakeThreshold The minimum stake threshold for agreement * @param account The account to use for the transaction * @returns The task ID (raw bigint) */ async executeByName(functionName, args, stakeThreshold = 1n, account) { const funcInfo = __navsRegistry[functionName]; if (!funcInfo) { throw new Error(`Function ${functionName} is not registered with navs`); } const encodedArgs = (0, utils_1.encodeArgs)(funcInfo.paramTypes, args); const walletClient = (0, viem_1.createWalletClient)({ chain, account: account, transport: config.l2rpcUrl?.startsWith('wss://') ? (0, viem_1.webSocket)(config.l2rpcUrl) : (0, viem_1.http)(config.l2rpcUrl) }); const taskDispatchWrite = (0, viem_1.getContract)({ abi: deploy_1.TaskDispatchAbi, address: config.taskDispatchL2, client: walletClient }); console.log(`Connected to TaskDispatch::${config.taskDispatchL2}`); const tx = await taskDispatchWrite.write.submitTask([ config.serviceName, config.serviceVersion, functionName, encodedArgs, stakeThreshold, funcInfo.consensus !== undefined ? ConsensusType.CUSTOM : ConsensusType.EXACT_MATCH, false, // isConsensus "0x0000000000000000000000000000000000000000", // callbackReceiver - not used in direct execution false, // isLocal - this is regular operator execution, not local development funcInfo.deterministic || (funcInfo.consensus !== undefined) || false // deterministic - auto-mark consensus tasks as deterministic ]); const receipt = await publicClient.waitForTransactionReceipt({ hash: tx }); console.log(`Got transaction: ${receipt.transactionHash}`); const events = (0, viem_1.parseEventLogs)({ logs: receipt.logs, abi: deploy_1.TaskDispatchAbi, eventName: 'TaskRequested' }); const task = events.map(event => event.args)[0]; if (!task) { throw new Error(`Failed to parse task ID from transaction logs (got ${events.length} matches)`, { cause: JSON.stringify(events, null, 2) }); } return task.taskId; }, /** * Execute a function by name and return a typed TaskId that can be awaited * * @param functionName The name of the function to execute * @param args The arguments to pass to the function * @param options Options for execution including account and stake threshold * @returns A typed TaskId that can be awaited for the result */ async executeByNameTyped(functionName, args, options) { const funcInfo = __navsRegistry[functionName]; if (!funcInfo) { throw new Error(`Function ${functionName} is not registered with navs`); } const taskId = await this.executeByName(functionName, args, options.stakeThreshold, options.account); return createTypedTaskId(taskId, funcInfo.returnType); }, /** * Get the status of a task * * @param taskId The ID of the task to check * @returns Task status information */ async getTaskStatus(taskId) { const result = await taskDispatchRead.read.getTaskStatus([taskId], { blockTag }); const [exists, completed, failed, finalResponse] = result; return { exists, completed, failed, finalResponse }; }, /** * Creates a typed TaskId wrapper for an existing task * * @param taskId The task ID (bigint or string) * @param expectedReturnType The expected return type for proper decoding * @returns A typed TaskId that can be awaited for the result */ async getTask(taskId, expectedReturnType) { const id = BigInt(taskId); // Verify the task exists const status = await this.getTaskStatus(id); if (!status.exists) { throw new Error(`Task #${id} does not exist`); } return createTypedTaskId(id, expectedReturnType); }, /** * Get the details of a task * * @param taskId The ID of the task to check * @returns Task details */ async getTaskDetails(taskId) { const result = await taskDispatchRead.read.getTaskDetails([taskId], { blockTag }); const [serviceName, serviceVersion, functionName, args, stakeThreshold, remainingStake] = result; return { serviceName, serviceVersion, functionName, args, stakeThreshold, remainingStake }; }, /** * Decorator function for registering functions with NAVS * * @param onResponse Optional consensus function for handling multiple operator responses * @param config Optional configuration object with deterministic flag (deprecated - not fully implemented) * @returns A decorator function */ navs(onResponse, navsConfig) { return function (target, propertyKey, descriptor) { // Ensure target is a regular function if (typeof descriptor.value !== 'function') { throw new Error(`@navs can only be applied to functions, but ${propertyKey} is not a function`); } const fn = descriptor.value; // Use TypeScript reflection metadata to get parameter types const paramTypes = Reflect.getMetadata(PARAM_TYPES_METADATA, target, propertyKey); let mappedParamTypes; if (paramTypes) { // Get parameter names to help with type inference const paramNames = (0, utils_1.extractParamNamesFromString)(fn); mappedParamTypes = paramTypes.map((type, index) => { const reflectionType = (0, utils_1.getTypeName)(type); // Special case: TypeScript reflection metadata doesn't preserve template literal types // For parameters that clearly are addresses (based on parameter name), map string to address if (reflectionType === 'string' && paramNames[index] && paramNames[index].toLowerCase().includes('address')) { return 'address'; } return reflectionType; }); } else { // Fallback to string parsing mappedParamTypes = (0, utils_1.extractParamTypesFromString)(fn); } // Get return type using reflection const returnTypeMetadata = Reflect.getMetadata(RETURN_TYPE_METADATA, target, propertyKey); const returnType = returnTypeMetadata ? (0, utils_1.getTypeName)(returnTypeMetadata) : 'unknown'; // Create a decoder function for this function's argument types const decode = (0, utils_1.createDecoder)(mappedParamTypes); // Register the function with the service name from config __navsRegistry[propertyKey] = { fn, paramTypes: mappedParamTypes, returnType, serviceName: config.serviceName, decodeArgs: decode, consensus: onResponse, deterministic: navsConfig?.deterministic }; // Return the original descriptor return descriptor; }; } }; }; exports.navs = navs;