npm package: @nosana/kit
Description: Nosana KIT
Author: Nosana
Latest version: 2.2.4

# 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.

#### 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 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)