@everstake/wallet-sdk-hysp-solana 1.0.0

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/src/hysp.ts ADDED
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Copyright (c) 2025, Everstake.
3
+ * Licensed under the BSD-3-Clause License. See LICENSE file for details.
4
+ */
5
+
6
+ import {
7
+ createSolanaRpc,
8
+ Address,
9
+ createNoopSigner,
10
+ address,
11
+ pipe,
12
+ createTransactionMessage,
13
+ setTransactionMessageFeePayer,
14
+ setTransactionMessageLifetimeUsingBlockhash,
15
+ appendTransactionMessageInstruction,
16
+ prependTransactionMessageInstruction,
17
+ TransactionMessage,
18
+ TransactionMessageWithLifetime,
19
+ Rpc,
20
+ SolanaRpcApi,
21
+ Instruction,
22
+ } from '@solana/kit';
23
+
24
+ import {
25
+ getSetComputeUnitLimitInstruction,
26
+ getSetComputeUnitPriceInstruction,
27
+ } from '@solana-program/compute-budget';
28
+
29
+ import { KaminoVault, VaultHoldings, APY } from '@kamino-finance/klend-sdk';
30
+
31
+ import { Decimal } from 'decimal.js';
32
+ import { Blockchain } from '../../utils';
33
+ import { ERROR_MESSAGES } from './constants/errors';
34
+ import { VAULTS, SupportedToken } from './constants';
35
+ import { ApiResponse, Params } from './types';
36
+
37
+ /**
38
+ * The `HyspSolana` class extends the `Blockchain` class and provides methods for interacting with vaults on Solana.
39
+ *
40
+ * This SDK allows users to perform vault operations such as deposits, withdrawals, and retrieving vault information.
41
+ *
42
+ * @property connection - The connection to the Solana blockchain.
43
+ * @property vault - The KaminoVault instance for vault operations.
44
+ * @property tokenSymbol - The supported token symbol for the vault.
45
+ * @property ERROR_MESSAGES - The error messages for the HYSP class.
46
+ * @property ORIGINAL_ERROR_MESSAGES - The original error messages for the HYSP class.
47
+ *
48
+ */
49
+ export class HyspSolana extends Blockchain {
50
+ protected ERROR_MESSAGES = ERROR_MESSAGES;
51
+ protected ORIGINAL_ERROR_MESSAGES = ERROR_MESSAGES;
52
+
53
+ private connection: Rpc<SolanaRpcApi>;
54
+ private vault: KaminoVault;
55
+
56
+ /**
57
+ * Creates a new instance of the KaminoSDK.
58
+ *
59
+ * @param rpcUrl - Optional custom RPC URL. If not provided, uses the default Solana mainnet RPC.
60
+ *
61
+ * @throws Throws an error if the token symbol is not supported.
62
+ * @throws Throws an error if there's an issue during SDK initialization.
63
+ */
64
+ constructor(tokenSymbol: SupportedToken, rpcUrl?: string) {
65
+ super();
66
+
67
+ const vaultAddress = this.getVaultAddress(tokenSymbol);
68
+
69
+ try {
70
+ const connectionUrl = rpcUrl || 'https://api.mainnet-beta.solana.com';
71
+ this.connection = createSolanaRpc(connectionUrl);
72
+ this.vault = new KaminoVault(this.connection, vaultAddress);
73
+ } catch (error) {
74
+ throw this.handleError('INITIALIZATION_ERROR', error);
75
+ }
76
+ }
77
+
78
+ private getVaultAddress(tokenSymbol: SupportedToken) {
79
+ const vaultAddress = VAULTS[tokenSymbol];
80
+ if (!vaultAddress) {
81
+ throw this.throwError('VAULT_NOT_FOUND_ERROR', tokenSymbol);
82
+ }
83
+
84
+ return vaultAddress;
85
+ }
86
+
87
+ private convertToDecimal(amount: number | string | bigint): Decimal {
88
+ return new Decimal(amount.toString());
89
+ }
90
+
91
+ /**
92
+ * Fetches the vault holdings data.
93
+ *
94
+ * @throws Throws an error if there's an issue loading vault holdings.
95
+ *
96
+ * @returns Returns a promise that resolves with the vault holdings.
97
+ */
98
+ async getVaultHoldings(): Promise<ApiResponse<VaultHoldings>> {
99
+ try {
100
+ const holdings = await this.vault.getVaultHoldings();
101
+
102
+ return { result: holdings };
103
+ } catch (error) {
104
+ throw this.handleError('VAULT_LOAD_ERROR', error);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Fetches the vault APY data.
110
+ *
111
+ * @throws Throws an error if there's an issue loading vault APYs.
112
+ *
113
+ * @returns Returns a promise that resolves with the vault actual APY information.
114
+ */
115
+ async getVaultAPYs(): Promise<ApiResponse<APY>> {
116
+ try {
117
+ const apys = await this.vault.getAPYs();
118
+
119
+ return { result: apys.actualAPY };
120
+ } catch (error) {
121
+ throw this.handleError('VAULT_LOAD_ERROR', error);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Fetches the current vault exchange rate (tokens per share).
127
+ *
128
+ * @throws Throws an error if there's an issue loading exchange rate.
129
+ *
130
+ * @returns Returns a promise that resolves with the exchange rate as Decimal.
131
+ */
132
+ async getExchangeRate(): Promise<ApiResponse<Decimal>> {
133
+ try {
134
+ const rate = await this.vault.getExchangeRate();
135
+
136
+ return { result: rate };
137
+ } catch (error) {
138
+ throw this.handleError('VAULT_LOAD_ERROR', error);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Fetches a user's raw share balance in the current vault.
144
+ *
145
+ * @param userAddress - The public key of the user account.
146
+ * @throws Throws an error if there's an issue fetching user shares.
147
+ * @returns Returns a promise that resolves with the user's shares amount.
148
+ */
149
+ async getUserShares(userAddress: Address): Promise<ApiResponse<Decimal>> {
150
+ try {
151
+ const shares = await this.vault.getUserShares(userAddress);
152
+
153
+ return {
154
+ result: shares.totalShares,
155
+ };
156
+ } catch (error) {
157
+ throw this.handleError('GET_SHARES_ERROR', error);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Fetches a user's token balance in the current vault: shares * exchange rate.
163
+ *
164
+ * @param userAddress - The public key of the user account.
165
+ * @throws Throws an error if there's an issue fetching user balance.
166
+ * @returns Returns a promise that resolves with the user's token balance amount.
167
+ */
168
+ async getUserBalance(userAddress: Address): Promise<ApiResponse<Decimal>> {
169
+ try {
170
+ const balance = await this.vault.getUserShares(userAddress);
171
+ const exchangeRate = await this.vault.getExchangeRate();
172
+ const tokenBalance = balance.totalShares.mul(exchangeRate);
173
+
174
+ return {
175
+ result: tokenBalance,
176
+ };
177
+ } catch (error) {
178
+ throw this.handleError('GET_BALANCE_ERROR', error);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Creates a deposit transaction to the vault.
184
+ *
185
+ * @param userAddress - The public key of the user account.
186
+ * @param amount - The amount to deposit.
187
+ * @param params - Optional transaction parameters.
188
+ *
189
+ * @throws Throws an error if there's an issue creating the deposit transaction.
190
+ *
191
+ * @returns Returns a promise that resolves with the deposit transaction response.
192
+ */
193
+ async deposit(
194
+ userAddress: Address,
195
+ amount: number | string | bigint | Decimal,
196
+ params?: Params,
197
+ ): Promise<ApiResponse<TransactionMessageWithLifetime>> {
198
+ try {
199
+ const signer = createNoopSigner(userAddress);
200
+ let decimalAmount: Decimal;
201
+ if (amount instanceof Decimal) {
202
+ decimalAmount = amount;
203
+ } else {
204
+ decimalAmount = this.convertToDecimal(amount);
205
+ }
206
+
207
+ const depositIxs = await this.vault.depositIxs(signer, decimalAmount);
208
+
209
+ const mergedDepositIxs: Instruction[] = [];
210
+
211
+ depositIxs.depositIxs.forEach((instruction) => {
212
+ mergedDepositIxs.push(instruction);
213
+ });
214
+ depositIxs.stakeInFarmIfNeededIxs.forEach((instruction) => {
215
+ mergedDepositIxs.push(instruction);
216
+ });
217
+
218
+ const transactionMessage = await this.buildTx(
219
+ userAddress.toString(),
220
+ mergedDepositIxs,
221
+ params,
222
+ );
223
+
224
+ return {
225
+ result: transactionMessage,
226
+ };
227
+ } catch (error) {
228
+ throw this.handleError('DEPOSIT_ERROR', error);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Creates a withdraw transaction from the vault.
234
+ *
235
+ * @param userAddress - The public key of the user account.
236
+ * @param sharesAmount - The amount of shares to withdraw.
237
+ * @param params - Optional transaction parameters.
238
+ *
239
+ * @throws Throws an error if there's issue creating the withdraw transaction.
240
+ *
241
+ * @returns Returns a promise that resolves with the withdraw transaction response.
242
+ */
243
+ async withdraw(
244
+ userAddress: Address,
245
+ sharesAmount: number | string | bigint | Decimal,
246
+ params?: Params,
247
+ ): Promise<ApiResponse<TransactionMessageWithLifetime>> {
248
+ try {
249
+ const signer = createNoopSigner(userAddress);
250
+ let sharesDecimal: Decimal;
251
+ if (sharesAmount instanceof Decimal) {
252
+ sharesDecimal = sharesAmount;
253
+ } else {
254
+ sharesDecimal = this.convertToDecimal(sharesAmount);
255
+ }
256
+
257
+ const withdrawIxs = await this.vault.withdrawIxs(signer, sharesDecimal);
258
+
259
+ const mergedWithdrawIxs: Instruction[] = [];
260
+
261
+ withdrawIxs.unstakeFromFarmIfNeededIxs.forEach((instruction) => {
262
+ mergedWithdrawIxs.push(instruction);
263
+ });
264
+ withdrawIxs.withdrawIxs.forEach((instruction) => {
265
+ mergedWithdrawIxs.push(instruction);
266
+ });
267
+ withdrawIxs.postWithdrawIxs.forEach((instruction) => {
268
+ mergedWithdrawIxs.push(instruction);
269
+ });
270
+
271
+ const transactionMessage = await this.buildTx(
272
+ userAddress.toString(),
273
+ mergedWithdrawIxs,
274
+ params,
275
+ );
276
+
277
+ return {
278
+ result: transactionMessage,
279
+ };
280
+ } catch (error) {
281
+ throw this.handleError('WITHDRAW_ERROR', error);
282
+ }
283
+ }
284
+
285
+ private async buildTx(
286
+ sender: string,
287
+ instructions: Instruction[],
288
+ params?: Params,
289
+ ): Promise<TransactionMessageWithLifetime> {
290
+ let transactionMessage: TransactionMessage = pipe(
291
+ createTransactionMessage({ version: 0 }),
292
+ (tx) => setTransactionMessageFeePayer(address(sender), tx),
293
+ );
294
+
295
+ if (
296
+ params?.computeUnitLimit !== undefined &&
297
+ params?.computeUnitLimit > 0
298
+ ) {
299
+ const unitLimitInstruction = getSetComputeUnitLimitInstruction({
300
+ /** Transaction compute unit limit used for prioritization fees. */
301
+ units: params?.computeUnitLimit,
302
+ });
303
+
304
+ transactionMessage = prependTransactionMessageInstruction(
305
+ unitLimitInstruction,
306
+ transactionMessage,
307
+ );
308
+ }
309
+
310
+ if (
311
+ params?.сomputeUnitPrice !== undefined &&
312
+ params?.сomputeUnitPrice > 0
313
+ ) {
314
+ const unitPriceInstruction = getSetComputeUnitPriceInstruction({
315
+ /** Transaction compute unit price used for prioritization fees. */
316
+ microLamports: params?.сomputeUnitPrice,
317
+ });
318
+ transactionMessage = prependTransactionMessageInstruction(
319
+ unitPriceInstruction,
320
+ transactionMessage,
321
+ );
322
+ }
323
+
324
+ for (const instruction of instructions) {
325
+ transactionMessage = appendTransactionMessageInstruction(
326
+ instruction,
327
+ transactionMessage,
328
+ );
329
+ }
330
+
331
+ if (params?.afterInstructions) {
332
+ for (const instruction of params.afterInstructions) {
333
+ transactionMessage = appendTransactionMessageInstruction(
334
+ instruction,
335
+ transactionMessage,
336
+ );
337
+ }
338
+ }
339
+
340
+ const finalLatestBlockhash =
341
+ params?.finalLatestBlockhash ||
342
+ (await this.connection.getLatestBlockhash().send()).value;
343
+
344
+ const txMessageWithBlockhashLifetime =
345
+ setTransactionMessageLifetimeUsingBlockhash(
346
+ finalLatestBlockhash,
347
+ transactionMessage,
348
+ );
349
+
350
+ return txMessageWithBlockhashLifetime;
351
+ }
352
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright (c) 2025, Everstake.
3
+ * Licensed under the BSD-3-Clause License. See LICENSE file for details.
4
+ */
5
+
6
+ export * from './hysp';
7
+
8
+ export * from '../../utils';
9
+
10
+ export * from './constants';
11
+
12
+ export type { Params, ApiResponse } from './types';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Copyright (c) 2025, Everstake.
3
+ * Licensed under the BSD-3-Clause License. See LICENSE file for details.
4
+ */
5
+
6
+ import { Blockhash, Instruction } from '@solana/kit';
7
+
8
+ export interface ApiResponse<T> {
9
+ result: T;
10
+ }
11
+
12
+ export type Params = {
13
+ сomputeUnitPrice?: bigint;
14
+ computeUnitLimit?: number;
15
+ finalLatestBlockhash?: {
16
+ /** a Hash as base-58 encoded string */
17
+ blockhash: Blockhash;
18
+ /** last block height at which the blockhash will be valid */
19
+ lastValidBlockHeight: bigint;
20
+ };
21
+ /** Instructions to be added after the main instructions created by SDK */
22
+ afterInstructions?: Instruction[];
23
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "skipLibCheck": true,
5
+ "target": "es2022",
6
+ "verbatimModuleSyntax": false,
7
+ "allowJs": true,
8
+ "resolveJsonModule": true,
9
+ "moduleDetection": "force",
10
+ "strict": true,
11
+ "noUncheckedIndexedAccess": true,
12
+ "moduleResolution": "Bundler",
13
+ "module": "ESNext",
14
+ "noEmit": true,
15
+ "lib": ["es2022", "dom", "dom.iterable"]
16
+ },
17
+ "include": ["src/**/*", "../.vscode/__tests__"]
18
+ }