@nosana/kit 2.0.66 → 2.0.69
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.
- package/README.md +1742 -0
- package/package.json +6 -6
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)
|