navskit
Version:
Deploy TypeScript logic on Ethereum. Includes core library, CLI tools, and utilities.
785 lines (784 loc) • 38.8 kB
JavaScript
;
/**
* 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;