@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.
Files changed (85) hide show
  1. package/.gitlab-ci.yml +41 -9
  2. package/.prettierignore +17 -0
  3. package/README.md +729 -59
  4. package/dist/config/defaultConfigs.d.ts +1 -1
  5. package/dist/config/defaultConfigs.js +2 -2
  6. package/dist/config/types.d.ts +1 -1
  7. package/dist/config/utils.d.ts +1 -1
  8. package/dist/config/utils.js +5 -5
  9. package/dist/errors/NosanaError.d.ts +1 -1
  10. package/dist/generated_clients/jobs/instructions/assign.d.ts +79 -0
  11. package/dist/generated_clients/jobs/instructions/assign.js +120 -0
  12. package/dist/generated_clients/jobs/instructions/extend.d.ts +9 -6
  13. package/dist/generated_clients/jobs/instructions/extend.js +4 -1
  14. package/dist/generated_clients/jobs/instructions/finish.d.ts +10 -10
  15. package/dist/generated_clients/jobs/instructions/finish.js +6 -6
  16. package/dist/generated_clients/jobs/instructions/index.d.ts +1 -0
  17. package/dist/generated_clients/jobs/instructions/index.js +1 -0
  18. package/dist/generated_clients/jobs/programs/nosanaJobs.d.ts +18 -15
  19. package/dist/generated_clients/jobs/programs/nosanaJobs.js +18 -14
  20. package/dist/generated_clients/staking/accounts/index.d.ts +9 -0
  21. package/dist/generated_clients/staking/accounts/index.js +9 -0
  22. package/dist/generated_clients/staking/accounts/settingsAccount.d.ts +29 -0
  23. package/dist/generated_clients/staking/accounts/settingsAccount.js +55 -0
  24. package/dist/generated_clients/staking/accounts/stakeAccount.d.ts +39 -0
  25. package/dist/generated_clients/staking/accounts/stakeAccount.js +65 -0
  26. package/dist/generated_clients/staking/errors/index.d.ts +8 -0
  27. package/dist/generated_clients/staking/errors/index.js +8 -0
  28. package/dist/generated_clients/staking/errors/nosanaStaking.d.ts +45 -0
  29. package/dist/generated_clients/staking/errors/nosanaStaking.js +62 -0
  30. package/dist/generated_clients/staking/index.d.ts +11 -0
  31. package/dist/generated_clients/staking/index.js +11 -0
  32. package/dist/generated_clients/staking/instructions/close.d.ts +48 -0
  33. package/dist/generated_clients/staking/instructions/close.js +81 -0
  34. package/dist/generated_clients/staking/instructions/extend.d.ts +43 -0
  35. package/dist/generated_clients/staking/instructions/extend.js +73 -0
  36. package/dist/generated_clients/staking/instructions/index.d.ts +17 -0
  37. package/dist/generated_clients/staking/instructions/index.js +17 -0
  38. package/dist/generated_clients/staking/instructions/init.d.ts +45 -0
  39. package/dist/generated_clients/staking/instructions/init.js +82 -0
  40. package/dist/generated_clients/staking/instructions/restake.d.ts +42 -0
  41. package/dist/generated_clients/staking/instructions/restake.js +70 -0
  42. package/dist/generated_clients/staking/instructions/slash.d.ts +55 -0
  43. package/dist/generated_clients/staking/instructions/slash.js +90 -0
  44. package/dist/generated_clients/staking/instructions/stake.d.ts +64 -0
  45. package/dist/generated_clients/staking/instructions/stake.js +106 -0
  46. package/dist/generated_clients/staking/instructions/topup.d.ts +52 -0
  47. package/dist/generated_clients/staking/instructions/topup.js +87 -0
  48. package/dist/generated_clients/staking/instructions/unstake.d.ts +42 -0
  49. package/dist/generated_clients/staking/instructions/unstake.js +70 -0
  50. package/dist/generated_clients/staking/instructions/updateSettings.d.ts +45 -0
  51. package/dist/generated_clients/staking/instructions/updateSettings.js +73 -0
  52. package/dist/generated_clients/staking/instructions/withdraw.d.ts +48 -0
  53. package/dist/generated_clients/staking/instructions/withdraw.js +81 -0
  54. package/dist/generated_clients/staking/programs/index.d.ts +8 -0
  55. package/dist/generated_clients/staking/programs/index.js +8 -0
  56. package/dist/generated_clients/staking/programs/nosanaStaking.d.ts +53 -0
  57. package/dist/generated_clients/staking/programs/nosanaStaking.js +71 -0
  58. package/dist/generated_clients/staking/shared/index.d.ts +49 -0
  59. package/dist/generated_clients/staking/shared/index.js +86 -0
  60. package/dist/index.d.ts +12 -4
  61. package/dist/index.js +18 -136
  62. package/dist/ipfs/IPFS.js +3 -0
  63. package/dist/programs/BaseProgram.d.ts +1 -1
  64. package/dist/programs/BaseProgram.js +3 -7
  65. package/dist/programs/JobsProgram.d.ts +37 -37
  66. package/dist/programs/JobsProgram.js +63 -67
  67. package/dist/programs/StakeProgram.d.ts +29 -0
  68. package/dist/programs/StakeProgram.js +91 -0
  69. package/dist/services/NosService.d.ts +48 -0
  70. package/dist/services/NosService.js +134 -0
  71. package/dist/{solana/SolanaUtils.d.ts → services/SolanaService.d.ts} +5 -5
  72. package/dist/{solana/SolanaUtils.js → services/SolanaService.js} +17 -7
  73. package/dist/utils/index.d.ts +2 -1
  74. package/dist/utils/index.js +2 -0
  75. package/dist/utils/walletConverter.d.ts +9 -0
  76. package/dist/utils/walletConverter.js +141 -0
  77. package/eslint.config.js +49 -0
  78. package/examples/node/README.md +115 -0
  79. package/examples/node/nos-service.ts +117 -0
  80. package/examples/node/package.json +3 -1
  81. package/examples/node/retrieve.ts +2 -2
  82. package/examples/node/stake-program.ts +84 -0
  83. package/package.json +11 -8
  84. package/scripts/generate-clients.ts +20 -7
  85. 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 "../generated_clients/jobs/index.js";
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
- * Fetch a run account by address
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
- * Fetch multiple job accounts by address
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
- * Fetch all job accounts
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: "base58",
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: "base58",
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: "base58",
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: "base58",
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: "base64",
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: "base58",
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
- * Fetch all run accounts
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: "base58",
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: "base58",
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: "base64",
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: "base58",
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
- * Fetch all market accounts
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: "base64",
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: "base58",
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
- * Monitor program account updates using callback functions
340
- * Uses WebSocket subscriptions with automatic restart on failure
341
- *
342
- * @example
343
- * ```typescript
344
- * // Example: Monitor job accounts and save to file
345
- * const stopMonitoring = await jobsProgram.monitor({
346
- * onJobAccount: async (jobAccount) => {
347
- * console.log('Job updated:', jobAccount.address.toString());
348
- * // Save to database, file, or process as needed
349
- * },
350
- * onRunAccount: async (runAccount) => {
351
- * console.log('Run updated:', runAccount.address.toString());
352
- * },
353
- * onError: async (error, accountType) => {
354
- * console.error('Error processing account:', error, accountType);
355
- * }
356
- * });
357
- *
358
- * // Stop monitoring when done
359
- * stopMonitoring();
360
- * ```
361
- *
362
- * @param options Configuration options for monitoring
363
- * @returns A function to stop monitoring
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 SolanaUtils {
3
+ export declare class SolanaService {
4
4
  private readonly sdk;
5
- readonly rpc: SolanaClient["rpc"];
6
- readonly rpcSubscriptions: SolanaClient["rpcSubscriptions"];
7
- readonly sendAndConfirmTransaction: SolanaClient["sendAndConfirmTransaction"];
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
  }