@nosana/kit 2.0.66 → 2.0.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +1742 -0
  2. package/package.json +5 -5
package/README.md ADDED
@@ -0,0 +1,1742 @@
1
+ # Nosana Kit
2
+
3
+ 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.
4
+
5
+ > **v2.0.0** - Major release featuring functional architecture, universal wallet support, and enhanced transaction handling. See [CHANGELOG.md](./CHANGELOG.md) for migration guide.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @nosana/kit
11
+ ```
12
+
13
+ ### Requirements
14
+
15
+ - Node.js >= 20.18.0
16
+ - TypeScript >= 5.3.0 (for development)
17
+ - pnpm >= 9.15.0 (for development)
18
+
19
+ ## Quick Start
20
+
21
+ ```typescript
22
+ import { createNosanaClient, NosanaNetwork } from '@nosana/kit';
23
+
24
+ // Initialize with mainnet defaults
25
+ const client = createNosanaClient();
26
+
27
+ // Or specify network and configuration
28
+ const client = createNosanaClient(NosanaNetwork.DEVNET, {
29
+ solana: {
30
+ rpcEndpoint: 'https://your-custom-rpc.com',
31
+ commitment: 'confirmed',
32
+ },
33
+ });
34
+
35
+ // Fetch a job by address
36
+ const job = await client.jobs.get('job-address');
37
+ console.log('Job state:', job.state);
38
+
39
+ // Query jobs with filters
40
+ const completedJobs = await client.jobs.all({
41
+ market: 'market-address',
42
+ state: 2, // JobState.COMPLETED
43
+ });
44
+ ```
45
+
46
+ ## Architecture
47
+
48
+ The SDK uses a functional architecture with factory functions for improved modularity and testability:
49
+
50
+ - **`services/`** - Utility services and program interfaces
51
+ - **`SolanaService`** - Low-level Solana RPC operations, transactions, and PDA derivations
52
+ - **`TokenService`** - Token account operations (configured for NOS token)
53
+ - **`programs/`** - On-chain program interfaces
54
+ - **`JobsProgram`** - Jobs, runs, and markets management
55
+ - **`StakeProgram`** - Staking account operations
56
+ - **`MerkleDistributorProgram`** - Merkle distributor and claim operations
57
+ - **`ipfs/`** - IPFS integration for pinning and retrieving data
58
+ - **`config/`** - Network configurations and defaults
59
+ - **`utils/`** - Helper utilities and type conversions
60
+ - **`packages/generated_clients/`** - Auto-generated Solana program clients (exported as namespaces)
61
+
62
+ All components use factory functions with explicit dependency injection, making the codebase modular, testable, and maintainable.
63
+
64
+ ## Configuration
65
+
66
+ ### Networks
67
+
68
+ The SDK supports two networks:
69
+
70
+ - **`NosanaNetwork.MAINNET`** - Production network (mainnet-beta)
71
+ - **`NosanaNetwork.DEVNET`** - Development network (devnet)
72
+
73
+ ### Configuration Options
74
+
75
+ ```typescript
76
+ import { createNosanaClient, NosanaNetwork, LogLevel } from '@nosana/kit';
77
+
78
+ const client = createNosanaClient(NosanaNetwork.MAINNET, {
79
+ solana: {
80
+ cluster: 'mainnet-beta',
81
+ rpcEndpoint: 'https://api.mainnet-beta.solana.com',
82
+ commitment: 'confirmed',
83
+ },
84
+ ipfs: {
85
+ api: 'https://api.pinata.cloud',
86
+ jwt: 'your-pinata-jwt-token',
87
+ gateway: 'https://gateway.pinata.cloud/ipfs/',
88
+ },
89
+ api: {
90
+ apiKey: 'your-api-key', // Optional: API key for authentication
91
+ },
92
+ logLevel: LogLevel.DEBUG,
93
+ wallet: myWallet, // Optional: Set wallet during initialization (must be a Wallet type)
94
+ });
95
+ ```
96
+
97
+ ## Core Components
98
+
99
+ ### NosanaClient
100
+
101
+ Main entry point for SDK interactions. Created using the `createNosanaClient()` factory function.
102
+
103
+ **Properties:**
104
+
105
+ - `config: ClientConfig` - Active configuration
106
+ - `jobs: JobsProgram` - Jobs program interface
107
+ - `stake: StakeProgram` - Staking program interface
108
+ - `merkleDistributor: MerkleDistributorProgram` - Merkle distributor program interface
109
+ - `solana: SolanaService` - General Solana utilities (RPC, transactions, PDAs)
110
+ - `nos: TokenService` - Token operations service (configured for NOS token)
111
+ - `api: NosanaApi | undefined` - Nosana API client for interacting with Nosana APIs (jobs, credits, markets)
112
+ - `ipfs: ReturnType<typeof createIpfsClient>` - IPFS operations for pinning and retrieving data
113
+ - `authorization: NosanaAuthorization | Omit<NosanaAuthorization, 'generate' | 'generateHeaders'>` - Authorization service for message signing and validation
114
+ - `logger: Logger` - Logging instance
115
+ - `wallet?: Wallet` - Active wallet (if set). Set this property directly to configure the wallet.
116
+
117
+ **Factory Function:**
118
+
119
+ - `createNosanaClient(network?: NosanaNetwork, customConfig?: PartialClientConfig): NosanaClient` - Creates a new client instance
120
+
121
+ ### Wallet Configuration
122
+
123
+ 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.
124
+
125
+ #### Wallet Requirements
126
+
127
+ The wallet must implement both `MessageSigner` and `TransactionSigner` interfaces from `@solana/kit`. This allows the SDK to use the wallet for:
128
+ - **Message signing** - For API authentication and authorization
129
+ - **Transaction signing** - For on-chain operations
130
+
131
+ #### Browser Wallets (Wallet-Standard)
132
+
133
+ Full support for wallet-standard compatible browser wallets (Phantom, Solflare, etc.):
134
+
135
+ ```typescript
136
+ import { createNosanaClient } from '@nosana/kit';
137
+ import { useWalletAccountSigner } from '@nosana/solana-vue';
138
+
139
+ // Create client
140
+ const client = createNosanaClient();
141
+
142
+ // Set browser wallet (wallet-standard compatible)
143
+ client.wallet = useWalletAccountSigner(account, currentChain);
144
+ ```
145
+
146
+ #### Keypair Wallets
147
+
148
+ Seamless support for keypair-based wallets:
149
+
150
+ ```typescript
151
+ import { createNosanaClient } from '@nosana/kit';
152
+ import { generateKeyPairSigner } from '@solana/kit';
153
+
154
+ // Create client
155
+ const client = createNosanaClient();
156
+
157
+ // Set keypair wallet
158
+ const keypair = generateKeyPairSigner();
159
+ client.wallet = keypair;
160
+ ```
161
+
162
+ #### Configuration Options
163
+
164
+ Wallets can be set at client initialization or dynamically assigned:
165
+
166
+ ```typescript
167
+ import { createNosanaClient, NosanaNetwork } from '@nosana/kit';
168
+ import type { Wallet } from '@nosana/kit';
169
+
170
+ // Option 1: Set wallet during initialization
171
+ const client = createNosanaClient(NosanaNetwork.MAINNET, {
172
+ wallet: myWallet,
173
+ });
174
+
175
+ // Option 2: Set wallet dynamically
176
+ const client = createNosanaClient();
177
+ client.wallet = myWallet;
178
+
179
+ // Option 3: Change wallet at runtime
180
+ client.wallet = anotherWallet;
181
+ ```
182
+
183
+ #### Type Safety
184
+
185
+ The SDK leverages `@solana/kit` types for compile-time safety, ensuring wallet compatibility before runtime.
186
+
187
+ ## Jobs Program API
188
+
189
+ ### Fetching Accounts
190
+
191
+ #### Get Single Job
192
+
193
+ ```typescript
194
+ async get(address: Address, checkRun?: boolean): Promise<Job>
195
+ ```
196
+
197
+ Fetch a job account. If `checkRun` is true (default), automatically checks for associated run accounts to determine if a queued job is actually running.
198
+
199
+ ```typescript
200
+ const job = await client.jobs.get('job-address');
201
+ console.log(job.state); // JobState enum
202
+ console.log(job.price); // Job price in smallest unit
203
+ console.log(job.ipfsJob); // IPFS CID of job definition
204
+ console.log(job.timeStart); // Start timestamp (if running)
205
+ ```
206
+
207
+ #### Get Single Run
208
+
209
+ ```typescript
210
+ async run(address: Address): Promise<Run>
211
+ ```
212
+
213
+ Fetch a run account by address.
214
+
215
+ ```typescript
216
+ const run = await client.jobs.run('run-address');
217
+ console.log(run.job); // Associated job address
218
+ console.log(run.node); // Node executing the run
219
+ console.log(run.time); // Run start time
220
+ ```
221
+
222
+ #### Get Single Market
223
+
224
+ ```typescript
225
+ async market(address: Address): Promise<Market>
226
+ ```
227
+
228
+ Fetch a market account by address.
229
+
230
+ ```typescript
231
+ const market = await client.jobs.market('market-address');
232
+ console.log(market.queueType); // MarketQueueType enum
233
+ console.log(market.jobPrice); // Market job price
234
+ ```
235
+
236
+ #### Get Multiple Jobs
237
+
238
+ ```typescript
239
+ async multiple(addresses: Address[], checkRuns?: boolean): Promise<Job[]>
240
+ ```
241
+
242
+ Batch fetch multiple jobs by addresses.
243
+
244
+ ```typescript
245
+ const jobs = await client.jobs.multiple(['job-address-1', 'job-address-2', 'job-address-3'], true);
246
+ ```
247
+
248
+ ### Querying with Filters
249
+
250
+ #### Query All Jobs
251
+
252
+ ```typescript
253
+ async all(filters?: {
254
+ state?: JobState,
255
+ market?: Address,
256
+ node?: Address,
257
+ project?: Address
258
+ }, checkRuns?: boolean): Promise<Job[]>
259
+ ```
260
+
261
+ Fetch all jobs matching filter criteria using getProgramAccounts.
262
+
263
+ ```typescript
264
+ import { JobState } from '@nosana/kit';
265
+
266
+ // Get all running jobs in a market
267
+ const runningJobs = await client.jobs.all({
268
+ state: JobState.RUNNING,
269
+ market: 'market-address',
270
+ });
271
+
272
+ // Get all jobs for a project
273
+ const projectJobs = await client.jobs.all({
274
+ project: 'project-address',
275
+ });
276
+ ```
277
+
278
+ #### Query All Runs
279
+
280
+ ```typescript
281
+ async runs(filters?: {
282
+ job?: Address,
283
+ node?: Address
284
+ }): Promise<Run[]>
285
+ ```
286
+
287
+ Fetch runs with optional filtering.
288
+
289
+ ```typescript
290
+ // Get all runs for a specific job
291
+ const jobRuns = await client.jobs.runs({ job: 'job-address' });
292
+
293
+ // Get all runs on a specific node
294
+ const nodeRuns = await client.jobs.runs({ node: 'node-address' });
295
+ ```
296
+
297
+ #### Query All Markets
298
+
299
+ ```typescript
300
+ async markets(): Promise<Market[]>
301
+ ```
302
+
303
+ Fetch all market accounts.
304
+
305
+ ```typescript
306
+ const markets = await client.jobs.markets();
307
+ ```
308
+
309
+ ### Creating Jobs
310
+
311
+ #### Post a Job
312
+
313
+ ```typescript
314
+ async post(params: {
315
+ market: Address,
316
+ timeout: number | bigint,
317
+ ipfsHash: string,
318
+ node?: Address
319
+ }): Promise<Instruction>
320
+ ```
321
+
322
+ 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.
323
+
324
+ ```typescript
325
+ // Set wallet first
326
+ client.wallet = yourWallet;
327
+
328
+ // Create job instruction (will list to market if node is not provided)
329
+ const instruction = await client.jobs.post({
330
+ market: address('market-address'),
331
+ timeout: 3600, // Timeout in seconds
332
+ ipfsHash: 'QmXxx...', // IPFS CID of job definition
333
+ node: address('node-address'), // Optional: target specific node (assigns directly if provided)
334
+ });
335
+
336
+ // Submit the instruction
337
+ await client.solana.buildSignAndSend(instruction);
338
+ ```
339
+
340
+ #### List a Job
341
+
342
+ ```typescript
343
+ async list(params: {
344
+ market: Address,
345
+ timeout: number | bigint,
346
+ ipfsHash: string,
347
+ payer?: TransactionSigner
348
+ }): Promise<Instruction>
349
+ ```
350
+
351
+ List a new job to a market queue. The job will be available for nodes to pick up.
352
+
353
+ ```typescript
354
+ // Set wallet first
355
+ client.wallet = yourWallet;
356
+
357
+ // List job to market
358
+ const instruction = await client.jobs.list({
359
+ market: address('market-address'),
360
+ timeout: 3600, // Timeout in seconds
361
+ ipfsHash: 'QmXxx...', // IPFS CID of job definition
362
+ });
363
+
364
+ // Submit the instruction
365
+ await client.solana.buildSignAndSend(instruction);
366
+ ```
367
+
368
+ #### Assign a Job
369
+
370
+ ```typescript
371
+ async assign(params: {
372
+ market: Address,
373
+ node: Address,
374
+ timeout: number | bigint,
375
+ ipfsHash: string,
376
+ payer?: TransactionSigner
377
+ }): Promise<Instruction>
378
+ ```
379
+
380
+ Assign a job directly to a specific node, bypassing the market queue.
381
+
382
+ ```typescript
383
+ // Set wallet first
384
+ client.wallet = yourWallet;
385
+
386
+ // Assign job directly to node
387
+ const instruction = await client.jobs.assign({
388
+ market: address('market-address'),
389
+ node: address('node-address'),
390
+ timeout: 3600, // Timeout in seconds
391
+ ipfsHash: 'QmXxx...', // IPFS CID of job definition
392
+ });
393
+
394
+ // Submit the instruction
395
+ await client.solana.buildSignAndSend(instruction);
396
+ ```
397
+
398
+ ### Managing Jobs
399
+
400
+ #### Extend Job Timeout
401
+
402
+ ```typescript
403
+ async extend(params: {
404
+ job: Address,
405
+ timeout: number | bigint,
406
+ payer?: TransactionSigner
407
+ }): Promise<Instruction>
408
+ ```
409
+
410
+ Extend an existing job's timeout by adding the specified amount to the current timeout.
411
+
412
+ ```typescript
413
+ // Set wallet first
414
+ client.wallet = yourWallet;
415
+
416
+ // Extend job timeout by 1800 seconds (30 minutes)
417
+ const instruction = await client.jobs.extend({
418
+ job: address('job-address'),
419
+ timeout: 1800, // Additional seconds to add
420
+ });
421
+
422
+ // Submit the instruction
423
+ await client.solana.buildSignAndSend(instruction);
424
+ ```
425
+
426
+ #### Delist a Job
427
+
428
+ ```typescript
429
+ async delist(params: {
430
+ job: Address
431
+ }): Promise<Instruction>
432
+ ```
433
+
434
+ Remove a job from the market queue. The job's deposit will be returned to the payer.
435
+
436
+ ```typescript
437
+ // Set wallet first
438
+ client.wallet = yourWallet;
439
+
440
+ // Delist job from market
441
+ const instruction = await client.jobs.delist({
442
+ job: address('job-address'),
443
+ });
444
+
445
+ // Submit the instruction
446
+ await client.solana.buildSignAndSend(instruction);
447
+ ```
448
+
449
+ #### Stop a Running Job
450
+
451
+ ```typescript
452
+ async end(params: {
453
+ job: Address
454
+ }): Promise<Instruction>
455
+ ```
456
+
457
+ Stop a job that is currently running. The job must have an associated run account.
458
+
459
+ ```typescript
460
+ // Set wallet first
461
+ client.wallet = yourWallet;
462
+
463
+ // Stop a running job
464
+ const instruction = await client.jobs.end({
465
+ job: address('job-address'),
466
+ });
467
+
468
+ // Submit the instruction
469
+ await client.solana.buildSignAndSend(instruction);
470
+ ```
471
+
472
+ ### Completing Jobs
473
+
474
+ #### Work (Enter Queue or Create Run)
475
+
476
+ ```typescript
477
+ async work(params: {
478
+ market: Address,
479
+ nft?: Address
480
+ }): Promise<Instruction>
481
+ ```
482
+
483
+ 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.
484
+
485
+ ```typescript
486
+ // Set wallet first (must be a node wallet)
487
+ client.wallet = nodeWallet;
488
+
489
+ // Enter market queue or create run
490
+ const instruction = await client.jobs.work({
491
+ market: address('market-address'),
492
+ nft: 'nft-address', // Optional: NFT for node verification
493
+ });
494
+
495
+ // Submit the instruction
496
+ await client.solana.buildSignAndSend(instruction);
497
+ ```
498
+
499
+ #### Finish a Stopped Job
500
+
501
+ ```typescript
502
+ async finish(params: {
503
+ job: Address,
504
+ ipfsResultsHash: string
505
+ }): Promise<Instruction | Instruction[]>
506
+ ```
507
+
508
+ 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.
509
+
510
+ ```typescript
511
+ // Set wallet first
512
+ client.wallet = yourWallet;
513
+
514
+ // Finish a stopped job with results
515
+ const instructions = await client.jobs.finish({
516
+ job: address('job-address'),
517
+ ipfsResultsHash: 'QmYyy...', // IPFS CID of job results
518
+ });
519
+
520
+ // Submit the instruction(s) - may be array if ATA creation is needed
521
+ await client.solana.buildSignAndSend(instructions);
522
+ ```
523
+
524
+ #### Complete a Job
525
+
526
+ ```typescript
527
+ async complete(params: {
528
+ job: Address,
529
+ ipfsResultsHash: string
530
+ }): Promise<Instruction>
531
+ ```
532
+
533
+ Complete a job that is in the COMPLETED state by posting the final result IPFS hash. This finalizes the job and allows payment processing.
534
+
535
+ ```typescript
536
+ // Set wallet first
537
+ client.wallet = yourWallet;
538
+
539
+ // Complete a job with final results
540
+ const instruction = await client.jobs.complete({
541
+ job: address('job-address'),
542
+ ipfsResultsHash: 'QmYyy...', // IPFS CID of final job results
543
+ });
544
+
545
+ // Submit the instruction
546
+ await client.solana.buildSignAndSend(instruction);
547
+ ```
548
+
549
+ #### Quit a Run
550
+
551
+ ```typescript
552
+ async quit(params: {
553
+ run: Address
554
+ }): Promise<Instruction>
555
+ ```
556
+
557
+ Quit a run account, removing the node from the job execution.
558
+
559
+ ```typescript
560
+ // Set wallet first (must be the node wallet)
561
+ client.wallet = nodeWallet;
562
+
563
+ // Quit a run
564
+ const instruction = await client.jobs.quit({
565
+ run: address('run-address'),
566
+ });
567
+
568
+ // Submit the instruction
569
+ await client.solana.buildSignAndSend(instruction);
570
+ ```
571
+
572
+ #### Stop (Exit Node Queue)
573
+
574
+ ```typescript
575
+ async stop(params: {
576
+ market: Address,
577
+ node?: Address
578
+ }): Promise<Instruction>
579
+ ```
580
+
581
+ Exit a market's node queue. If no node is specified, uses the wallet's address.
582
+
583
+ ```typescript
584
+ // Set wallet first (must be a node wallet)
585
+ client.wallet = nodeWallet;
586
+
587
+ // Exit market queue
588
+ const instruction = await client.jobs.stop({
589
+ market: address('market-address'),
590
+ node: address('node-address'), // Optional: defaults to wallet address
591
+ });
592
+
593
+ // Submit the instruction
594
+ await client.solana.buildSignAndSend(instruction);
595
+ ```
596
+
597
+ ### Market Management
598
+
599
+ #### Open a Market
600
+
601
+ ```typescript
602
+ async open(params: {
603
+ nodeAccessKey?: Address,
604
+ jobExpiration?: number | bigint,
605
+ jobType?: number,
606
+ jobPrice?: number | bigint,
607
+ jobTimeout?: number | bigint,
608
+ nodeStakeMinimum?: number | bigint,
609
+ payer?: TransactionSigner
610
+ }): Promise<Instruction>
611
+ ```
612
+
613
+ 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.
614
+
615
+ ```typescript
616
+ // Set wallet first
617
+ client.wallet = yourWallet;
618
+
619
+ // Create a new market with default values
620
+ const instruction = await client.jobs.open({});
621
+
622
+ // Or create with custom parameters
623
+ const instruction = await client.jobs.open({
624
+ jobPrice: 10, // Nos per second in smallest unit
625
+ jobTimeout: 3600, // 60 minutes
626
+ jobExpiration: 172800, // 48 hours
627
+ nodeStakeMinimum: 5000000, // Minimum stake required
628
+ nodeAccessKey: address('access-key-address'), // Optional: defaults to system program
629
+ });
630
+
631
+ // Submit the instruction
632
+ await client.solana.buildSignAndSend(instruction);
633
+ ```
634
+
635
+ #### Create Market (Synonym)
636
+
637
+ ```typescript
638
+ async createMarket(params: OpenParams): Promise<OpenInstruction>
639
+ ```
640
+
641
+ Synonym for `open()`. Creates a new market with the same parameters.
642
+
643
+ ```typescript
644
+ // Set wallet first
645
+ client.wallet = yourWallet;
646
+
647
+ // Create market using synonym
648
+ const instruction = await client.jobs.createMarket({
649
+ jobPrice: 10,
650
+ jobTimeout: 3600,
651
+ });
652
+
653
+ // Submit the instruction
654
+ await client.solana.buildSignAndSend(instruction);
655
+ ```
656
+
657
+ #### Close a Market
658
+
659
+ ```typescript
660
+ async close(params: {
661
+ market: Address,
662
+ payer?: TransactionSigner
663
+ }): Promise<Instruction>
664
+ ```
665
+
666
+ Close an existing market. This will return any remaining funds to the market authority.
667
+
668
+ ```typescript
669
+ // Set wallet first (must be market authority)
670
+ client.wallet = yourWallet;
671
+
672
+ // Close a market
673
+ const instruction = await client.jobs.close({
674
+ market: address('market-address'),
675
+ });
676
+
677
+ // Submit the instruction
678
+ await client.solana.buildSignAndSend(instruction);
679
+ ```
680
+
681
+ #### Close Market (Synonym)
682
+
683
+ ```typescript
684
+ async closeMarket(params: CloseParams): Promise<CloseInstruction>
685
+ ```
686
+
687
+ Synonym for `close()`. Closes a market with the same parameters.
688
+
689
+ ```typescript
690
+ // Set wallet first (must be market authority)
691
+ client.wallet = yourWallet;
692
+
693
+ // Close market using synonym
694
+ const instruction = await client.jobs.closeMarket({
695
+ market: address('market-address'),
696
+ });
697
+
698
+ // Submit the instruction
699
+ await client.solana.buildSignAndSend(instruction);
700
+ ```
701
+
702
+ ### Real-time Monitoring
703
+
704
+ #### Monitor Account Updates
705
+
706
+ The SDK provides two monitoring methods using async iterators for real-time account updates via WebSocket:
707
+
708
+ **Simple Monitoring** (`monitor()`) - Automatically merges run account data into job events:
709
+
710
+ ```typescript
711
+ async monitor(): Promise<[AsyncIterable<SimpleMonitorEvent>, () => void]>
712
+ ```
713
+
714
+ ```typescript
715
+ import { MonitorEventType } from '@nosana/kit';
716
+
717
+ // Start monitoring
718
+ const [eventStream, stop] = await client.jobs.monitor();
719
+
720
+ // Process events using async iteration
721
+ for await (const event of eventStream) {
722
+ if (event.type === MonitorEventType.JOB) {
723
+ console.log('Job update:', event.data.address, event.data.state);
724
+ // event.data will have state, node, and timeStart from run account if it exists
725
+
726
+ // Process updates - save to database, trigger workflows, etc.
727
+ if (event.data.state === JobState.COMPLETED) {
728
+ await processCompletedJob(event.data);
729
+ }
730
+ } else if (event.type === MonitorEventType.MARKET) {
731
+ console.log('Market update:', event.data.address);
732
+ }
733
+ }
734
+
735
+ // Stop monitoring when done
736
+ stop();
737
+ ```
738
+
739
+ **Detailed Monitoring** (`monitorDetailed()`) - Provides separate events for job, market, and run accounts:
740
+
741
+ ```typescript
742
+ async monitorDetailed(): Promise<[AsyncIterable<MonitorEvent>, () => void]>
743
+ ```
744
+
745
+ ```typescript
746
+ import { MonitorEventType } from '@nosana/kit';
747
+
748
+ // Start detailed monitoring
749
+ const [eventStream, stop] = await client.jobs.monitorDetailed();
750
+
751
+ // Process events using async iteration
752
+ for await (const event of eventStream) {
753
+ switch (event.type) {
754
+ case MonitorEventType.JOB:
755
+ console.log('Job update:', event.data.address);
756
+ break;
757
+ case MonitorEventType.MARKET:
758
+ console.log('Market update:', event.data.address);
759
+ break;
760
+ case MonitorEventType.RUN:
761
+ console.log('Run started:', event.data.job, 'on node', event.data.node);
762
+ break;
763
+ }
764
+ }
765
+
766
+ // Stop monitoring when done
767
+ stop();
768
+ ```
769
+
770
+ 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.
771
+
772
+ ## Account Types
773
+
774
+ ### Job
775
+
776
+ ```typescript
777
+ type Job = {
778
+ address: Address;
779
+ state: JobState; // QUEUED | RUNNING | COMPLETED | STOPPED
780
+ ipfsJob: string | null; // IPFS CID of job definition
781
+ ipfsResult: string | null; // IPFS CID of job result
782
+ market: Address;
783
+ node: Address;
784
+ payer: Address;
785
+ project: Address;
786
+ price: number;
787
+ timeStart: number; // Unix timestamp
788
+ timeEnd: number; // Unix timestamp
789
+ timeout: number; // Seconds
790
+ };
791
+
792
+ enum JobState {
793
+ QUEUED = 0,
794
+ RUNNING = 1,
795
+ COMPLETED = 2,
796
+ STOPPED = 3,
797
+ }
798
+ ```
799
+
800
+ ### Run
801
+
802
+ ```typescript
803
+ type Run = {
804
+ address: Address;
805
+ job: Address; // Associated job
806
+ node: Address; // Node executing the job
807
+ time: number; // Unix timestamp
808
+ };
809
+ ```
810
+
811
+ ### Market
812
+
813
+ ```typescript
814
+ type Market = {
815
+ address: Address;
816
+ queueType: MarketQueueType; // JOB_QUEUE | NODE_QUEUE
817
+ jobPrice: number;
818
+ nodeStakeMinimum: number;
819
+ jobTimeout: number;
820
+ jobType: number;
821
+ project: Address;
822
+ // ... additional fields
823
+ };
824
+
825
+ enum MarketQueueType {
826
+ JOB_QUEUE = 0,
827
+ NODE_QUEUE = 1,
828
+ }
829
+ ```
830
+
831
+ ## Solana Service
832
+
833
+ General Solana utility service for low-level RPC operations, transactions, and PDA derivations.
834
+
835
+ ### Methods
836
+
837
+ ```typescript
838
+ // Build, sign, and send transaction in one call (convenience method)
839
+ buildSignAndSend(
840
+ instructions: Instruction | Instruction[],
841
+ options?: {
842
+ feePayer?: TransactionSigner;
843
+ commitment?: 'processed' | 'confirmed' | 'finalized';
844
+ }
845
+ ): Promise<Signature>
846
+
847
+ // Build transaction from instructions
848
+ buildTransaction(
849
+ instructions: Instruction | Instruction[],
850
+ options?: { feePayer?: TransactionSigner }
851
+ ): Promise<TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithBlockhashLifetime>
852
+
853
+ // Sign a transaction message
854
+ signTransaction(
855
+ transactionMessage: TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithBlockhashLifetime
856
+ ): Promise<SendableTransaction & Transaction & TransactionWithBlockhashLifetime>
857
+
858
+ // Send and confirm a signed transaction
859
+ sendTransaction(
860
+ transaction: SendableTransaction & Transaction & TransactionWithBlockhashLifetime,
861
+ options?: { commitment?: 'processed' | 'confirmed' | 'finalized' }
862
+ ): Promise<Signature>
863
+
864
+ // Get account balance
865
+ getBalance(address?: Address | string): Promise<bigint>
866
+
867
+ // Derive program derived address
868
+ pda(seeds: Array<Address | string>, programId: Address): Promise<Address>
869
+
870
+ // Get instruction to transfer SOL
871
+ transfer(params: {
872
+ to: Address | string;
873
+ amount: number | bigint;
874
+ from?: TransactionSigner;
875
+ }): Promise<Instruction> // Returns TransferSolInstruction
876
+ ```
877
+
878
+ ### Examples
879
+
880
+ ```typescript
881
+ // Send a single instruction (convenience method)
882
+ const signature = await client.solana.buildSignAndSend(instruction);
883
+
884
+ // Send multiple instructions atomically
885
+ const signature = await client.solana.buildSignAndSend([ix1, ix2, ix3]);
886
+
887
+ // Or build, sign, and send separately for more control
888
+ const transactionMessage = await client.solana.buildTransaction(instruction);
889
+ const signedTransaction = await client.solana.signTransaction(transactionMessage);
890
+ const signature = await client.solana.sendTransaction(signedTransaction);
891
+
892
+ // Check account balance
893
+ const balance = await client.solana.getBalance('address');
894
+ console.log(`Balance: ${balance} lamports`);
895
+
896
+ // Derive PDA
897
+ const pda = await client.solana.pda(['seed1', 'seed2'], programAddress);
898
+
899
+ // Get instruction to transfer SOL
900
+ const transferSolIx = await client.solana.transfer({
901
+ to: 'recipient-address',
902
+ amount: 1000000, // lamports (can be number or bigint)
903
+ // from is optional - uses wallet if not provided
904
+ });
905
+
906
+ // Execute the transfer
907
+ await client.solana.buildSignAndSend(transferSolIx);
908
+ ```
909
+
910
+ ## IPFS Service
911
+
912
+ 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.
913
+
914
+ ### Configuration
915
+
916
+ ```typescript
917
+ const client = createNosanaClient(NosanaNetwork.MAINNET, {
918
+ ipfs: {
919
+ api: 'https://api.pinata.cloud',
920
+ jwt: 'your-pinata-jwt-token',
921
+ gateway: 'https://gateway.pinata.cloud/ipfs/',
922
+ },
923
+ });
924
+ ```
925
+
926
+ ### Methods
927
+
928
+ ```typescript
929
+ // Pin JSON data to IPFS
930
+ pin(data: object): Promise<string>
931
+
932
+ // Pin a file to IPFS
933
+ pinFile(filePath: string): Promise<string>
934
+
935
+ // Retrieve data from IPFS
936
+ retrieve(hash: string | Uint8Array): Promise<any>
937
+ ```
938
+
939
+ ### Examples
940
+
941
+ ```typescript
942
+ // Pin job definition to IPFS
943
+ const cid = await client.ipfs.pin({
944
+ version: 1,
945
+ type: 'docker',
946
+ image: 'ubuntu:latest',
947
+ command: ['echo', 'hello'],
948
+ });
949
+ console.log('Pinned to IPFS:', cid);
950
+
951
+ // Pin a file to IPFS
952
+ const fileCid = await client.ipfs.pinFile('/path/to/file.txt');
953
+
954
+ // Retrieve job results from IPFS
955
+ const results = await client.ipfs.retrieve(job.ipfsResult);
956
+ console.log('Job results:', results);
957
+ ```
958
+
959
+ ### Utility Functions
960
+
961
+ The SDK also exports utility functions for converting between Solana hash formats and IPFS CIDs:
962
+
963
+ ```typescript
964
+ import { solBytesArrayToIpfsHash, ipfsHashToSolBytesArray } from '@nosana/kit';
965
+
966
+ // Convert Solana hash bytes to IPFS CID
967
+ const ipfsCid = solBytesArrayToIpfsHash(solanaHashBytes);
968
+
969
+ // Convert IPFS CID to Solana hash bytes
970
+ const solanaHash = ipfsHashToSolBytesArray(ipfsCid);
971
+ ```
972
+
973
+ ## API Service
974
+
975
+ The API service provides access to Nosana APIs for jobs, credits, and markets. It's automatically configured based on your authentication method.
976
+
977
+ ### Authentication Methods
978
+
979
+ The API service supports two authentication methods:
980
+
981
+ 1. **API Key Authentication** (Recommended for server-side applications)
982
+ - Provide an API key in the configuration
983
+ - API key takes precedence over wallet-based authentication
984
+
985
+ 2. **Wallet-Based Authentication** (For client-side applications)
986
+ - Set a wallet on the client
987
+ - Uses message signing for authentication
988
+ - Automatically enabled when a wallet is configured
989
+
990
+ ### Configuration
991
+
992
+ ```typescript
993
+ // Option 1: Use API key (recommended for servers)
994
+ const client = createNosanaClient(NosanaNetwork.MAINNET, {
995
+ api: {
996
+ apiKey: 'your-api-key-here',
997
+ },
998
+ });
999
+
1000
+ // Option 2: Use wallet-based auth (for client-side)
1001
+ const client = createNosanaClient(NosanaNetwork.MAINNET);
1002
+ client.wallet = myWallet;
1003
+
1004
+ // Option 3: API key takes precedence when both are provided
1005
+ const client = createNosanaClient(NosanaNetwork.MAINNET, {
1006
+ api: {
1007
+ apiKey: 'your-api-key-here',
1008
+ },
1009
+ wallet: myWallet, // API key will be used, not wallet
1010
+ });
1011
+ ```
1012
+
1013
+ ### Behavior
1014
+
1015
+ - **With API Key**: API is created immediately with API key authentication
1016
+ - **With Wallet**: API is created when wallet is set, using wallet-based authentication
1017
+ - **Without Both**: API is `undefined` until either an API key or wallet is provided
1018
+ - **Priority**: If both API key and wallet are provided, API key is used
1019
+
1020
+ ### API Structure
1021
+
1022
+ The API service provides access to three main APIs:
1023
+
1024
+ ```typescript
1025
+ client.api?.jobs // Jobs API
1026
+ client.api?.credits // Credits API
1027
+ client.api?.markets // Markets API
1028
+ ```
1029
+
1030
+ ### Examples
1031
+
1032
+ ```typescript
1033
+ // Using API key
1034
+ const client = createNosanaClient(NosanaNetwork.MAINNET, {
1035
+ api: { apiKey: 'your-api-key' },
1036
+ });
1037
+
1038
+ // API is immediately available
1039
+ if (client.api) {
1040
+ // Use the API
1041
+ const jobs = await client.api.jobs.list();
1042
+ }
1043
+
1044
+ // Using wallet-based auth
1045
+ const client = createNosanaClient(NosanaNetwork.MAINNET);
1046
+ client.wallet = myWallet;
1047
+
1048
+ // API is now available
1049
+ if (client.api) {
1050
+ const credits = await client.api.credits.get();
1051
+ }
1052
+
1053
+ // API updates reactively when wallet changes
1054
+ client.wallet = undefined; // API becomes undefined
1055
+ client.wallet = anotherWallet; // API is recreated with new wallet
1056
+ ```
1057
+
1058
+ ## Authorization Service
1059
+
1060
+ 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.
1061
+
1062
+ ### Behavior
1063
+
1064
+ - **With Wallet**: When a wallet is set, the authorization service provides all methods including `generate`, `validate`, `generateHeaders`, and `validateHeaders`.
1065
+ - **Without Wallet**: When no wallet is set, the authorization service only provides `validate` and `validateHeaders` methods (read-only validation).
1066
+
1067
+ ### Methods
1068
+
1069
+ ```typescript
1070
+ // Generate a signed message (requires wallet)
1071
+ generate(message: string | Uint8Array, options?: GenerateOptions): Promise<string>
1072
+
1073
+ // Validate a signed message
1074
+ validate(
1075
+ message: string | Uint8Array,
1076
+ signature: string | Uint8Array,
1077
+ publicKey?: string | Uint8Array
1078
+ ): Promise<boolean>
1079
+
1080
+ // Generate signed HTTP headers (requires wallet)
1081
+ generateHeaders(
1082
+ method: string,
1083
+ path: string,
1084
+ body?: string | Uint8Array,
1085
+ options?: GenerateHeaderOptions
1086
+ ): Promise<Headers>
1087
+
1088
+ // Validate HTTP headers
1089
+ validateHeaders(headers: Headers | Record<string, string>): Promise<boolean>
1090
+ ```
1091
+
1092
+ ### Examples
1093
+
1094
+ ```typescript
1095
+ // Set wallet first to enable signing
1096
+ client.wallet = myWallet;
1097
+
1098
+ // Generate a signed message
1099
+ const signedMessage = await client.authorization.generate('Hello, Nosana!');
1100
+ console.log('Signed message:', signedMessage);
1101
+
1102
+ // Validate a signed message
1103
+ const isValid = await client.authorization.validate('Hello, Nosana!', signedMessage);
1104
+ console.log('Message is valid:', isValid);
1105
+
1106
+ // Generate signed HTTP headers for API requests
1107
+ const headers = await client.authorization.generateHeaders(
1108
+ 'POST',
1109
+ '/api/jobs',
1110
+ JSON.stringify({ data: 'example' })
1111
+ );
1112
+
1113
+ // Use headers in HTTP request
1114
+ fetch('https://api.nosana.com/api/jobs', {
1115
+ method: 'POST',
1116
+ headers: headers,
1117
+ body: JSON.stringify({ data: 'example' }),
1118
+ });
1119
+
1120
+ // Validate incoming HTTP headers
1121
+ const isValidRequest = await client.authorization.validateHeaders(requestHeaders);
1122
+ if (!isValidRequest) {
1123
+ throw new Error('Invalid authorization');
1124
+ }
1125
+ ```
1126
+
1127
+ ### Use Cases
1128
+
1129
+ - **API Authentication**: Sign requests to Nosana APIs using message signatures
1130
+ - **Message Verification**: Verify signed messages from other parties
1131
+ - **Secure Communication**: Establish authenticated communication channels
1132
+ - **Request Authorization**: Validate incoming API requests
1133
+
1134
+ ## Merkle Distributor Program
1135
+
1136
+ The MerkleDistributorProgram provides methods to interact with merkle distributor accounts and claim tokens from distributions.
1137
+
1138
+ ### Get a Single Distributor
1139
+
1140
+ Fetch a merkle distributor account by its address:
1141
+
1142
+ ```typescript
1143
+ const distributor = await client.merkleDistributor.get('distributor-address');
1144
+
1145
+ console.log('Distributor:', distributor.address);
1146
+ console.log('Admin:', distributor.admin);
1147
+ console.log('Mint:', distributor.mint);
1148
+ console.log('Root:', distributor.root);
1149
+ ```
1150
+
1151
+ ### Get All Distributors
1152
+
1153
+ Fetch all merkle distributor accounts:
1154
+
1155
+ ```typescript
1156
+ const distributors = await client.merkleDistributor.all();
1157
+ console.log(`Found ${distributors.length} distributors`);
1158
+ ```
1159
+
1160
+ ### Get Claim Status
1161
+
1162
+ Fetch claim status for a specific distributor and claimant:
1163
+
1164
+ ```typescript
1165
+ // Get claim status for the wallet's address
1166
+ const claimStatus =
1167
+ await client.merkleDistributor.getClaimStatusForDistributor('distributor-address');
1168
+
1169
+ // Or specify a claimant address
1170
+ const claimStatus = await client.merkleDistributor.getClaimStatusForDistributor(
1171
+ 'distributor-address',
1172
+ 'claimant-address'
1173
+ );
1174
+
1175
+ if (claimStatus) {
1176
+ console.log('Claimed:', claimStatus.claimed);
1177
+ console.log('Amount Unlocked:', claimStatus.amountUnlocked);
1178
+ console.log('Amount Locked:', claimStatus.amountLocked);
1179
+ } else {
1180
+ console.log('No claim status found');
1181
+ }
1182
+ ```
1183
+
1184
+ ### Get Claim Status PDA
1185
+
1186
+ Derive the ClaimStatus PDA address:
1187
+
1188
+ ```typescript
1189
+ // Derive for wallet's address
1190
+ const pda = await client.merkleDistributor.getClaimStatusPda('distributor-address');
1191
+
1192
+ // Or specify a claimant address
1193
+ const pda = await client.merkleDistributor.getClaimStatusPda(
1194
+ 'distributor-address',
1195
+ 'claimant-address'
1196
+ );
1197
+ ```
1198
+
1199
+ ### Claim Tokens
1200
+
1201
+ Claim tokens from a merkle distributor:
1202
+
1203
+ ```typescript
1204
+ // Set wallet first
1205
+ client.wallet = yourWallet;
1206
+
1207
+ // Claim tokens
1208
+ const instruction = await client.merkleDistributor.claim({
1209
+ distributor: 'distributor-address',
1210
+ amountUnlocked: 1000000, // Amount in smallest unit
1211
+ amountLocked: 500000,
1212
+ proof: [
1213
+ /* merkle proof array */
1214
+ ],
1215
+ target: ClaimTarget.YES, // or ClaimTarget.NO
1216
+ });
1217
+
1218
+ // Submit the instruction
1219
+ await client.solana.buildSignAndSend(instruction);
1220
+ ```
1221
+
1222
+ ### Clawback Tokens
1223
+
1224
+ Clawback tokens from a merkle distributor (admin only):
1225
+
1226
+ ```typescript
1227
+ // Set wallet first (must be admin)
1228
+ client.wallet = adminWallet;
1229
+
1230
+ // Clawback tokens
1231
+ const instruction = await client.merkleDistributor.clawback({
1232
+ distributor: 'distributor-address',
1233
+ });
1234
+
1235
+ // Submit the instruction
1236
+ await client.solana.buildSignAndSend(instruction);
1237
+ ```
1238
+
1239
+ ### Type Definitions
1240
+
1241
+ ```typescript
1242
+ interface MerkleDistributor {
1243
+ address: Address;
1244
+ admin: Address;
1245
+ mint: Address;
1246
+ root: string; // Base58 encoded merkle root
1247
+ buffer0: string;
1248
+ buffer1: string;
1249
+ buffer2: string;
1250
+ // ... additional fields
1251
+ }
1252
+
1253
+ interface ClaimStatus {
1254
+ address: Address;
1255
+ distributor: Address;
1256
+ claimant: Address;
1257
+ claimed: boolean;
1258
+ amountUnlocked: number;
1259
+ amountLocked: number;
1260
+ }
1261
+
1262
+ enum ClaimTarget {
1263
+ YES = 'YES',
1264
+ NO = 'NO',
1265
+ }
1266
+ ```
1267
+
1268
+ ### Use Cases
1269
+
1270
+ - **Airdrop Claims**: Allow users to claim tokens from merkle tree distributions
1271
+ - **Reward Distribution**: Distribute rewards to eligible addresses
1272
+ - **Token Vesting**: Manage locked and unlocked token distributions
1273
+ - **Governance**: Distribute governance tokens to eligible participants
1274
+
1275
+ ## Staking Program
1276
+
1277
+ The StakeProgram provides methods to interact with Nosana staking accounts on-chain.
1278
+
1279
+ ### Get a Single Stake Account
1280
+
1281
+ Fetch a stake account by its address:
1282
+
1283
+ ```typescript
1284
+ const stake = await client.stake.get('stake-account-address');
1285
+
1286
+ console.log('Stake Account:', stake.address);
1287
+ console.log('Authority:', stake.authority);
1288
+ console.log('Staked Amount:', stake.amount);
1289
+ console.log('xNOS Tokens:', stake.xnos);
1290
+ console.log('Duration:', stake.duration);
1291
+ console.log('Time to Unstake:', stake.timeUnstake);
1292
+ console.log('Vault:', stake.vault);
1293
+ ```
1294
+
1295
+ ### Get Multiple Stake Accounts
1296
+
1297
+ Fetch multiple stake accounts by their addresses:
1298
+
1299
+ ```typescript
1300
+ const addresses = ['address1', 'address2', 'address3'];
1301
+ const stakes = await client.stake.multiple(addresses);
1302
+
1303
+ stakes.forEach((stake) => {
1304
+ console.log(`${stake.address}: ${stake.amount} staked`);
1305
+ });
1306
+ ```
1307
+
1308
+ ### Get All Stake Accounts
1309
+
1310
+ Fetch all stake accounts in the program:
1311
+
1312
+ ```typescript
1313
+ // Get all stakes
1314
+ const allStakes = await client.stake.all();
1315
+ console.log(`Found ${allStakes.length} stake accounts`);
1316
+ ```
1317
+
1318
+ ### Type Definitions
1319
+
1320
+ ```typescript
1321
+ interface Stake {
1322
+ address: Address;
1323
+ amount: number;
1324
+ authority: Address;
1325
+ duration: number;
1326
+ timeUnstake: number;
1327
+ vault: Address;
1328
+ vaultBump: number;
1329
+ xnos: number;
1330
+ }
1331
+ ```
1332
+
1333
+ ### Use Cases
1334
+
1335
+ - **Portfolio Tracking**: Monitor your staked NOS tokens
1336
+ - **Analytics**: Analyze staking patterns and distributions
1337
+ - **Governance**: Check voting power based on staked amounts
1338
+ - **Rewards Calculation**: Calculate rewards based on stake duration and amount
1339
+
1340
+ ### Example: Analyze Staking Distribution
1341
+
1342
+ ```typescript
1343
+ const allStakes = await client.stake.all();
1344
+
1345
+ // Calculate total staked
1346
+ const totalStaked = allStakes.reduce((sum, stake) => sum + stake.amount, 0);
1347
+
1348
+ // Find average stake
1349
+ const averageStake = totalStaked / allStakes.length;
1350
+
1351
+ // Find largest stake
1352
+ const largestStake = allStakes.reduce((max, stake) => Math.max(max, stake.amount), 0);
1353
+
1354
+ console.log('Staking Statistics:');
1355
+ console.log(`Total Staked: ${totalStaked.toLocaleString()} NOS`);
1356
+ console.log(`Average Stake: ${averageStake.toLocaleString()} NOS`);
1357
+ console.log(`Largest Stake: ${largestStake.toLocaleString()} NOS`);
1358
+ console.log(`Number of Stakers: ${allStakes.length}`);
1359
+ ```
1360
+
1361
+ ## Token Service
1362
+
1363
+ 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`.
1364
+
1365
+ ### Get All Token Holders
1366
+
1367
+ Fetch all accounts holding NOS tokens using a single RPC call:
1368
+
1369
+ ```typescript
1370
+ // Get all holders (excludes zero balance accounts by default)
1371
+ const holders = await client.nos.getAllTokenHolders();
1372
+
1373
+ console.log(`Found ${holders.length} NOS token holders`);
1374
+
1375
+ holders.forEach((holder) => {
1376
+ console.log(`${holder.owner}: ${holder.uiAmount} NOS`);
1377
+ });
1378
+
1379
+ // Include accounts with zero balance
1380
+ const allAccounts = await client.nos.getAllTokenHolders({ includeZeroBalance: true });
1381
+ console.log(`Total accounts: ${allAccounts.length}`);
1382
+
1383
+ // Exclude PDA accounts (smart contract-owned token accounts)
1384
+ const userAccounts = await client.nos.getAllTokenHolders({ excludePdaAccounts: true });
1385
+ console.log(`User-owned accounts: ${userAccounts.length}`);
1386
+
1387
+ // Combine filters
1388
+ const activeUsers = await client.nos.getAllTokenHolders({
1389
+ includeZeroBalance: false,
1390
+ excludePdaAccounts: true,
1391
+ });
1392
+ console.log(`Active user accounts: ${activeUsers.length}`);
1393
+ ```
1394
+
1395
+ ### Get Token Account for Address
1396
+
1397
+ Retrieve the NOS token account for a specific owner:
1398
+
1399
+ ```typescript
1400
+ const account = await client.nos.getTokenAccountForAddress('owner-address');
1401
+
1402
+ if (account) {
1403
+ console.log('Token Account:', account.pubkey);
1404
+ console.log('Owner:', account.owner);
1405
+ console.log('Balance:', account.uiAmount, 'NOS');
1406
+ console.log('Raw Amount:', account.amount.toString());
1407
+ console.log('Decimals:', account.decimals);
1408
+ } else {
1409
+ console.log('No NOS token account found');
1410
+ }
1411
+ ```
1412
+
1413
+ ### Get Balance
1414
+
1415
+ Convenience method to get just the NOS balance for an address:
1416
+
1417
+ ```typescript
1418
+ const balance = await client.nos.getBalance('owner-address');
1419
+ console.log(`Balance: ${balance} NOS`);
1420
+ // Returns 0 if no token account exists
1421
+ ```
1422
+
1423
+ ### Transfer Tokens
1424
+
1425
+ 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:
1426
+
1427
+ ```typescript
1428
+ // Get transfer instruction(s)
1429
+ const instructions = await client.nos.transfer({
1430
+ to: 'recipient-address',
1431
+ amount: 1000000, // token base units (can be number or bigint)
1432
+ // from is optional - uses wallet if not provided
1433
+ });
1434
+
1435
+ // Execute the transfer
1436
+ // instructions is a tuple:
1437
+ // - [TransferInstruction] when recipient ATA exists (1 instruction)
1438
+ // - [CreateAssociatedTokenIdempotentInstruction, TransferInstruction] when ATA needs creation (2 instructions)
1439
+ await client.solana.buildSignAndSend(instructions);
1440
+ ```
1441
+
1442
+ The function automatically:
1443
+ - Finds the sender's associated token account
1444
+ - Finds the recipient's associated token account
1445
+ - Creates the recipient's ATA if it doesn't exist (returns 2 instructions: create ATA + transfer)
1446
+ - Returns only the transfer instruction if the recipient's ATA already exists (returns 1 instruction)
1447
+
1448
+ ### Type Definitions
1449
+
1450
+ ```typescript
1451
+ interface TokenAccount {
1452
+ pubkey: Address;
1453
+ owner: Address;
1454
+ mint: Address;
1455
+ amount: bigint;
1456
+ decimals: number;
1457
+ }
1458
+
1459
+ interface TokenAccountWithBalance extends TokenAccount {
1460
+ uiAmount: number; // Balance with decimals applied
1461
+ }
1462
+ ```
1463
+
1464
+ ### Use Cases
1465
+
1466
+ - **Analytics**: Analyze token distribution and holder statistics
1467
+ - **Airdrops**: Get list of all token holders for campaigns
1468
+ - **Balance Checks**: Check NOS balances for specific addresses
1469
+ - **Leaderboards**: Create holder rankings sorted by balance
1470
+ - **Monitoring**: Track large holder movements
1471
+
1472
+ ### Example: Filter Large Holders
1473
+
1474
+ ```typescript
1475
+ const holders = await client.nos.getAllTokenHolders();
1476
+
1477
+ // Find holders with at least 1000 NOS
1478
+ const largeHolders = holders.filter((h) => h.uiAmount >= 1000);
1479
+
1480
+ // Sort by balance descending
1481
+ largeHolders.sort((a, b) => b.uiAmount - a.uiAmount);
1482
+
1483
+ // Display top 10
1484
+ largeHolders.slice(0, 10).forEach((holder, i) => {
1485
+ console.log(`${i + 1}. ${holder.owner}: ${holder.uiAmount.toLocaleString()} NOS`);
1486
+ });
1487
+ ```
1488
+
1489
+ ## Error Handling
1490
+
1491
+ The SDK provides structured error handling with specific error codes.
1492
+
1493
+ ### NosanaError
1494
+
1495
+ ```typescript
1496
+ class NosanaError extends Error {
1497
+ code: string;
1498
+ details?: any;
1499
+ }
1500
+ ```
1501
+
1502
+ ### Error Codes
1503
+
1504
+ ```typescript
1505
+ enum ErrorCodes {
1506
+ INVALID_NETWORK = 'INVALID_NETWORK',
1507
+ INVALID_CONFIG = 'INVALID_CONFIG',
1508
+ RPC_ERROR = 'RPC_ERROR',
1509
+ TRANSACTION_ERROR = 'TRANSACTION_ERROR',
1510
+ PROGRAM_ERROR = 'PROGRAM_ERROR',
1511
+ VALIDATION_ERROR = 'VALIDATION_ERROR',
1512
+ NO_WALLET = 'NO_WALLET',
1513
+ FILE_ERROR = 'FILE_ERROR',
1514
+ WALLET_CONVERSION_ERROR = 'WALLET_CONVERSION_ERROR',
1515
+ }
1516
+ ```
1517
+
1518
+ ### Examples
1519
+
1520
+ ```typescript
1521
+ import { NosanaError, ErrorCodes } from '@nosana/kit';
1522
+
1523
+ try {
1524
+ const job = await client.jobs.get('invalid-address');
1525
+ } catch (error) {
1526
+ if (error instanceof NosanaError) {
1527
+ switch (error.code) {
1528
+ case ErrorCodes.RPC_ERROR:
1529
+ console.error('RPC connection failed:', error.message);
1530
+ break;
1531
+ case ErrorCodes.NO_WALLET:
1532
+ console.error('Wallet not configured');
1533
+ client.wallet = myWallet;
1534
+ break;
1535
+ case ErrorCodes.TRANSACTION_ERROR:
1536
+ console.error('Transaction failed:', error.details);
1537
+ break;
1538
+ default:
1539
+ console.error('Unknown error:', error.message);
1540
+ }
1541
+ } else {
1542
+ throw error; // Re-throw non-Nosana errors
1543
+ }
1544
+ }
1545
+ ```
1546
+
1547
+ ## Logging
1548
+
1549
+ The SDK includes a built-in singleton logger with configurable levels.
1550
+
1551
+ ### Log Levels
1552
+
1553
+ ```typescript
1554
+ enum LogLevel {
1555
+ DEBUG = 'debug',
1556
+ INFO = 'info',
1557
+ WARN = 'warn',
1558
+ ERROR = 'error',
1559
+ NONE = 'none',
1560
+ }
1561
+ ```
1562
+
1563
+ ### Configuration
1564
+
1565
+ ```typescript
1566
+ import { createNosanaClient, LogLevel } from '@nosana/kit';
1567
+
1568
+ // Set log level during initialization
1569
+ const client = createNosanaClient(NosanaNetwork.MAINNET, {
1570
+ logLevel: LogLevel.DEBUG,
1571
+ });
1572
+
1573
+ // Access logger directly
1574
+ client.logger.info('Information message');
1575
+ client.logger.error('Error message');
1576
+ client.logger.debug('Debug details');
1577
+ ```
1578
+
1579
+ ## Testing
1580
+
1581
+ The SDK includes comprehensive test coverage.
1582
+
1583
+ ```bash
1584
+ # Run tests (all workspace packages; only @nosana/kit has tests for now)
1585
+ pnpm test
1586
+
1587
+ # Kit-only: watch mode or coverage
1588
+ pnpm --filter @nosana/kit run test:watch
1589
+ pnpm --filter @nosana/kit run test:coverage
1590
+ ```
1591
+
1592
+ ## Development
1593
+
1594
+ This project uses **pnpm workspaces** with a monorepo structure:
1595
+
1596
+ - **Root** – Workspace orchestration (build, dev, test, generate-clients)
1597
+ - **`packages/kit`** (`@nosana/kit`) – Main SDK
1598
+ - **`packages/generated_clients/*`** – Generated Solana program clients (jobs, stake, merkle_distributor)
1599
+ - **`docs`** (`@nosana/docs`) – Documentation site (VitePress)
1600
+
1601
+ All commands below are run from the **repository root** unless stated otherwise.
1602
+
1603
+ ### Setup
1604
+
1605
+ ```bash
1606
+ # Install dependencies for all workspace packages
1607
+ pnpm install
1608
+
1609
+ # Build all packages (generated clients first, then kit, then docs)
1610
+ pnpm build
1611
+
1612
+ # Run tests
1613
+ pnpm test
1614
+ ```
1615
+
1616
+ ### Building only what you need
1617
+
1618
+ If you're only working on one package, you can build that package and its workspace dependencies instead of the whole repo:
1619
+
1620
+ ```bash
1621
+ # From root: build kit and its deps (generated clients), skip docs
1622
+ pnpm --filter @nosana/kit run build:with-deps
1623
+
1624
+ # Or from the package directory
1625
+ cd packages/kit && pnpm run build:with-deps
1626
+ ```
1627
+
1628
+ Same for docs (builds kit first, then docs):
1629
+
1630
+ ```bash
1631
+ pnpm --filter @nosana/docs run build:with-deps
1632
+ ```
1633
+
1634
+ Use `pnpm build` when you want everything built (e.g. before opening a PR or publishing).
1635
+
1636
+ ### Regenerating program clients
1637
+
1638
+ After changing IDLs in `/idl`, regenerate the client packages:
1639
+
1640
+ ```bash
1641
+ pnpm run generate-clients
1642
+ ```
1643
+
1644
+ Generated code is written into each package’s **`src/`** directory so `package.json` and `tsconfig.json` at the package root are never overwritten. Then run `pnpm build` so the rest of the workspace picks up the changes.
1645
+
1646
+ ### Development mode
1647
+
1648
+ Start all watch processes and the docs dev server in parallel:
1649
+
1650
+ ```bash
1651
+ # From root (run `pnpm build` once first so dist/ exist)
1652
+ pnpm dev
1653
+ ```
1654
+
1655
+ This runs `tsc --watch` for the generated client packages and kit, and starts the VitePress dev server for docs.
1656
+
1657
+ To work only on kit (no docs server), run watch for kit and its workspace deps:
1658
+
1659
+ ```bash
1660
+ pnpm --filter @nosana/kit run dev:with-deps
1661
+ # or from package dir: cd packages/kit && pnpm run dev:with-deps
1662
+ ```
1663
+
1664
+ To work on docs (kit + docs dev server):
1665
+
1666
+ ```bash
1667
+ pnpm --filter @nosana/docs run dev:with-deps
1668
+ ```
1669
+
1670
+ ### Lint and format
1671
+
1672
+ From root, lint and format run in all workspace packages (only `@nosana/kit` has real lint/format for now):
1673
+
1674
+ ```bash
1675
+ pnpm lint
1676
+ pnpm format
1677
+ pnpm format:fix
1678
+ ```
1679
+
1680
+ For a single package: `pnpm --filter @nosana/kit run lint` (or `format` / `format:fix`).
1681
+
1682
+ ### Documentation
1683
+
1684
+ Run docs scripts via the filter (no root shorthand; use the same pattern for any package-specific script):
1685
+
1686
+ ```bash
1687
+ # Start docs dev server
1688
+ pnpm --filter @nosana/docs dev
1689
+
1690
+ # Build docs
1691
+ pnpm --filter @nosana/docs build
1692
+
1693
+ # Preview built docs
1694
+ pnpm --filter @nosana/docs preview
1695
+ ```
1696
+
1697
+ ## TypeScript Support
1698
+
1699
+ The SDK is written in TypeScript and provides complete type definitions. All types are exported for use in your applications:
1700
+
1701
+ ```typescript
1702
+ import type {
1703
+ Job,
1704
+ Run,
1705
+ Market,
1706
+ JobState,
1707
+ MarketQueueType,
1708
+ Stake,
1709
+ MerkleDistributor,
1710
+ ClaimStatus,
1711
+ ClaimTarget,
1712
+ ClientConfig,
1713
+ NosanaClient,
1714
+ Wallet,
1715
+ Address,
1716
+ } from '@nosana/kit';
1717
+
1718
+ // The `address` utility function is also available for creating typed addresses
1719
+ import { address } from '@nosana/kit';
1720
+ const jobAddress = address('your-job-address');
1721
+ ```
1722
+
1723
+ ## Dependencies
1724
+
1725
+ Core dependencies:
1726
+
1727
+ - `@solana/kit` 5.0.0 - Solana web3 library
1728
+ - `@solana-program/token` 0.8.0 - Token program utilities
1729
+ - `@solana-program/system` 0.10.0 - System program utilities
1730
+ - `@solana-program/compute-budget` 0.11.0 - Compute budget utilities
1731
+ - `bs58` 6.0.0 - Base58 encoding
1732
+
1733
+ ## License
1734
+
1735
+ MIT
1736
+
1737
+ ## Links
1738
+
1739
+ - [Nosana Documentation](https://docs.nosana.com)
1740
+ - [Nosana Network](https://nosana.com)
1741
+ - [GitHub Repository](https://github.com/nosana-ci/nosana-kit)
1742
+ - [NPM Package](https://www.npmjs.com/package/@nosana/kit)