UNPKG

@nosana/kit

Version:

Nosana KIT

1,289 lines (969 loc) 34.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) ## 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 - **`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', }, 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) }); ``` ## 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. #### Wallet Requirements The wallet must implement both `MessageSigner` and `TransactionSigner` interfaces from `@solana/kit`. This allows the SDK to use the wallet for: - **Message signing** - For API authentication and authorization - **Transaction signing** - For on-chain operations #### 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); ``` #### Keypair Wallets Seamless support for keypair-based wallets: ```typescript import { createNosanaClient } from '@nosana/kit'; import { generateKeyPairSigner } from '@solana/kit'; // Create client const client = createNosanaClient(); // Set keypair wallet const keypair = generateKeyPairSigner(); client.wallet = keypair; ``` #### Configuration Options Wallets can be set at client initialization or dynamically assigned: ```typescript import { createNosanaClient, NosanaNetwork } from '@nosana/kit'; import type { Wallet } from '@nosana/kit'; // Option 1: Set wallet during initialization const client = createNosanaClient(NosanaNetwork.MAINNET, { wallet: myWallet, }); // Option 2: Set wallet dynamically const client = createNosanaClient(); client.wallet = myWallet; // Option 3: Change wallet at runtime client.wallet = anotherWallet; ``` #### Type Safety The SDK leverages `@solana/kit` types for compile-time safety, ensuring wallet compatibility before runtime. ## 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 list instruction for posting a job to a market. Returns an instruction that must be submitted to the network. ```typescript // Set wallet first client.wallet = yourWallet; // Create job instruction const instruction = await client.jobs.post({ market: 'market-address', timeout: 3600, // Timeout in seconds ipfsHash: 'QmXxx...', // IPFS CID of job definition node: 'node-address', // Optional: target specific node }); // 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. ```bash # Run tests npm test # Run tests in watch mode npm run test:watch # Generate coverage report npm run test:coverage ``` ## Development ```bash # Build the SDK npm run build # Lint code npm run lint # Format code npm run format # Generate Solana program clients npm run generate-clients ``` ## 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, } from '@nosana/kit'; import type { Address } from '@solana/kit'; ``` ## 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)