UNPKG

@nosana/kit

Version:

Nosana KIT

1,702 lines (1,280 loc) 45.5 kB
# Nosana Kit TypeScript SDK for interacting with the Nosana Network on Solana. Provides comprehensive tools for managing jobs, markets, runs, and protocol operations on the Nosana decentralized compute network. > **v2.0.0** - Major release featuring functional architecture, universal wallet support, and enhanced transaction handling. See [CHANGELOG.md](./CHANGELOG.md) for migration guide. ## Installation ```bash npm install @nosana/kit ``` ### Requirements - Node.js >= 20.18.0 - TypeScript >= 5.3.0 (for development) - pnpm >= 9.15.0 (for development) ## Quick Start ```typescript import { createNosanaClient, NosanaNetwork } from '@nosana/kit'; // Initialize with mainnet defaults const client = createNosanaClient(); // Or specify network and configuration const client = createNosanaClient(NosanaNetwork.DEVNET, { solana: { rpcEndpoint: 'https://your-custom-rpc.com', commitment: 'confirmed', }, }); // Fetch a job by address const job = await client.jobs.get('job-address'); console.log('Job state:', job.state); // Query jobs with filters const completedJobs = await client.jobs.all({ market: 'market-address', state: 2, // JobState.COMPLETED }); ``` ## Architecture The SDK uses a functional architecture with factory functions for improved modularity and testability: - **`services/`** - Utility services and program interfaces - **`SolanaService`** - Low-level Solana RPC operations, transactions, and PDA derivations - **`TokenService`** - Token account operations (configured for NOS token) - **`programs/`** - On-chain program interfaces - **`JobsProgram`** - Jobs, runs, and markets management - **`StakeProgram`** - Staking account operations - **`MerkleDistributorProgram`** - Merkle distributor and claim operations - **`ipfs/`** - IPFS integration for pinning and retrieving data - **`config/`** - Network configurations and defaults - **`utils/`** - Helper utilities and type conversions - **`packages/generated_clients/`** - Auto-generated Solana program clients (exported as namespaces) All components use factory functions with explicit dependency injection, making the codebase modular, testable, and maintainable. ## Configuration ### Networks The SDK supports two networks: - **`NosanaNetwork.MAINNET`** - Production network (mainnet-beta) - **`NosanaNetwork.DEVNET`** - Development network (devnet) ### Configuration Options ```typescript import { createNosanaClient, NosanaNetwork, LogLevel } from '@nosana/kit'; const client = createNosanaClient(NosanaNetwork.MAINNET, { solana: { cluster: 'mainnet-beta', rpcEndpoint: 'https://api.mainnet-beta.solana.com', commitment: 'confirmed', // Optional: priority fees (fixed or dynamic). Default configs use dynamic with strategy 'medium'. // priorityFees: { type: 'fixed', microLamports: 10_000 }, // priorityFees: { type: 'dynamic', strategy: 'medium', min: 10_000, max: 15_000_000 }, }, ipfs: { api: 'https://api.pinata.cloud', jwt: 'your-pinata-jwt-token', gateway: 'https://gateway.pinata.cloud/ipfs/', }, api: { apiKey: 'your-api-key', // Optional: API key for authentication }, logLevel: LogLevel.DEBUG, wallet: myWallet, // Optional: Set wallet during initialization (must be a Wallet type) }); ``` ### Priority fees Transactions built via `solana.buildTransaction` or `solana.buildSignAndSend` can include a priority fee (compute unit price) when `solana.priorityFees` is set: - **Fixed** – `{ type: 'fixed', microLamports: number }` uses the same microLamports per compute unit every time. - **Dynamic** – `{ type: 'dynamic', strategy?: 'low'|'medium'|'high', percentile?: number, min?: number, max?: number, accountAddresses?: Address[] }` fetches recent fees from the RPC (`getRecentPrioritizationFees`), takes a percentile (or strategy preset), and clamps to min/max. On empty or error it falls back to `min`. Default configs use dynamic with strategy `medium`, min 10k, max 15M, and SOL + USDC mints for fee estimation. ## Core Components ### NosanaClient Main entry point for SDK interactions. Created using the `createNosanaClient()` factory function. **Properties:** - `config: ClientConfig` - Active configuration - `jobs: JobsProgram` - Jobs program interface - `stake: StakeProgram` - Staking program interface - `merkleDistributor: MerkleDistributorProgram` - Merkle distributor program interface - `solana: SolanaService` - General Solana utilities (RPC, transactions, PDAs) - `nos: TokenService` - Token operations service (configured for NOS token) - `api: NosanaApi | undefined` - Nosana API client for interacting with Nosana APIs (jobs, credits, markets) - `ipfs: ReturnType<typeof createIpfsClient>` - IPFS operations for pinning and retrieving data - `authorization: NosanaAuthorization | Omit<NosanaAuthorization, 'generate' | 'generateHeaders'>` - Authorization service for message signing and validation - `logger: Logger` - Logging instance - `wallet?: Wallet` - Active wallet (if set). Set this property directly to configure the wallet. **Factory Function:** - `createNosanaClient(network?: NosanaNetwork, customConfig?: PartialClientConfig): NosanaClient` - Creates a new client instance ### Wallet Configuration The SDK supports universal wallet configuration through a unified `Wallet` type that must support both message and transaction signing (`MessageSigner & TransactionSigner`). This enables compatibility with both browser wallets (wallet-standard) and keypair-based wallets. #### Keypair Helpers The SDK provides convenient helper functions so you don't need to install `@solana/kit` directly: ```typescript import { createNosanaClient, generateWallet, loadWalletFromFile, createWalletFromBase58, createWalletFromBytes, } from '@nosana/kit'; // Generate a new random wallet const wallet = await generateWallet(); // Load from Solana CLI keypair file (defaults to ~/.config/solana/id.json) const wallet2 = await loadWalletFromFile(); const wallet3 = await loadWalletFromFile('/path/to/keypair.json'); // Create from a base58-encoded private key const wallet4 = await createWalletFromBase58('5MaiiCavjCmn9Hs...'); // Create from raw bytes (Uint8Array or number[]) const wallet5 = await createWalletFromBytes(new Uint8Array([174, 47, 154, ...])); const client = createNosanaClient(); client.wallet = wallet; ``` #### Browser Wallets (Wallet-Standard) Full support for wallet-standard compatible browser wallets (Phantom, Solflare, etc.): ```typescript import { createNosanaClient } from '@nosana/kit'; import { useWalletAccountSigner } from '@nosana/solana-vue'; // Create client const client = createNosanaClient(); // Set browser wallet (wallet-standard compatible) client.wallet = useWalletAccountSigner(account, currentChain); ``` #### Configuration Options Wallets can be set at client initialization or dynamically assigned: ```typescript import { createNosanaClient, NosanaNetwork, generateWallet } from '@nosana/kit'; const myWallet = await generateWallet(); // Option 1: Set wallet during initialization const client = createNosanaClient(NosanaNetwork.MAINNET, { wallet: myWallet, }); // Option 2: Set wallet dynamically const client2 = createNosanaClient(); client2.wallet = myWallet; // Option 3: Change wallet at runtime client2.wallet = anotherWallet; ``` ## Jobs Program API ### Fetching Accounts #### Get Single Job ```typescript async get(address: Address, checkRun?: boolean): Promise<Job> ``` Fetch a job account. If `checkRun` is true (default), automatically checks for associated run accounts to determine if a queued job is actually running. ```typescript const job = await client.jobs.get('job-address'); console.log(job.state); // JobState enum console.log(job.price); // Job price in smallest unit console.log(job.ipfsJob); // IPFS CID of job definition console.log(job.timeStart); // Start timestamp (if running) ``` #### Get Single Run ```typescript async run(address: Address): Promise<Run> ``` Fetch a run account by address. ```typescript const run = await client.jobs.run('run-address'); console.log(run.job); // Associated job address console.log(run.node); // Node executing the run console.log(run.time); // Run start time ``` #### Get Single Market ```typescript async market(address: Address): Promise<Market> ``` Fetch a market account by address. ```typescript const market = await client.jobs.market('market-address'); console.log(market.queueType); // MarketQueueType enum console.log(market.jobPrice); // Market job price ``` #### Get Multiple Jobs ```typescript async multiple(addresses: Address[], checkRuns?: boolean): Promise<Job[]> ``` Batch fetch multiple jobs by addresses. ```typescript const jobs = await client.jobs.multiple(['job-address-1', 'job-address-2', 'job-address-3'], true); ``` ### Querying with Filters #### Query All Jobs ```typescript async all(filters?: { state?: JobState, market?: Address, node?: Address, project?: Address }, checkRuns?: boolean): Promise<Job[]> ``` Fetch all jobs matching filter criteria using getProgramAccounts. ```typescript import { JobState } from '@nosana/kit'; // Get all running jobs in a market const runningJobs = await client.jobs.all({ state: JobState.RUNNING, market: 'market-address', }); // Get all jobs for a project const projectJobs = await client.jobs.all({ project: 'project-address', }); ``` #### Query All Runs ```typescript async runs(filters?: { job?: Address, node?: Address }): Promise<Run[]> ``` Fetch runs with optional filtering. ```typescript // Get all runs for a specific job const jobRuns = await client.jobs.runs({ job: 'job-address' }); // Get all runs on a specific node const nodeRuns = await client.jobs.runs({ node: 'node-address' }); ``` #### Query All Markets ```typescript async markets(): Promise<Market[]> ``` Fetch all market accounts. ```typescript const markets = await client.jobs.markets(); ``` ### Creating Jobs #### Post a Job ```typescript async post(params: { market: Address, timeout: number | bigint, ipfsHash: string, node?: Address }): Promise<Instruction> ``` Create a job instruction that can either list a job to a market or assign it directly to a node, depending on whether the `node` parameter is provided. Returns an instruction that must be submitted to the network. ```typescript // Set wallet first client.wallet = yourWallet; // Create job instruction (will list to market if node is not provided) const instruction = await client.jobs.post({ market: address('market-address'), timeout: 3600, // Timeout in seconds ipfsHash: 'QmXxx...', // IPFS CID of job definition node: address('node-address'), // Optional: target specific node (assigns directly if provided) }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` #### List a Job ```typescript async list(params: { market: Address, timeout: number | bigint, ipfsHash: string, payer?: TransactionSigner }): Promise<Instruction> ``` List a new job to a market queue. The job will be available for nodes to pick up. ```typescript // Set wallet first client.wallet = yourWallet; // List job to market const instruction = await client.jobs.list({ market: address('market-address'), timeout: 3600, // Timeout in seconds ipfsHash: 'QmXxx...', // IPFS CID of job definition }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` #### Assign a Job ```typescript async assign(params: { market: Address, node: Address, timeout: number | bigint, ipfsHash: string, payer?: TransactionSigner }): Promise<Instruction> ``` Assign a job directly to a specific node, bypassing the market queue. ```typescript // Set wallet first client.wallet = yourWallet; // Assign job directly to node const instruction = await client.jobs.assign({ market: address('market-address'), node: address('node-address'), timeout: 3600, // Timeout in seconds ipfsHash: 'QmXxx...', // IPFS CID of job definition }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` ### Managing Jobs #### Extend Job Timeout ```typescript async extend(params: { job: Address, timeout: number | bigint, payer?: TransactionSigner }): Promise<Instruction> ``` Extend an existing job's timeout by adding the specified amount to the current timeout. ```typescript // Set wallet first client.wallet = yourWallet; // Extend job timeout by 1800 seconds (30 minutes) const instruction = await client.jobs.extend({ job: address('job-address'), timeout: 1800, // Additional seconds to add }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` #### Delist a Job ```typescript async delist(params: { job: Address }): Promise<Instruction> ``` Remove a job from the market queue. The job's deposit will be returned to the payer. ```typescript // Set wallet first client.wallet = yourWallet; // Delist job from market const instruction = await client.jobs.delist({ job: address('job-address'), }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` #### Stop a Running Job ```typescript async end(params: { job: Address }): Promise<Instruction> ``` Stop a job that is currently running. The job must have an associated run account. ```typescript // Set wallet first client.wallet = yourWallet; // Stop a running job const instruction = await client.jobs.end({ job: address('job-address'), }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` ### Completing Jobs #### Work (Enter Queue or Create Run) ```typescript async work(params: { market: Address, nft?: Address }): Promise<Instruction> ``` Enter a market's node queue or create a run account if a job is available. If an NFT is provided, it will be used for node verification. ```typescript // Set wallet first (must be a node wallet) client.wallet = nodeWallet; // Enter market queue or create run const instruction = await client.jobs.work({ market: address('market-address'), nft: 'nft-address', // Optional: NFT for node verification }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` #### Finish a Stopped Job ```typescript async finish(params: { job: Address, ipfsResultsHash: string }): Promise<Instruction | Instruction[]> ``` Finish a job that has been stopped. This posts the result IPFS hash and may return multiple instructions if associated token accounts need to be created. ```typescript // Set wallet first client.wallet = yourWallet; // Finish a stopped job with results const instructions = await client.jobs.finish({ job: address('job-address'), ipfsResultsHash: 'QmYyy...', // IPFS CID of job results }); // Submit the instruction(s) - may be array if ATA creation is needed await client.solana.buildSignAndSend(instructions); ``` #### Complete a Job ```typescript async complete(params: { job: Address, ipfsResultsHash: string }): Promise<Instruction> ``` Complete a job that is in the COMPLETED state by posting the final result IPFS hash. This finalizes the job and allows payment processing. ```typescript // Set wallet first client.wallet = yourWallet; // Complete a job with final results const instruction = await client.jobs.complete({ job: address('job-address'), ipfsResultsHash: 'QmYyy...', // IPFS CID of final job results }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` #### Quit a Run ```typescript async quit(params: { run: Address }): Promise<Instruction> ``` Quit a run account, removing the node from the job execution. ```typescript // Set wallet first (must be the node wallet) client.wallet = nodeWallet; // Quit a run const instruction = await client.jobs.quit({ run: address('run-address'), }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` #### Stop (Exit Node Queue) ```typescript async stop(params: { market: Address, node?: Address }): Promise<Instruction> ``` Exit a market's node queue. If no node is specified, uses the wallet's address. ```typescript // Set wallet first (must be a node wallet) client.wallet = nodeWallet; // Exit market queue const instruction = await client.jobs.stop({ market: address('market-address'), node: address('node-address'), // Optional: defaults to wallet address }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` ### Market Management #### Open a Market ```typescript async open(params: { nodeAccessKey?: Address, jobExpiration?: number | bigint, jobType?: number, jobPrice?: number | bigint, jobTimeout?: number | bigint, nodeStakeMinimum?: number | bigint, payer?: TransactionSigner }): Promise<Instruction> ``` Create a new market. Returns an instruction that creates a market account. Default values: `jobExpiration` = 86400 (24 hours), `jobTimeout` = 7200 (120 minutes), `nodeStakeMinimum` = 0, `nodeAccessKey` = system program. ```typescript // Set wallet first client.wallet = yourWallet; // Create a new market with default values const instruction = await client.jobs.open({}); // Or create with custom parameters const instruction = await client.jobs.open({ jobPrice: 10, // Nos per second in smallest unit jobTimeout: 3600, // 60 minutes jobExpiration: 172800, // 48 hours nodeStakeMinimum: 5000000, // Minimum stake required nodeAccessKey: address('access-key-address'), // Optional: defaults to system program }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` #### Create Market (Synonym) ```typescript async createMarket(params: OpenParams): Promise<OpenInstruction> ``` Synonym for `open()`. Creates a new market with the same parameters. ```typescript // Set wallet first client.wallet = yourWallet; // Create market using synonym const instruction = await client.jobs.createMarket({ jobPrice: 10, jobTimeout: 3600, }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` #### Close a Market ```typescript async close(params: { market: Address, payer?: TransactionSigner }): Promise<Instruction> ``` Close an existing market. This will return any remaining funds to the market authority. ```typescript // Set wallet first (must be market authority) client.wallet = yourWallet; // Close a market const instruction = await client.jobs.close({ market: address('market-address'), }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` #### Close Market (Synonym) ```typescript async closeMarket(params: CloseParams): Promise<CloseInstruction> ``` Synonym for `close()`. Closes a market with the same parameters. ```typescript // Set wallet first (must be market authority) client.wallet = yourWallet; // Close market using synonym const instruction = await client.jobs.closeMarket({ market: address('market-address'), }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` ### Real-time Monitoring #### Monitor Account Updates The SDK provides two monitoring methods using async iterators for real-time account updates via WebSocket: **Simple Monitoring** (`monitor()`) - Automatically merges run account data into job events: ```typescript async monitor(): Promise<[AsyncIterable<SimpleMonitorEvent>, () => void]> ``` ```typescript import { MonitorEventType } from '@nosana/kit'; // Start monitoring const [eventStream, stop] = await client.jobs.monitor(); // Process events using async iteration for await (const event of eventStream) { if (event.type === MonitorEventType.JOB) { console.log('Job update:', event.data.address, event.data.state); // event.data will have state, node, and timeStart from run account if it exists // Process updates - save to database, trigger workflows, etc. if (event.data.state === JobState.COMPLETED) { await processCompletedJob(event.data); } } else if (event.type === MonitorEventType.MARKET) { console.log('Market update:', event.data.address); } } // Stop monitoring when done stop(); ``` **Detailed Monitoring** (`monitorDetailed()`) - Provides separate events for job, market, and run accounts: ```typescript async monitorDetailed(): Promise<[AsyncIterable<MonitorEvent>, () => void]> ``` ```typescript import { MonitorEventType } from '@nosana/kit'; // Start detailed monitoring const [eventStream, stop] = await client.jobs.monitorDetailed(); // Process events using async iteration for await (const event of eventStream) { switch (event.type) { case MonitorEventType.JOB: console.log('Job update:', event.data.address); break; case MonitorEventType.MARKET: console.log('Market update:', event.data.address); break; case MonitorEventType.RUN: console.log('Run started:', event.data.job, 'on node', event.data.node); break; } } // Stop monitoring when done stop(); ``` Both methods handle WebSocket reconnection automatically and continue processing updates until explicitly stopped. The simple `monitor()` method is recommended for most use cases as it automatically merges run account data into job updates, eliminating the need to manually track run accounts. ## Account Types ### Job ```typescript type Job = { address: Address; state: JobState; // QUEUED | RUNNING | COMPLETED | STOPPED ipfsJob: string | null; // IPFS CID of job definition ipfsResult: string | null; // IPFS CID of job result market: Address; node: Address; payer: Address; project: Address; price: number; timeStart: number; // Unix timestamp timeEnd: number; // Unix timestamp timeout: number; // Seconds }; enum JobState { QUEUED = 0, RUNNING = 1, COMPLETED = 2, STOPPED = 3, } ``` ### Run ```typescript type Run = { address: Address; job: Address; // Associated job node: Address; // Node executing the job time: number; // Unix timestamp }; ``` ### Market ```typescript type Market = { address: Address; queueType: MarketQueueType; // JOB_QUEUE | NODE_QUEUE jobPrice: number; nodeStakeMinimum: number; jobTimeout: number; jobType: number; project: Address; // ... additional fields }; enum MarketQueueType { JOB_QUEUE = 0, NODE_QUEUE = 1, } ``` ## Solana Service General Solana utility service for low-level RPC operations, transactions, and PDA derivations. ### Methods ```typescript // Build, sign, and send transaction in one call (convenience method) buildSignAndSend( instructions: Instruction | Instruction[], options?: { feePayer?: TransactionSigner; commitment?: 'processed' | 'confirmed' | 'finalized'; } ): Promise<Signature> // Build transaction from instructions buildTransaction( instructions: Instruction | Instruction[], options?: { feePayer?: TransactionSigner } ): Promise<TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithBlockhashLifetime> // Sign a transaction message signTransaction( transactionMessage: TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithBlockhashLifetime ): Promise<SendableTransaction & Transaction & TransactionWithBlockhashLifetime> // Send and confirm a signed transaction sendTransaction( transaction: SendableTransaction & Transaction & TransactionWithBlockhashLifetime, options?: { commitment?: 'processed' | 'confirmed' | 'finalized' } ): Promise<Signature> // Get account balance getBalance(address?: Address | string): Promise<bigint> // Derive program derived address pda(seeds: Array<Address | string>, programId: Address): Promise<Address> // Get instruction to transfer SOL transfer(params: { to: Address | string; amount: number | bigint; from?: TransactionSigner; }): Promise<Instruction> // Returns TransferSolInstruction ``` ### Examples ```typescript // Send a single instruction (convenience method) const signature = await client.solana.buildSignAndSend(instruction); // Send multiple instructions atomically const signature = await client.solana.buildSignAndSend([ix1, ix2, ix3]); // Or build, sign, and send separately for more control const transactionMessage = await client.solana.buildTransaction(instruction); const signedTransaction = await client.solana.signTransaction(transactionMessage); const signature = await client.solana.sendTransaction(signedTransaction); // Check account balance const balance = await client.solana.getBalance('address'); console.log(`Balance: ${balance} lamports`); // Derive PDA const pda = await client.solana.pda(['seed1', 'seed2'], programAddress); // Get instruction to transfer SOL const transferSolIx = await client.solana.transfer({ to: 'recipient-address', amount: 1000000, // lamports (can be number or bigint) // from is optional - uses wallet if not provided }); // Execute the transfer await client.solana.buildSignAndSend(transferSolIx); ``` ## IPFS Service The IPFS service provides methods to pin data to IPFS and retrieve data from IPFS. It's configured via the `ipfs` property in the client configuration. ### Configuration ```typescript const client = createNosanaClient(NosanaNetwork.MAINNET, { ipfs: { api: 'https://api.pinata.cloud', jwt: 'your-pinata-jwt-token', gateway: 'https://gateway.pinata.cloud/ipfs/', }, }); ``` ### Methods ```typescript // Pin JSON data to IPFS pin(data: object): Promise<string> // Pin a file to IPFS pinFile(filePath: string): Promise<string> // Retrieve data from IPFS retrieve(hash: string | Uint8Array): Promise<any> ``` ### Examples ```typescript // Pin job definition to IPFS const cid = await client.ipfs.pin({ version: 1, type: 'docker', image: 'ubuntu:latest', command: ['echo', 'hello'], }); console.log('Pinned to IPFS:', cid); // Pin a file to IPFS const fileCid = await client.ipfs.pinFile('/path/to/file.txt'); // Retrieve job results from IPFS const results = await client.ipfs.retrieve(job.ipfsResult); console.log('Job results:', results); ``` ### Utility Functions The SDK also exports utility functions for converting between Solana hash formats and IPFS CIDs: ```typescript import { solBytesArrayToIpfsHash, ipfsHashToSolBytesArray } from '@nosana/kit'; // Convert Solana hash bytes to IPFS CID const ipfsCid = solBytesArrayToIpfsHash(solanaHashBytes); // Convert IPFS CID to Solana hash bytes const solanaHash = ipfsHashToSolBytesArray(ipfsCid); ``` ## API Service The API service provides access to Nosana APIs for jobs, credits, and markets. It's automatically configured based on your authentication method. ### Authentication Methods The API service supports two authentication methods: 1. **API Key Authentication** (Recommended for server-side applications) - Provide an API key in the configuration - API key takes precedence over wallet-based authentication 2. **Wallet-Based Authentication** (For client-side applications) - Set a wallet on the client - Uses message signing for authentication - Automatically enabled when a wallet is configured ### Configuration ```typescript // Option 1: Use API key (recommended for servers) const client = createNosanaClient(NosanaNetwork.MAINNET, { api: { apiKey: 'your-api-key-here', }, }); // Option 2: Use wallet-based auth (for client-side) const client = createNosanaClient(NosanaNetwork.MAINNET); client.wallet = myWallet; // Option 3: API key takes precedence when both are provided const client = createNosanaClient(NosanaNetwork.MAINNET, { api: { apiKey: 'your-api-key-here', }, wallet: myWallet, // API key will be used, not wallet }); ``` ### Behavior - **With API Key**: API is created immediately with API key authentication - **With Wallet**: API is created when wallet is set, using wallet-based authentication - **Without Both**: API is `undefined` until either an API key or wallet is provided - **Priority**: If both API key and wallet are provided, API key is used ### API Structure The API service provides access to three main APIs: ```typescript client.api?.jobs // Jobs API client.api?.credits // Credits API client.api?.markets // Markets API ``` ### Examples ```typescript // Using API key const client = createNosanaClient(NosanaNetwork.MAINNET, { api: { apiKey: 'your-api-key' }, }); // API is immediately available if (client.api) { // Use the API const jobs = await client.api.jobs.list(); } // Using wallet-based auth const client = createNosanaClient(NosanaNetwork.MAINNET); client.wallet = myWallet; // API is now available if (client.api) { const credits = await client.api.credits.get(); } // API updates reactively when wallet changes client.wallet = undefined; // API becomes undefined client.wallet = anotherWallet; // API is recreated with new wallet ``` ## Authorization Service The authorization service provides cryptographic message signing and validation using Ed25519 signatures. It's automatically available on the client and adapts based on whether a wallet is configured. ### Behavior - **With Wallet**: When a wallet is set, the authorization service provides all methods including `generate`, `validate`, `generateHeaders`, and `validateHeaders`. - **Without Wallet**: When no wallet is set, the authorization service only provides `validate` and `validateHeaders` methods (read-only validation). ### Methods ```typescript // Generate a signed message (requires wallet) generate(message: string | Uint8Array, options?: GenerateOptions): Promise<string> // Validate a signed message validate( message: string | Uint8Array, signature: string | Uint8Array, publicKey?: string | Uint8Array ): Promise<boolean> // Generate signed HTTP headers (requires wallet) generateHeaders( method: string, path: string, body?: string | Uint8Array, options?: GenerateHeaderOptions ): Promise<Headers> // Validate HTTP headers validateHeaders(headers: Headers | Record<string, string>): Promise<boolean> ``` ### Examples ```typescript // Set wallet first to enable signing client.wallet = myWallet; // Generate a signed message const signedMessage = await client.authorization.generate('Hello, Nosana!'); console.log('Signed message:', signedMessage); // Validate a signed message const isValid = await client.authorization.validate('Hello, Nosana!', signedMessage); console.log('Message is valid:', isValid); // Generate signed HTTP headers for API requests const headers = await client.authorization.generateHeaders( 'POST', '/api/jobs', JSON.stringify({ data: 'example' }) ); // Use headers in HTTP request fetch('https://api.nosana.com/api/jobs', { method: 'POST', headers: headers, body: JSON.stringify({ data: 'example' }), }); // Validate incoming HTTP headers const isValidRequest = await client.authorization.validateHeaders(requestHeaders); if (!isValidRequest) { throw new Error('Invalid authorization'); } ``` ### Use Cases - **API Authentication**: Sign requests to Nosana APIs using message signatures - **Message Verification**: Verify signed messages from other parties - **Secure Communication**: Establish authenticated communication channels - **Request Authorization**: Validate incoming API requests ## Merkle Distributor Program The MerkleDistributorProgram provides methods to interact with merkle distributor accounts and claim tokens from distributions. ### Get a Single Distributor Fetch a merkle distributor account by its address: ```typescript const distributor = await client.merkleDistributor.get('distributor-address'); console.log('Distributor:', distributor.address); console.log('Admin:', distributor.admin); console.log('Mint:', distributor.mint); console.log('Root:', distributor.root); ``` ### Get All Distributors Fetch all merkle distributor accounts: ```typescript const distributors = await client.merkleDistributor.all(); console.log(`Found ${distributors.length} distributors`); ``` ### Get Claim Status Fetch claim status for a specific distributor and claimant: ```typescript // Get claim status for the wallet's address const claimStatus = await client.merkleDistributor.getClaimStatusForDistributor('distributor-address'); // Or specify a claimant address const claimStatus = await client.merkleDistributor.getClaimStatusForDistributor( 'distributor-address', 'claimant-address' ); if (claimStatus) { console.log('Claimed:', claimStatus.claimed); console.log('Amount Unlocked:', claimStatus.amountUnlocked); console.log('Amount Locked:', claimStatus.amountLocked); } else { console.log('No claim status found'); } ``` ### Get Claim Status PDA Derive the ClaimStatus PDA address: ```typescript // Derive for wallet's address const pda = await client.merkleDistributor.getClaimStatusPda('distributor-address'); // Or specify a claimant address const pda = await client.merkleDistributor.getClaimStatusPda( 'distributor-address', 'claimant-address' ); ``` ### Claim Tokens Claim tokens from a merkle distributor: ```typescript // Set wallet first client.wallet = yourWallet; // Claim tokens const instruction = await client.merkleDistributor.claim({ distributor: 'distributor-address', amountUnlocked: 1000000, // Amount in smallest unit amountLocked: 500000, proof: [ /* merkle proof array */ ], target: ClaimTarget.YES, // or ClaimTarget.NO }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` ### Clawback Tokens Clawback tokens from a merkle distributor (admin only): ```typescript // Set wallet first (must be admin) client.wallet = adminWallet; // Clawback tokens const instruction = await client.merkleDistributor.clawback({ distributor: 'distributor-address', }); // Submit the instruction await client.solana.buildSignAndSend(instruction); ``` ### Type Definitions ```typescript interface MerkleDistributor { address: Address; admin: Address; mint: Address; root: string; // Base58 encoded merkle root buffer0: string; buffer1: string; buffer2: string; // ... additional fields } interface ClaimStatus { address: Address; distributor: Address; claimant: Address; claimed: boolean; amountUnlocked: number; amountLocked: number; } enum ClaimTarget { YES = 'YES', NO = 'NO', } ``` ### Use Cases - **Airdrop Claims**: Allow users to claim tokens from merkle tree distributions - **Reward Distribution**: Distribute rewards to eligible addresses - **Token Vesting**: Manage locked and unlocked token distributions - **Governance**: Distribute governance tokens to eligible participants ## Staking Program The StakeProgram provides methods to interact with Nosana staking accounts on-chain. ### Get a Single Stake Account Fetch a stake account by its address: ```typescript const stake = await client.stake.get('stake-account-address'); console.log('Stake Account:', stake.address); console.log('Authority:', stake.authority); console.log('Staked Amount:', stake.amount); console.log('xNOS Tokens:', stake.xnos); console.log('Duration:', stake.duration); console.log('Time to Unstake:', stake.timeUnstake); console.log('Vault:', stake.vault); ``` ### Get Multiple Stake Accounts Fetch multiple stake accounts by their addresses: ```typescript const addresses = ['address1', 'address2', 'address3']; const stakes = await client.stake.multiple(addresses); stakes.forEach((stake) => { console.log(`${stake.address}: ${stake.amount} staked`); }); ``` ### Get All Stake Accounts Fetch all stake accounts in the program: ```typescript // Get all stakes const allStakes = await client.stake.all(); console.log(`Found ${allStakes.length} stake accounts`); ``` ### Type Definitions ```typescript interface Stake { address: Address; amount: number; authority: Address; duration: number; timeUnstake: number; vault: Address; vaultBump: number; xnos: number; } ``` ### Use Cases - **Portfolio Tracking**: Monitor your staked NOS tokens - **Analytics**: Analyze staking patterns and distributions - **Governance**: Check voting power based on staked amounts - **Rewards Calculation**: Calculate rewards based on stake duration and amount ### Example: Analyze Staking Distribution ```typescript const allStakes = await client.stake.all(); // Calculate total staked const totalStaked = allStakes.reduce((sum, stake) => sum + stake.amount, 0); // Find average stake const averageStake = totalStaked / allStakes.length; // Find largest stake const largestStake = allStakes.reduce((max, stake) => Math.max(max, stake.amount), 0); console.log('Staking Statistics:'); console.log(`Total Staked: ${totalStaked.toLocaleString()} NOS`); console.log(`Average Stake: ${averageStake.toLocaleString()} NOS`); console.log(`Largest Stake: ${largestStake.toLocaleString()} NOS`); console.log(`Number of Stakers: ${allStakes.length}`); ``` ## Token Service The TokenService provides methods to interact with token accounts on Solana. In the NosanaClient, it's configured for the NOS token and accessible via `client.nos`. ### Get All Token Holders Fetch all accounts holding NOS tokens using a single RPC call: ```typescript // Get all holders (excludes zero balance accounts by default) const holders = await client.nos.getAllTokenHolders(); console.log(`Found ${holders.length} NOS token holders`); holders.forEach((holder) => { console.log(`${holder.owner}: ${holder.uiAmount} NOS`); }); // Include accounts with zero balance const allAccounts = await client.nos.getAllTokenHolders({ includeZeroBalance: true }); console.log(`Total accounts: ${allAccounts.length}`); // Exclude PDA accounts (smart contract-owned token accounts) const userAccounts = await client.nos.getAllTokenHolders({ excludePdaAccounts: true }); console.log(`User-owned accounts: ${userAccounts.length}`); // Combine filters const activeUsers = await client.nos.getAllTokenHolders({ includeZeroBalance: false, excludePdaAccounts: true, }); console.log(`Active user accounts: ${activeUsers.length}`); ``` ### Get Token Account for Address Retrieve the NOS token account for a specific owner: ```typescript const account = await client.nos.getTokenAccountForAddress('owner-address'); if (account) { console.log('Token Account:', account.pubkey); console.log('Owner:', account.owner); console.log('Balance:', account.uiAmount, 'NOS'); console.log('Raw Amount:', account.amount.toString()); console.log('Decimals:', account.decimals); } else { console.log('No NOS token account found'); } ``` ### Get Balance Convenience method to get just the NOS balance for an address: ```typescript const balance = await client.nos.getBalance('owner-address'); console.log(`Balance: ${balance} NOS`); // Returns 0 if no token account exists ``` ### Transfer Tokens Get instruction(s) to transfer SPL tokens. Returns either 1 or 2 instructions depending on whether the recipient's associated token account needs to be created: ```typescript // Get transfer instruction(s) const instructions = await client.nos.transfer({ to: 'recipient-address', amount: 1000000, // token base units (can be number or bigint) // from is optional - uses wallet if not provided }); // Execute the transfer // instructions is a tuple: // - [TransferInstruction] when recipient ATA exists (1 instruction) // - [CreateAssociatedTokenIdempotentInstruction, TransferInstruction] when ATA needs creation (2 instructions) await client.solana.buildSignAndSend(instructions); ``` The function automatically: - Finds the sender's associated token account - Finds the recipient's associated token account - Creates the recipient's ATA if it doesn't exist (returns 2 instructions: create ATA + transfer) - Returns only the transfer instruction if the recipient's ATA already exists (returns 1 instruction) ### Type Definitions ```typescript interface TokenAccount { pubkey: Address; owner: Address; mint: Address; amount: bigint; decimals: number; } interface TokenAccountWithBalance extends TokenAccount { uiAmount: number; // Balance with decimals applied } ``` ### Use Cases - **Analytics**: Analyze token distribution and holder statistics - **Airdrops**: Get list of all token holders for campaigns - **Balance Checks**: Check NOS balances for specific addresses - **Leaderboards**: Create holder rankings sorted by balance - **Monitoring**: Track large holder movements ### Example: Filter Large Holders ```typescript const holders = await client.nos.getAllTokenHolders(); // Find holders with at least 1000 NOS const largeHolders = holders.filter((h) => h.uiAmount >= 1000); // Sort by balance descending largeHolders.sort((a, b) => b.uiAmount - a.uiAmount); // Display top 10 largeHolders.slice(0, 10).forEach((holder, i) => { console.log(`${i + 1}. ${holder.owner}: ${holder.uiAmount.toLocaleString()} NOS`); }); ``` ## Error Handling The SDK provides structured error handling with specific error codes. ### NosanaError ```typescript class NosanaError extends Error { code: string; details?: any; } ``` ### Error Codes ```typescript enum ErrorCodes { INVALID_NETWORK = 'INVALID_NETWORK', INVALID_CONFIG = 'INVALID_CONFIG', RPC_ERROR = 'RPC_ERROR', TRANSACTION_ERROR = 'TRANSACTION_ERROR', PROGRAM_ERROR = 'PROGRAM_ERROR', VALIDATION_ERROR = 'VALIDATION_ERROR', NO_WALLET = 'NO_WALLET', FILE_ERROR = 'FILE_ERROR', WALLET_CONVERSION_ERROR = 'WALLET_CONVERSION_ERROR', } ``` ### Examples ```typescript import { NosanaError, ErrorCodes } from '@nosana/kit'; try { const job = await client.jobs.get('invalid-address'); } catch (error) { if (error instanceof NosanaError) { switch (error.code) { case ErrorCodes.RPC_ERROR: console.error('RPC connection failed:', error.message); break; case ErrorCodes.NO_WALLET: console.error('Wallet not configured'); client.wallet = myWallet; break; case ErrorCodes.TRANSACTION_ERROR: console.error('Transaction failed:', error.details); break; default: console.error('Unknown error:', error.message); } } else { throw error; // Re-throw non-Nosana errors } } ``` ## Logging The SDK includes a built-in singleton logger with configurable levels. ### Log Levels ```typescript enum LogLevel { DEBUG = 'debug', INFO = 'info', WARN = 'warn', ERROR = 'error', NONE = 'none', } ``` ### Configuration ```typescript import { createNosanaClient, LogLevel } from '@nosana/kit'; // Set log level during initialization const client = createNosanaClient(NosanaNetwork.MAINNET, { logLevel: LogLevel.DEBUG, }); // Access logger directly client.logger.info('Information message'); client.logger.error('Error message'); client.logger.debug('Debug details'); ``` ## Testing The SDK includes comprehensive test coverage. From the **repository root**: ```bash pnpm test ``` Kit-only (watch or coverage): ```bash pnpm --filter @nosana/kit run test:watch pnpm --filter @nosana/kit run test:coverage ``` ### Scenario Tests Scenario tests exercise SDK functionality against a real Solana validator. They can run against localnet, devnet, or mainnet — controlled entirely via environment variables (no code changes needed). Scenario tests live in the [`@nosana/scenario`](../scenario) package. Run them from the workspace root: #### Localnet (default) Starts a Docker-based Solana validator with Nosana programs pre-baked, runs the tests, then you can stop it: ```bash # Start localnet + run tests pnpm --filter @nosana/scenario run test:scenario:localnet # Or step by step pnpm --filter @nosana/scenario run localnet:up pnpm --filter @nosana/scenario run test:scenario pnpm --filter @nosana/scenario run localnet:down ``` #### Devnet Requires a funded wallet (with SOL and NOS tokens on devnet): ```bash NOSANA_NETWORK=devnet \ NOSANA_WALLET=~/.config/solana/id.json \ pnpm --filter @nosana/scenario run test:scenario ``` #### Mainnet ```bash NOSANA_NETWORK=mainnet \ NOSANA_WALLET=/path/to/mainnet-keypair.json \ pnpm --filter @nosana/scenario run test:scenario ``` #### Environment Variables | Variable | Default | Description | |----------|---------|-------------| | `NOSANA_NETWORK` | `localnet` | Target network: `localnet`, `devnet`, or `mainnet` | | `NOSANA_WALLET` | — | Path to a Solana keypair JSON file (required for devnet/mainnet) | For monorepo setup, build, and development, see the [root README](../../README.md). ## TypeScript Support The SDK is written in TypeScript and provides complete type definitions. All types are exported for use in your applications: ```typescript import type { Job, Run, Market, JobState, MarketQueueType, Stake, MerkleDistributor, ClaimStatus, ClaimTarget, ClientConfig, NosanaClient, Wallet, Address, } from '@nosana/kit'; // The `address` utility function is also available for creating typed addresses import { address } from '@nosana/kit'; const jobAddress = address('your-job-address'); ``` ## Dependencies Core dependencies: - `@solana/kit` 5.0.0 - Solana web3 library - `@solana-program/token` 0.8.0 - Token program utilities - `@solana-program/system` 0.10.0 - System program utilities - `@solana-program/compute-budget` 0.11.0 - Compute budget utilities - `bs58` 6.0.0 - Base58 encoding ## License MIT ## Links - [Nosana Documentation](https://docs.nosana.com) - [Nosana Network](https://nosana.com) - [GitHub Repository](https://github.com/nosana-ci/nosana-kit) - [NPM Package](https://www.npmjs.com/package/@nosana/kit)