@nosana/kit 0.1.7 → 1.0.2
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/.gitlab-ci.yml +41 -9
- package/.prettierignore +17 -0
- package/README.md +729 -59
- package/dist/config/defaultConfigs.d.ts +1 -1
- package/dist/config/defaultConfigs.js +2 -2
- package/dist/config/types.d.ts +1 -1
- package/dist/config/utils.d.ts +1 -1
- package/dist/config/utils.js +5 -5
- package/dist/errors/NosanaError.d.ts +1 -1
- package/dist/generated_clients/jobs/instructions/assign.d.ts +79 -0
- package/dist/generated_clients/jobs/instructions/assign.js +120 -0
- package/dist/generated_clients/jobs/instructions/extend.d.ts +9 -6
- package/dist/generated_clients/jobs/instructions/extend.js +4 -1
- package/dist/generated_clients/jobs/instructions/finish.d.ts +10 -10
- package/dist/generated_clients/jobs/instructions/finish.js +6 -6
- package/dist/generated_clients/jobs/instructions/index.d.ts +1 -0
- package/dist/generated_clients/jobs/instructions/index.js +1 -0
- package/dist/generated_clients/jobs/programs/nosanaJobs.d.ts +18 -15
- package/dist/generated_clients/jobs/programs/nosanaJobs.js +18 -14
- package/dist/generated_clients/staking/accounts/index.d.ts +9 -0
- package/dist/generated_clients/staking/accounts/index.js +9 -0
- package/dist/generated_clients/staking/accounts/settingsAccount.d.ts +29 -0
- package/dist/generated_clients/staking/accounts/settingsAccount.js +55 -0
- package/dist/generated_clients/staking/accounts/stakeAccount.d.ts +39 -0
- package/dist/generated_clients/staking/accounts/stakeAccount.js +65 -0
- package/dist/generated_clients/staking/errors/index.d.ts +8 -0
- package/dist/generated_clients/staking/errors/index.js +8 -0
- package/dist/generated_clients/staking/errors/nosanaStaking.d.ts +45 -0
- package/dist/generated_clients/staking/errors/nosanaStaking.js +62 -0
- package/dist/generated_clients/staking/index.d.ts +11 -0
- package/dist/generated_clients/staking/index.js +11 -0
- package/dist/generated_clients/staking/instructions/close.d.ts +48 -0
- package/dist/generated_clients/staking/instructions/close.js +81 -0
- package/dist/generated_clients/staking/instructions/extend.d.ts +43 -0
- package/dist/generated_clients/staking/instructions/extend.js +73 -0
- package/dist/generated_clients/staking/instructions/index.d.ts +17 -0
- package/dist/generated_clients/staking/instructions/index.js +17 -0
- package/dist/generated_clients/staking/instructions/init.d.ts +45 -0
- package/dist/generated_clients/staking/instructions/init.js +82 -0
- package/dist/generated_clients/staking/instructions/restake.d.ts +42 -0
- package/dist/generated_clients/staking/instructions/restake.js +70 -0
- package/dist/generated_clients/staking/instructions/slash.d.ts +55 -0
- package/dist/generated_clients/staking/instructions/slash.js +90 -0
- package/dist/generated_clients/staking/instructions/stake.d.ts +64 -0
- package/dist/generated_clients/staking/instructions/stake.js +106 -0
- package/dist/generated_clients/staking/instructions/topup.d.ts +52 -0
- package/dist/generated_clients/staking/instructions/topup.js +87 -0
- package/dist/generated_clients/staking/instructions/unstake.d.ts +42 -0
- package/dist/generated_clients/staking/instructions/unstake.js +70 -0
- package/dist/generated_clients/staking/instructions/updateSettings.d.ts +45 -0
- package/dist/generated_clients/staking/instructions/updateSettings.js +73 -0
- package/dist/generated_clients/staking/instructions/withdraw.d.ts +48 -0
- package/dist/generated_clients/staking/instructions/withdraw.js +81 -0
- package/dist/generated_clients/staking/programs/index.d.ts +8 -0
- package/dist/generated_clients/staking/programs/index.js +8 -0
- package/dist/generated_clients/staking/programs/nosanaStaking.d.ts +53 -0
- package/dist/generated_clients/staking/programs/nosanaStaking.js +71 -0
- package/dist/generated_clients/staking/shared/index.d.ts +49 -0
- package/dist/generated_clients/staking/shared/index.js +86 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.js +18 -136
- package/dist/ipfs/IPFS.js +3 -0
- package/dist/programs/BaseProgram.d.ts +1 -1
- package/dist/programs/BaseProgram.js +3 -7
- package/dist/programs/JobsProgram.d.ts +37 -37
- package/dist/programs/JobsProgram.js +63 -67
- package/dist/programs/StakeProgram.d.ts +29 -0
- package/dist/programs/StakeProgram.js +91 -0
- package/dist/services/NosService.d.ts +48 -0
- package/dist/services/NosService.js +134 -0
- package/dist/{solana/SolanaUtils.d.ts → services/SolanaService.d.ts} +5 -5
- package/dist/{solana/SolanaUtils.js → services/SolanaService.js} +17 -7
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/walletConverter.d.ts +9 -0
- package/dist/utils/walletConverter.js +141 -0
- package/eslint.config.js +49 -0
- package/examples/node/README.md +115 -0
- package/examples/node/nos-service.ts +117 -0
- package/examples/node/package.json +3 -1
- package/examples/node/retrieve.ts +2 -2
- package/examples/node/stake-program.ts +84 -0
- package/package.json +11 -8
- package/scripts/generate-clients.ts +20 -7
- package/vitest.config.ts +31 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseProgram } from './BaseProgram.js';
|
|
2
|
-
import { generateKeyPairSigner, parseBase64RpcAccount } from 'gill';
|
|
2
|
+
import { generateKeyPairSigner, parseBase64RpcAccount, } from 'gill';
|
|
3
3
|
import { ErrorCodes, NosanaError } from '../index.js';
|
|
4
|
-
import * as programClient from
|
|
4
|
+
import * as programClient from '../generated_clients/jobs/index.js';
|
|
5
5
|
import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
|
|
6
6
|
import bs58 from 'bs58';
|
|
7
7
|
import { IPFS } from '../ipfs/IPFS.js';
|
|
@@ -65,8 +65,8 @@ export class JobsProgram extends BaseProgram {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
/**
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
* Fetch a run account by address
|
|
69
|
+
*/
|
|
70
70
|
async market(addr) {
|
|
71
71
|
try {
|
|
72
72
|
const marketAccount = await this.client.fetchMarketAccount(this.sdk.solana.rpc, addr);
|
|
@@ -79,18 +79,17 @@ export class JobsProgram extends BaseProgram {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
* Fetch multiple job accounts by address
|
|
83
|
+
*/
|
|
84
84
|
async multiple(addresses, checkRuns = false) {
|
|
85
85
|
try {
|
|
86
86
|
const jobAccounts = await this.client.fetchAllJobAccount(this.sdk.solana.rpc, addresses);
|
|
87
|
-
;
|
|
88
|
-
const jobs = jobAccounts.map(jobAccount => (this.transformJobAccount(jobAccount)));
|
|
87
|
+
const jobs = jobAccounts.map((jobAccount) => this.transformJobAccount(jobAccount));
|
|
89
88
|
if (checkRuns) {
|
|
90
89
|
const runs = await this.runs();
|
|
91
|
-
jobs.forEach(job => {
|
|
90
|
+
jobs.forEach((job) => {
|
|
92
91
|
if (job.state === JobState.QUEUED) {
|
|
93
|
-
const run = runs.find(run => run.job === job.address);
|
|
92
|
+
const run = runs.find((run) => run.job === job.address);
|
|
94
93
|
if (run) {
|
|
95
94
|
job.state = JobState.RUNNING;
|
|
96
95
|
job.timeStart = run.time;
|
|
@@ -107,8 +106,8 @@ export class JobsProgram extends BaseProgram {
|
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
108
|
/**
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
* Fetch all job accounts
|
|
110
|
+
*/
|
|
112
111
|
async all(filters, checkRuns = false) {
|
|
113
112
|
try {
|
|
114
113
|
const extraGPAFilters = [];
|
|
@@ -118,7 +117,7 @@ export class JobsProgram extends BaseProgram {
|
|
|
118
117
|
memcmp: {
|
|
119
118
|
offset: BigInt(208),
|
|
120
119
|
bytes: bs58.encode(Buffer.from([filters.state])),
|
|
121
|
-
encoding:
|
|
120
|
+
encoding: 'base58',
|
|
122
121
|
},
|
|
123
122
|
});
|
|
124
123
|
}
|
|
@@ -127,7 +126,7 @@ export class JobsProgram extends BaseProgram {
|
|
|
127
126
|
memcmp: {
|
|
128
127
|
offset: BigInt(176),
|
|
129
128
|
bytes: filters.project.toString(),
|
|
130
|
-
encoding:
|
|
129
|
+
encoding: 'base58',
|
|
131
130
|
},
|
|
132
131
|
});
|
|
133
132
|
}
|
|
@@ -136,7 +135,7 @@ export class JobsProgram extends BaseProgram {
|
|
|
136
135
|
memcmp: {
|
|
137
136
|
offset: BigInt(104),
|
|
138
137
|
bytes: filters.node.toString(),
|
|
139
|
-
encoding:
|
|
138
|
+
encoding: 'base58',
|
|
140
139
|
},
|
|
141
140
|
});
|
|
142
141
|
}
|
|
@@ -145,20 +144,20 @@ export class JobsProgram extends BaseProgram {
|
|
|
145
144
|
memcmp: {
|
|
146
145
|
offset: BigInt(72),
|
|
147
146
|
bytes: filters.market.toString(),
|
|
148
|
-
encoding:
|
|
147
|
+
encoding: 'base58',
|
|
149
148
|
},
|
|
150
149
|
});
|
|
151
150
|
}
|
|
152
151
|
}
|
|
153
152
|
const getProgramAccountsResponse = await this.sdk.solana.rpc
|
|
154
153
|
.getProgramAccounts(this.getProgramId(), {
|
|
155
|
-
encoding:
|
|
154
|
+
encoding: 'base64',
|
|
156
155
|
filters: [
|
|
157
156
|
{
|
|
158
157
|
memcmp: {
|
|
159
158
|
offset: BigInt(0),
|
|
160
159
|
bytes: bs58.encode(Buffer.from(programClient.JOB_ACCOUNT_DISCRIMINATOR)),
|
|
161
|
-
encoding:
|
|
160
|
+
encoding: 'base58',
|
|
162
161
|
},
|
|
163
162
|
},
|
|
164
163
|
...extraGPAFilters,
|
|
@@ -179,9 +178,9 @@ export class JobsProgram extends BaseProgram {
|
|
|
179
178
|
.filter((account) => account !== null);
|
|
180
179
|
if (checkRuns) {
|
|
181
180
|
const runs = await this.runs();
|
|
182
|
-
jobs.forEach(job => {
|
|
181
|
+
jobs.forEach((job) => {
|
|
183
182
|
if (job.state === JobState.QUEUED) {
|
|
184
|
-
const run = runs.find(run => run.job === job.address);
|
|
183
|
+
const run = runs.find((run) => run.job === job.address);
|
|
185
184
|
if (run) {
|
|
186
185
|
job.state = JobState.RUNNING;
|
|
187
186
|
job.timeStart = run.time;
|
|
@@ -198,8 +197,8 @@ export class JobsProgram extends BaseProgram {
|
|
|
198
197
|
}
|
|
199
198
|
}
|
|
200
199
|
/**
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
* Fetch all run accounts
|
|
201
|
+
*/
|
|
203
202
|
async runs(filters) {
|
|
204
203
|
try {
|
|
205
204
|
const extraGPAFilters = [];
|
|
@@ -209,7 +208,7 @@ export class JobsProgram extends BaseProgram {
|
|
|
209
208
|
memcmp: {
|
|
210
209
|
offset: BigInt(40),
|
|
211
210
|
bytes: filters.node.toString(),
|
|
212
|
-
encoding:
|
|
211
|
+
encoding: 'base58',
|
|
213
212
|
},
|
|
214
213
|
});
|
|
215
214
|
}
|
|
@@ -218,20 +217,20 @@ export class JobsProgram extends BaseProgram {
|
|
|
218
217
|
memcmp: {
|
|
219
218
|
offset: BigInt(8),
|
|
220
219
|
bytes: filters.job.toString(),
|
|
221
|
-
encoding:
|
|
220
|
+
encoding: 'base58',
|
|
222
221
|
},
|
|
223
222
|
});
|
|
224
223
|
}
|
|
225
224
|
}
|
|
226
225
|
const getProgramAccountsResponse = await this.sdk.solana.rpc
|
|
227
226
|
.getProgramAccounts(this.getProgramId(), {
|
|
228
|
-
encoding:
|
|
227
|
+
encoding: 'base64',
|
|
229
228
|
filters: [
|
|
230
229
|
{
|
|
231
230
|
memcmp: {
|
|
232
231
|
offset: BigInt(0),
|
|
233
232
|
bytes: bs58.encode(Buffer.from(programClient.RUN_ACCOUNT_DISCRIMINATOR)),
|
|
234
|
-
encoding:
|
|
233
|
+
encoding: 'base58',
|
|
235
234
|
},
|
|
236
235
|
},
|
|
237
236
|
],
|
|
@@ -257,19 +256,19 @@ export class JobsProgram extends BaseProgram {
|
|
|
257
256
|
}
|
|
258
257
|
}
|
|
259
258
|
/**
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
* Fetch all market accounts
|
|
260
|
+
*/
|
|
262
261
|
async markets() {
|
|
263
262
|
try {
|
|
264
263
|
const getProgramAccountsResponse = await this.sdk.solana.rpc
|
|
265
264
|
.getProgramAccounts(this.getProgramId(), {
|
|
266
|
-
encoding:
|
|
265
|
+
encoding: 'base64',
|
|
267
266
|
filters: [
|
|
268
267
|
{
|
|
269
268
|
memcmp: {
|
|
270
269
|
offset: BigInt(0),
|
|
271
270
|
bytes: bs58.encode(Buffer.from(programClient.MARKET_ACCOUNT_DISCRIMINATOR)),
|
|
272
|
-
encoding:
|
|
271
|
+
encoding: 'base58',
|
|
273
272
|
},
|
|
274
273
|
},
|
|
275
274
|
],
|
|
@@ -305,7 +304,7 @@ export class JobsProgram extends BaseProgram {
|
|
|
305
304
|
const [associatedTokenAddress] = await findAssociatedTokenPda({
|
|
306
305
|
mint: this.sdk.config.programs.nosTokenAddress,
|
|
307
306
|
owner: this.sdk.wallet.address,
|
|
308
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS
|
|
307
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
309
308
|
});
|
|
310
309
|
try {
|
|
311
310
|
const staticAccounts = await this.getStaticAccounts();
|
|
@@ -315,17 +314,14 @@ export class JobsProgram extends BaseProgram {
|
|
|
315
314
|
market: params.market,
|
|
316
315
|
run: runKey,
|
|
317
316
|
user: associatedTokenAddress,
|
|
318
|
-
vault: await this.sdk.solana.pda([
|
|
319
|
-
params.market,
|
|
320
|
-
this.sdk.config.programs.nosTokenAddress,
|
|
321
|
-
], staticAccounts.jobsProgram),
|
|
317
|
+
vault: await this.sdk.solana.pda([params.market, this.sdk.config.programs.nosTokenAddress], staticAccounts.jobsProgram),
|
|
322
318
|
payer: this.sdk.wallet,
|
|
323
319
|
rewardsReflection: staticAccounts.rewardsReflection,
|
|
324
320
|
rewardsVault: staticAccounts.rewardsVault,
|
|
325
321
|
authority: this.sdk.wallet,
|
|
326
322
|
rewardsProgram: staticAccounts.rewardsProgram,
|
|
327
323
|
ipfsJob: bs58.decode(params.ipfsHash).subarray(2),
|
|
328
|
-
timeout: params.timeout
|
|
324
|
+
timeout: params.timeout,
|
|
329
325
|
});
|
|
330
326
|
return instruction;
|
|
331
327
|
}
|
|
@@ -336,32 +332,32 @@ export class JobsProgram extends BaseProgram {
|
|
|
336
332
|
}
|
|
337
333
|
}
|
|
338
334
|
/**
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
335
|
+
* Monitor program account updates using callback functions
|
|
336
|
+
* Uses WebSocket subscriptions with automatic restart on failure
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```typescript
|
|
340
|
+
* // Example: Monitor job accounts and save to file
|
|
341
|
+
* const stopMonitoring = await jobsProgram.monitor({
|
|
342
|
+
* onJobAccount: async (jobAccount) => {
|
|
343
|
+
* console.log('Job updated:', jobAccount.address.toString());
|
|
344
|
+
* // Save to database, file, or process as needed
|
|
345
|
+
* },
|
|
346
|
+
* onRunAccount: async (runAccount) => {
|
|
347
|
+
* console.log('Run updated:', runAccount.address.toString());
|
|
348
|
+
* },
|
|
349
|
+
* onError: async (error, accountType) => {
|
|
350
|
+
* console.error('Error processing account:', error, accountType);
|
|
351
|
+
* }
|
|
352
|
+
* });
|
|
353
|
+
*
|
|
354
|
+
* // Stop monitoring when done
|
|
355
|
+
* stopMonitoring();
|
|
356
|
+
* ```
|
|
357
|
+
*
|
|
358
|
+
* @param options Configuration options for monitoring
|
|
359
|
+
* @returns A function to stop monitoring
|
|
360
|
+
*/
|
|
365
361
|
async monitor(options = {}) {
|
|
366
362
|
const { onJobAccount, onMarketAccount, onRunAccount, onError } = options;
|
|
367
363
|
const programId = this.getProgramId();
|
|
@@ -397,13 +393,13 @@ export class JobsProgram extends BaseProgram {
|
|
|
397
393
|
}
|
|
398
394
|
if (isMonitoring) {
|
|
399
395
|
this.sdk.logger.info('Retrying WebSocket subscription in 5 seconds...');
|
|
400
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
396
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
401
397
|
}
|
|
402
398
|
}
|
|
403
399
|
}
|
|
404
400
|
};
|
|
405
401
|
// Start the subscription loop
|
|
406
|
-
startSubscription().catch(error => {
|
|
402
|
+
startSubscription().catch((error) => {
|
|
407
403
|
this.sdk.logger.error(`Failed to start subscription loop: ${error}`);
|
|
408
404
|
});
|
|
409
405
|
this.sdk.logger.info(`Successfully started monitoring job program account updates`);
|
|
@@ -464,7 +460,7 @@ export class JobsProgram extends BaseProgram {
|
|
|
464
460
|
}
|
|
465
461
|
}
|
|
466
462
|
transformJobAccount(jobAccount) {
|
|
467
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
463
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
468
464
|
const { discriminator: _, ...jobAccountData } = jobAccount.data;
|
|
469
465
|
const converted = convertBigIntToNumber(jobAccountData);
|
|
470
466
|
return {
|
|
@@ -476,7 +472,7 @@ export class JobsProgram extends BaseProgram {
|
|
|
476
472
|
};
|
|
477
473
|
}
|
|
478
474
|
transformRunAccount(runAccount) {
|
|
479
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
475
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
480
476
|
const { discriminator: _, ...runAccountData } = runAccount.data;
|
|
481
477
|
return {
|
|
482
478
|
address: runAccount.address,
|
|
@@ -484,7 +480,7 @@ export class JobsProgram extends BaseProgram {
|
|
|
484
480
|
};
|
|
485
481
|
}
|
|
486
482
|
transformMarketAccount(marketAccount) {
|
|
487
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
483
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
488
484
|
const { discriminator: _, ...marketAccountData } = marketAccount.data;
|
|
489
485
|
const converted = convertBigIntToNumber(marketAccountData);
|
|
490
486
|
return {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseProgram } from './BaseProgram.js';
|
|
2
|
+
import { Address, Account } from 'gill';
|
|
3
|
+
import { NosanaClient } from '../index.js';
|
|
4
|
+
import * as programClient from '../generated_clients/staking/index.js';
|
|
5
|
+
import { ConvertTypesForDb } from '../utils/index.js';
|
|
6
|
+
export type Stake = ConvertTypesForDb<programClient.StakeAccountArgs> & {
|
|
7
|
+
address: Address;
|
|
8
|
+
};
|
|
9
|
+
export declare class StakeProgram extends BaseProgram {
|
|
10
|
+
readonly client: typeof programClient;
|
|
11
|
+
constructor(sdk: NosanaClient);
|
|
12
|
+
protected getProgramId(): Address;
|
|
13
|
+
/**
|
|
14
|
+
* Fetch a stake account by address
|
|
15
|
+
*/
|
|
16
|
+
get(addr: Address): Promise<Stake>;
|
|
17
|
+
/**
|
|
18
|
+
* Fetch multiple stake accounts by address
|
|
19
|
+
*/
|
|
20
|
+
multiple(addresses: Address[]): Promise<Stake[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Fetch all stake accounts
|
|
23
|
+
*/
|
|
24
|
+
all(): Promise<Stake[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Transform stake account to include address and convert BigInt to numbers
|
|
27
|
+
*/
|
|
28
|
+
transformStakeAccount(stakeAccount: Account<programClient.StakeAccount>): Stake;
|
|
29
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { BaseProgram } from './BaseProgram.js';
|
|
2
|
+
import { parseBase64RpcAccount } from 'gill';
|
|
3
|
+
import * as programClient from '../generated_clients/staking/index.js';
|
|
4
|
+
import { convertBigIntToNumber } from '../utils/index.js';
|
|
5
|
+
import bs58 from 'bs58';
|
|
6
|
+
export class StakeProgram extends BaseProgram {
|
|
7
|
+
constructor(sdk) {
|
|
8
|
+
super(sdk);
|
|
9
|
+
this.client = programClient;
|
|
10
|
+
}
|
|
11
|
+
getProgramId() {
|
|
12
|
+
return this.sdk.config.programs.stakeAddress;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Fetch a stake account by address
|
|
16
|
+
*/
|
|
17
|
+
async get(addr) {
|
|
18
|
+
try {
|
|
19
|
+
const stakeAccount = await this.client.fetchStakeAccount(this.sdk.solana.rpc, addr);
|
|
20
|
+
const stake = this.transformStakeAccount(stakeAccount);
|
|
21
|
+
return stake;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
this.sdk.logger.error(`Failed to fetch stake ${err}`);
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fetch multiple stake accounts by address
|
|
30
|
+
*/
|
|
31
|
+
async multiple(addresses) {
|
|
32
|
+
try {
|
|
33
|
+
const stakeAccounts = await this.client.fetchAllStakeAccount(this.sdk.solana.rpc, addresses);
|
|
34
|
+
const stakes = stakeAccounts.map((stakeAccount) => this.transformStakeAccount(stakeAccount));
|
|
35
|
+
return stakes;
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
this.sdk.logger.error(`Failed to fetch stakes ${err}`);
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fetch all stake accounts
|
|
44
|
+
*/
|
|
45
|
+
async all() {
|
|
46
|
+
try {
|
|
47
|
+
const getProgramAccountsResponse = await this.sdk.solana.rpc
|
|
48
|
+
.getProgramAccounts(this.getProgramId(), {
|
|
49
|
+
encoding: 'base64',
|
|
50
|
+
filters: [
|
|
51
|
+
{
|
|
52
|
+
memcmp: {
|
|
53
|
+
offset: BigInt(0),
|
|
54
|
+
bytes: bs58.encode(Buffer.from(this.client.STAKE_ACCOUNT_DISCRIMINATOR)),
|
|
55
|
+
encoding: 'base58',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
})
|
|
60
|
+
.send();
|
|
61
|
+
const stakes = getProgramAccountsResponse
|
|
62
|
+
.map((result) => {
|
|
63
|
+
try {
|
|
64
|
+
const stakeAccount = programClient.decodeStakeAccount(parseBase64RpcAccount(result.pubkey, result.account));
|
|
65
|
+
return this.transformStakeAccount(stakeAccount);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
this.sdk.logger.error(`Failed to decode stake ${err}`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
.filter((account) => account !== null);
|
|
73
|
+
return stakes;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
this.sdk.logger.error(`Failed to fetch all stakes ${err}`);
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Transform stake account to include address and convert BigInt to numbers
|
|
82
|
+
*/
|
|
83
|
+
transformStakeAccount(stakeAccount) {
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
85
|
+
const { discriminator: _, ...stakeAccountData } = stakeAccount.data;
|
|
86
|
+
return {
|
|
87
|
+
address: stakeAccount.address,
|
|
88
|
+
...convertBigIntToNumber(stakeAccountData),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Address } from 'gill';
|
|
2
|
+
import { NosanaClient } from '../index.js';
|
|
3
|
+
export interface TokenAccount {
|
|
4
|
+
pubkey: Address;
|
|
5
|
+
owner: Address;
|
|
6
|
+
mint: Address;
|
|
7
|
+
amount: bigint;
|
|
8
|
+
decimals: number;
|
|
9
|
+
}
|
|
10
|
+
export interface TokenAccountWithBalance extends TokenAccount {
|
|
11
|
+
uiAmount: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class NosService {
|
|
14
|
+
private readonly sdk;
|
|
15
|
+
constructor(sdk: NosanaClient);
|
|
16
|
+
/**
|
|
17
|
+
* Get the NOS token mint address for the current network
|
|
18
|
+
*/
|
|
19
|
+
private getNosTokenMint;
|
|
20
|
+
/**
|
|
21
|
+
* Retrieve all token accounts for all NOS token holders
|
|
22
|
+
* Uses a single RPC call to fetch all accounts holding the NOS token
|
|
23
|
+
*
|
|
24
|
+
* @param options - Optional configuration
|
|
25
|
+
* @param options.includeZeroBalance - Whether to include accounts with zero balance (default: false)
|
|
26
|
+
* @param options.excludePdaAccounts - Whether to exclude PDA (Program Derived Address) accounts owned by smart contracts (default: false)
|
|
27
|
+
* @returns Array of token accounts with their balances
|
|
28
|
+
*/
|
|
29
|
+
getAllTokenHolders(options?: {
|
|
30
|
+
includeZeroBalance?: boolean;
|
|
31
|
+
excludePdaAccounts?: boolean;
|
|
32
|
+
}): Promise<TokenAccountWithBalance[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Retrieve the NOS token account for a specific owner address
|
|
35
|
+
*
|
|
36
|
+
* @param owner - The owner address to query
|
|
37
|
+
* @returns The token account with balance, or null if no account exists
|
|
38
|
+
*/
|
|
39
|
+
getTokenAccountForAddress(owner: string | Address): Promise<TokenAccountWithBalance | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Get the NOS token balance for a specific owner address
|
|
42
|
+
* Convenience method that returns just the balance
|
|
43
|
+
*
|
|
44
|
+
* @param owner - The owner address to query
|
|
45
|
+
* @returns The token balance as a UI amount (with decimals), or 0 if no account exists
|
|
46
|
+
*/
|
|
47
|
+
getBalance(owner: string | Address): Promise<number>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { address } from 'gill';
|
|
2
|
+
import { NosanaError, ErrorCodes } from '../errors/NosanaError.js';
|
|
3
|
+
import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
|
|
4
|
+
// Offset of mint address in token account data structure
|
|
5
|
+
const MINT_OFFSET = 0;
|
|
6
|
+
export class NosService {
|
|
7
|
+
constructor(sdk) {
|
|
8
|
+
this.sdk = sdk;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Get the NOS token mint address for the current network
|
|
12
|
+
*/
|
|
13
|
+
getNosTokenMint() {
|
|
14
|
+
return this.sdk.config.programs.nosTokenAddress;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Retrieve all token accounts for all NOS token holders
|
|
18
|
+
* Uses a single RPC call to fetch all accounts holding the NOS token
|
|
19
|
+
*
|
|
20
|
+
* @param options - Optional configuration
|
|
21
|
+
* @param options.includeZeroBalance - Whether to include accounts with zero balance (default: false)
|
|
22
|
+
* @param options.excludePdaAccounts - Whether to exclude PDA (Program Derived Address) accounts owned by smart contracts (default: false)
|
|
23
|
+
* @returns Array of token accounts with their balances
|
|
24
|
+
*/
|
|
25
|
+
async getAllTokenHolders(options) {
|
|
26
|
+
try {
|
|
27
|
+
const nosMint = this.getNosTokenMint();
|
|
28
|
+
this.sdk.logger.debug(`Fetching all NOS token holders for mint: ${nosMint}`);
|
|
29
|
+
// Use getProgramAccounts to fetch all token accounts for the NOS mint
|
|
30
|
+
const accounts = await this.sdk.solana.rpc
|
|
31
|
+
.getProgramAccounts(TOKEN_PROGRAM_ADDRESS, {
|
|
32
|
+
encoding: 'jsonParsed',
|
|
33
|
+
filters: [
|
|
34
|
+
{
|
|
35
|
+
memcmp: {
|
|
36
|
+
offset: BigInt(MINT_OFFSET),
|
|
37
|
+
bytes: nosMint.toString(),
|
|
38
|
+
encoding: 'base58',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
.send();
|
|
44
|
+
this.sdk.logger.info(`Found ${accounts.length} NOS token accounts`);
|
|
45
|
+
// Parse the response
|
|
46
|
+
const allAccounts = accounts.map((accountInfo) => {
|
|
47
|
+
const parsed = accountInfo.account.data.parsed.info;
|
|
48
|
+
return {
|
|
49
|
+
pubkey: accountInfo.pubkey,
|
|
50
|
+
owner: parsed.owner,
|
|
51
|
+
mint: parsed.mint,
|
|
52
|
+
amount: BigInt(parsed.tokenAmount.amount),
|
|
53
|
+
decimals: parsed.tokenAmount.decimals,
|
|
54
|
+
uiAmount: parsed.tokenAmount.uiAmount ?? 0,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
// Apply filters
|
|
58
|
+
const includeZeroBalance = options?.includeZeroBalance ?? false;
|
|
59
|
+
const excludePdaAccounts = options?.excludePdaAccounts ?? false;
|
|
60
|
+
let filteredAccounts = allAccounts;
|
|
61
|
+
// Filter out zero balance accounts unless explicitly included
|
|
62
|
+
if (!includeZeroBalance) {
|
|
63
|
+
filteredAccounts = filteredAccounts.filter((account) => account.uiAmount > 0);
|
|
64
|
+
}
|
|
65
|
+
// Filter out PDA accounts (where token account equals owner, indicating smart contract ownership)
|
|
66
|
+
if (excludePdaAccounts) {
|
|
67
|
+
const beforePdaFilter = filteredAccounts.length;
|
|
68
|
+
filteredAccounts = filteredAccounts.filter((account) => account.pubkey !== account.owner);
|
|
69
|
+
const pdaCount = beforePdaFilter - filteredAccounts.length;
|
|
70
|
+
this.sdk.logger.debug(`Filtered out ${pdaCount} PDA accounts`);
|
|
71
|
+
}
|
|
72
|
+
const filterInfo = [];
|
|
73
|
+
if (!includeZeroBalance)
|
|
74
|
+
filterInfo.push('excluding zero balances');
|
|
75
|
+
if (excludePdaAccounts)
|
|
76
|
+
filterInfo.push('excluding PDA accounts');
|
|
77
|
+
const filterText = filterInfo.length > 0 ? ` (${filterInfo.join(', ')})` : '';
|
|
78
|
+
this.sdk.logger.info(`Returning ${filteredAccounts.length} NOS token holders${filterText}`);
|
|
79
|
+
return filteredAccounts;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
this.sdk.logger.error(`Failed to fetch NOS token holders: ${error}`);
|
|
83
|
+
throw new NosanaError('Failed to fetch NOS token holders', ErrorCodes.RPC_ERROR, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Retrieve the NOS token account for a specific owner address
|
|
88
|
+
*
|
|
89
|
+
* @param owner - The owner address to query
|
|
90
|
+
* @returns The token account with balance, or null if no account exists
|
|
91
|
+
*/
|
|
92
|
+
async getTokenAccountForAddress(owner) {
|
|
93
|
+
try {
|
|
94
|
+
const ownerAddr = typeof owner === 'string' ? address(owner) : owner;
|
|
95
|
+
const nosMint = this.getNosTokenMint();
|
|
96
|
+
this.sdk.logger.debug(`Fetching NOS token account for owner: ${ownerAddr}`);
|
|
97
|
+
// Use getTokenAccountsByOwner to fetch token accounts for this owner filtered by NOS mint
|
|
98
|
+
const response = await this.sdk.solana.rpc
|
|
99
|
+
.getTokenAccountsByOwner(ownerAddr, { mint: nosMint }, { encoding: 'jsonParsed' })
|
|
100
|
+
.send();
|
|
101
|
+
if (response.value.length === 0) {
|
|
102
|
+
this.sdk.logger.debug(`No NOS token account found for owner: ${ownerAddr}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
// Typically there should only be one token account per owner per mint
|
|
106
|
+
const accountInfo = response.value[0];
|
|
107
|
+
const parsed = accountInfo.account.data.parsed.info;
|
|
108
|
+
this.sdk.logger.info(`Found NOS token account for owner ${ownerAddr}: balance = ${parsed.tokenAmount.uiAmount}`);
|
|
109
|
+
return {
|
|
110
|
+
pubkey: accountInfo.pubkey,
|
|
111
|
+
owner: parsed.owner,
|
|
112
|
+
mint: parsed.mint,
|
|
113
|
+
amount: BigInt(parsed.tokenAmount.amount),
|
|
114
|
+
decimals: parsed.tokenAmount.decimals,
|
|
115
|
+
uiAmount: parsed.tokenAmount.uiAmount ?? 0,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
this.sdk.logger.error(`Failed to fetch NOS token account for owner: ${error}`);
|
|
120
|
+
throw new NosanaError('Failed to fetch NOS token account', ErrorCodes.RPC_ERROR, error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get the NOS token balance for a specific owner address
|
|
125
|
+
* Convenience method that returns just the balance
|
|
126
|
+
*
|
|
127
|
+
* @param owner - The owner address to query
|
|
128
|
+
* @returns The token balance as a UI amount (with decimals), or 0 if no account exists
|
|
129
|
+
*/
|
|
130
|
+
async getBalance(owner) {
|
|
131
|
+
const account = await this.getTokenAccountForAddress(owner);
|
|
132
|
+
return account ? account.uiAmount : 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Address, SolanaClient, Signature, IInstruction, TransactionMessageWithBlockhashLifetime, CompilableTransactionMessage, FullySignedTransaction, TransactionWithBlockhashLifetime } from 'gill';
|
|
2
2
|
import { NosanaClient } from '../index.js';
|
|
3
|
-
export declare class
|
|
3
|
+
export declare class SolanaService {
|
|
4
4
|
private readonly sdk;
|
|
5
|
-
readonly rpc: SolanaClient[
|
|
6
|
-
readonly rpcSubscriptions: SolanaClient[
|
|
7
|
-
readonly sendAndConfirmTransaction: SolanaClient[
|
|
5
|
+
readonly rpc: SolanaClient['rpc'];
|
|
6
|
+
readonly rpcSubscriptions: SolanaClient['rpcSubscriptions'];
|
|
7
|
+
readonly sendAndConfirmTransaction: SolanaClient['sendAndConfirmTransaction'];
|
|
8
8
|
constructor(sdk: NosanaClient);
|
|
9
9
|
pda(seeds: Array<Address | string>, programId: Address): Promise<Address>;
|
|
10
10
|
getBalance(addressStr: string | Address): Promise<bigint>;
|
|
@@ -25,5 +25,5 @@ export declare class SolanaUtils {
|
|
|
25
25
|
* @param instructionsOrTransaction Single instruction, array of instructions, or pre-built transaction
|
|
26
26
|
* @returns The transaction signature
|
|
27
27
|
*/
|
|
28
|
-
send(instructionsOrTransaction: IInstruction | IInstruction[] | CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime | FullySignedTransaction & TransactionWithBlockhashLifetime): Promise<Signature>;
|
|
28
|
+
send(instructionsOrTransaction: IInstruction | IInstruction[] | (CompilableTransactionMessage & TransactionMessageWithBlockhashLifetime) | (FullySignedTransaction & TransactionWithBlockhashLifetime)): Promise<Signature>;
|
|
29
29
|
}
|