UNPKG

@nosana/kit

Version:

Nosana KIT

493 lines 18.9 kB
import bs58 from 'bs58'; import { solBytesArrayToIpfsHash } from '@nosana/ipfs'; import { parseBase64RpcAccount } from '@solana/kit'; import { ErrorCodes, NosanaError } from '../../../errors/NosanaError.js'; import { convertBigIntToNumber } from '../../../utils/index.js'; import { getStaticAccounts as getStaticAccountsFn, } from '../../../utils/getStaticAccounts.js'; import * as Instructions from './instructions/index.js'; import * as programClient from '../../../generated_clients/jobs/index.js'; import { createMonitorFunctions } from './monitor/index.js'; export var JobState; (function (JobState) { JobState[JobState["QUEUED"] = 0] = "QUEUED"; JobState[JobState["RUNNING"] = 1] = "RUNNING"; JobState[JobState["COMPLETED"] = 2] = "COMPLETED"; JobState[JobState["STOPPED"] = 3] = "STOPPED"; })(JobState || (JobState = {})); export var MarketQueueType; (function (MarketQueueType) { MarketQueueType[MarketQueueType["JOB_QUEUE"] = 0] = "JOB_QUEUE"; MarketQueueType[MarketQueueType["NODE_QUEUE"] = 1] = "NODE_QUEUE"; })(MarketQueueType || (MarketQueueType = {})); // Re-export monitor types for convenience export { MonitorEventType } from './monitor/index.js'; /** * Creates a new JobsProgram instance. * * @param deps - Program dependencies (config, logger, solana service, wallet getter) * @returns A JobsProgram instance with methods to interact with the jobs program * * @example * ```ts * import { createJobsProgram } from '@nosana/kit'; * * const jobsProgram = createJobsProgram({ * config, * logger, * solana, * getWallet, * }); * * const job = await jobsProgram.get('job-address'); * ``` */ export function createJobsProgram(deps, config) { const programId = config.jobsAddress; const client = programClient; // Cache for static accounts (memoization) const staticAccountsCache = {}; /** * Transform job account to include address and convert types */ /** * Convert Solana bytes array to IPFS hash, returning null for empty/invalid hashes */ function solBytesToIpfsHashOrNull(hashArray) { const result = solBytesArrayToIpfsHash(Array.from(hashArray)); // Return null for the empty hash value if (result === 'QmNLei78zWmzUdbeRB3CiUfAizWUrbeeZh5K1rhAQKCh51') { return null; } return result; } function transformJobAccount(jobAccount) { const { discriminator: _, ...jobAccountData } = jobAccount.data; const converted = convertBigIntToNumber(jobAccountData); return { address: jobAccount.address, ...converted, ipfsJob: solBytesToIpfsHashOrNull(jobAccountData.ipfsJob), ipfsResult: solBytesToIpfsHashOrNull(jobAccountData.ipfsResult), state: converted.state, }; } /** * Transform run account to include address and convert types */ function transformRunAccount(runAccount) { const { discriminator: _, ...runAccountData } = runAccount.data; return { address: runAccount.address, ...convertBigIntToNumber(runAccountData), }; } /** * Merge run account data into a job account. * Updates the job state to RUNNING and sets node and timeStart from the run account. */ function mergeRunIntoJob(job, run) { return { ...job, state: JobState.RUNNING, node: run.node, timeStart: run.time, }; } /** * Transform market account to include address and convert types */ function transformMarketAccount(marketAccount) { const { discriminator: _, ...marketAccountData } = marketAccount.data; const converted = convertBigIntToNumber(marketAccountData); return { address: marketAccount.address, ...converted, queueType: converted.queueType, }; } /** * Get the required wallet or throw an error if not available */ function getRequiredWallet() { const wallet = deps.getWallet(); if (!wallet) { throw new NosanaError('Wallet is required for this operation', ErrorCodes.NO_WALLET); } return wallet; } function getStaticAccounts() { return getStaticAccountsFn(config, deps.solana, staticAccountsCache); } function createInstructionsHelper(get, getRuns) { return { deps, config, client, get, getRuns, getRequiredWallet, getStaticAccounts, getNosATA: deps.nos.getATA, }; } return { /** * Fetch a job account by address */ async get(addr, checkRun = true) { try { const jobAccount = await client.fetchJobAccount(deps.solana.rpc, addr); const job = transformJobAccount(jobAccount); if (checkRun && job.state === JobState.QUEUED) { // If job is queued, check if there is a run account for the job const runs = await this.runs({ job: job.address }); if (runs.length > 0) { const run = runs[0]; job.state = JobState.RUNNING; job.timeStart = run.time; job.node = run.node; } } return job; } catch (err) { deps.logger.error(`Failed to fetch job ${err}`); throw err; } }, /** * Fetch a run account by address */ async run(addr) { try { const runAccount = await client.fetchRunAccount(deps.solana.rpc, addr); const run = transformRunAccount(runAccount); return run; } catch (err) { deps.logger.error(`Failed to fetch run ${err}`); throw err; } }, /** * Fetch a market account by address */ async market(addr) { try { const marketAccount = await client.fetchMarketAccount(deps.solana.rpc, addr); const market = transformMarketAccount(marketAccount); return market; } catch (err) { deps.logger.error(`Failed to fetch market ${err}`); throw err; } }, /** * Fetch multiple job accounts by address */ async multiple(addresses, checkRuns = false) { try { const jobAccounts = await client.fetchAllJobAccount(deps.solana.rpc, addresses); const jobs = jobAccounts.map((jobAccount) => transformJobAccount(jobAccount)); if (checkRuns) { const runs = await this.runs(); jobs.forEach((job) => { if (job.state === JobState.QUEUED) { const run = runs.find((run) => run.job === job.address); if (run) { job.state = JobState.RUNNING; job.timeStart = run.time; job.node = run.node; } } }); } return jobs; } catch (err) { deps.logger.error(`Failed to fetch job ${err}`); throw err; } }, /** * Fetch all job accounts */ async all(filters, checkRuns = false) { try { const extraGPAFilters = []; if (filters) { if (typeof filters.state === 'number') { extraGPAFilters.push({ memcmp: { offset: BigInt(208), bytes: bs58.encode(Buffer.from([filters.state])), encoding: 'base58', }, }); } if (filters.project) { extraGPAFilters.push({ memcmp: { offset: BigInt(176), bytes: filters.project.toString(), encoding: 'base58', }, }); } if (filters.node) { extraGPAFilters.push({ memcmp: { offset: BigInt(104), bytes: filters.node.toString(), encoding: 'base58', }, }); } if (filters.market) { extraGPAFilters.push({ memcmp: { offset: BigInt(72), bytes: filters.market.toString(), encoding: 'base58', }, }); } } const getProgramAccountsResponse = await deps.solana.rpc .getProgramAccounts(programId, { encoding: 'base64', filters: [ { memcmp: { offset: BigInt(0), bytes: bs58.encode(Buffer.from(client.JOB_ACCOUNT_DISCRIMINATOR)), encoding: 'base58', }, }, ...extraGPAFilters, ], }) .send(); const jobs = getProgramAccountsResponse .map((result) => { try { const jobAccount = client.decodeJobAccount(parseBase64RpcAccount(result.pubkey, result.account)); return transformJobAccount(jobAccount); } catch (err) { deps.logger.error(`Failed to decode job ${err}`); return null; } }) .filter((account) => account !== null); if (checkRuns) { const runs = await this.runs(); jobs.forEach((job) => { if (job.state === JobState.QUEUED) { const run = runs.find((run) => run.job === job.address); if (run) { job.state = JobState.RUNNING; job.timeStart = run.time; job.node = run.node; } } }); } return jobs; } catch (err) { deps.logger.error(`Failed to fetch all jobs ${err}`); throw err; } }, /** * Fetch all run accounts */ async runs(filters) { try { const extraGPAFilters = []; if (filters) { if (filters.node) { extraGPAFilters.push({ memcmp: { offset: BigInt(40), bytes: filters.node.toString(), encoding: 'base58', }, }); } if (filters.job) { extraGPAFilters.push({ memcmp: { offset: BigInt(8), bytes: filters.job.toString(), encoding: 'base58', }, }); } } const getProgramAccountsResponse = await deps.solana.rpc .getProgramAccounts(programId, { encoding: 'base64', filters: [ { memcmp: { offset: BigInt(0), bytes: bs58.encode(Buffer.from(client.RUN_ACCOUNT_DISCRIMINATOR)), encoding: 'base58', }, }, ], }) .send(); const runAccounts = getProgramAccountsResponse .map((result) => { try { const runAccount = client.decodeRunAccount(parseBase64RpcAccount(result.pubkey, result.account)); return transformRunAccount(runAccount); } catch (err) { deps.logger.error(`Failed to decode run ${err}`); return null; } }) .filter((account) => account !== null); return runAccounts; } catch (err) { deps.logger.error(`Failed to fetch all runs ${err}`); throw err; } }, /** * Fetch all market accounts */ async markets() { try { const getProgramAccountsResponse = await deps.solana.rpc .getProgramAccounts(programId, { encoding: 'base64', filters: [ { memcmp: { offset: BigInt(0), bytes: bs58.encode(Buffer.from(client.MARKET_ACCOUNT_DISCRIMINATOR)), encoding: 'base58', }, }, ], }) .send(); const marketAccounts = getProgramAccountsResponse .map((result) => { try { const marketAccount = client.decodeMarketAccount(parseBase64RpcAccount(result.pubkey, result.account)); return transformMarketAccount(marketAccount); } catch (err) { deps.logger.error(`Failed to decode market ${err}`); return null; } }) .filter((account) => account !== null); return marketAccounts; } catch (err) { deps.logger.error(`Failed to fetch all markets ${err}`); throw err; } }, /** * Post a new job to the marketplace */ async post(params) { return Instructions.post(params, createInstructionsHelper(this.get, this.runs)); }, async extend(params) { return Instructions.extend(params, createInstructionsHelper(this.get, this.runs)); }, async delist(params) { return Instructions.delist(params, createInstructionsHelper(this.get, this.runs)); }, async end(params) { return Instructions.end(params, createInstructionsHelper(this.get, this.runs)); }, /** * Monitor program account updates using async iterators. * Automatically merges run account data into job account updates. * Uses WebSocket subscriptions with automatic restart on failure. * * @example * ```typescript * // Example: Simple monitoring - run accounts are automatically merged into job updates * const [eventStream, stop] = await jobsProgram.monitor(); * for await (const event of eventStream) { * if (event.type === MonitorEventType.JOB) { * console.log('Job updated:', event.data.address.toString()); * // event.data will have state, node, and timeStart from run account if it exists * } else if (event.type === MonitorEventType.MARKET) { * console.log('Market updated:', event.data.address.toString()); * } * } * // Stop monitoring when done * stop(); * ``` * * @returns A tuple of [eventStream, stopFunction] */ async monitor() { const monitorFunctions = createMonitorFunctions(this.get, this.runs, { deps, config, client, transformJobAccount, transformRunAccount, transformMarketAccount, mergeRunIntoJob, }); return monitorFunctions.monitor(); }, /** * Monitor program account updates with detailed events for each account type. * Uses WebSocket subscriptions with automatic restart on failure. * Provides separate events for job, market, and run accounts. * * @example * ```typescript * // Example: Monitor job accounts and save to file * const [eventStream, stop] = await jobsProgram.monitorDetailed(); * for await (const event of eventStream) { * switch (event.type) { * case MonitorEventType.JOB: * console.log('Job updated:', event.data.address.toString()); * break; * case MonitorEventType.MARKET: * console.log('Market updated:', event.data.address.toString()); * break; * case MonitorEventType.RUN: * console.log('Run updated:', event.data.address.toString()); * break; * } * } * // Stop monitoring when done * stop(); * ``` * * @returns A tuple of [eventStream, stopFunction] */ async monitorDetailed() { const monitorFunctions = createMonitorFunctions(this.get, this.runs, { deps, config, client, transformJobAccount, transformRunAccount, transformMarketAccount, mergeRunIntoJob, }); return monitorFunctions.monitorDetailed(); }, }; } //# sourceMappingURL=JobsProgram.js.map